addon-sdk/source/lib/sdk/selection.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/addon-sdk/source/lib/sdk/selection.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,467 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +module.metadata = {
    1.11 +  "stability": "stable",
    1.12 +  "engines": {
    1.13 +    "Firefox": "*"
    1.14 +  }
    1.15 +};
    1.16 +
    1.17 +const { Ci, Cc } = require("chrome"),
    1.18 +    { setTimeout } = require("./timers"),
    1.19 +    { emit, off } = require("./event/core"),
    1.20 +    { Class, obscure } = require("./core/heritage"),
    1.21 +    { EventTarget } = require("./event/target"),
    1.22 +    { ns } = require("./core/namespace"),
    1.23 +    { when: unload } = require("./system/unload"),
    1.24 +    { ignoreWindow } = require('./private-browsing/utils'),
    1.25 +    { getTabs, getTabContentWindow, getTabForContentWindow,
    1.26 +      getAllTabContentWindows } = require('./tabs/utils'),
    1.27 +    winUtils = require("./window/utils"),
    1.28 +    events = require("./system/events"),
    1.29 +    { iteratorSymbol, forInIterator } = require("./util/iteration");
    1.30 +
    1.31 +// The selection types
    1.32 +const HTML = 0x01,
    1.33 +      TEXT = 0x02,
    1.34 +      DOM  = 0x03; // internal use only
    1.35 +
    1.36 +// A more developer-friendly message than the caught exception when is not
    1.37 +// possible change a selection.
    1.38 +const ERR_CANNOT_CHANGE_SELECTION =
    1.39 +  "It isn't possible to change the selection, as there isn't currently a selection";
    1.40 +
    1.41 +const selections = ns();
    1.42 +
    1.43 +const Selection = Class({
    1.44 +  /**
    1.45 +   * Creates an object from which a selection can be set, get, etc. Each
    1.46 +   * object has an associated with a range number. Range numbers are the
    1.47 +   * 0-indexed counter of selection ranges as explained at
    1.48 +   * https://developer.mozilla.org/en/DOM/Selection.
    1.49 +   *
    1.50 +   * @param rangeNumber
    1.51 +   *        The zero-based range index into the selection
    1.52 +   */
    1.53 +  initialize: function initialize(rangeNumber) {
    1.54 +    // In order to hide the private `rangeNumber` argument from API consumers
    1.55 +    // while still enabling Selection getters/setters to access it, we define
    1.56 +    // it as non enumerable, non configurable property. While consumers still
    1.57 +    // may discover it they won't be able to do any harm which is good enough
    1.58 +    // in this case.
    1.59 +    Object.defineProperties(this, {
    1.60 +      rangeNumber: {
    1.61 +        enumerable: false,
    1.62 +        configurable: false,
    1.63 +        value: rangeNumber
    1.64 +      }
    1.65 +    });
    1.66 +  },
    1.67 +  get text() { return getSelection(TEXT, this.rangeNumber); },
    1.68 +  set text(value) { setSelection(TEXT, value, this.rangeNumber); },
    1.69 +  get html() { return getSelection(HTML, this.rangeNumber); },
    1.70 +  set html(value) { setSelection(HTML, value, this.rangeNumber); },
    1.71 +  get isContiguous() {
    1.72 +
    1.73 +    // If there are multiple non empty ranges, the selection is definitely
    1.74 +    // discontiguous. It returns `false` also if there are no valid selection.
    1.75 +    let count = 0;
    1.76 +    for (let sel in selectionIterator)
    1.77 +      if (++count > 1)
    1.78 +        break;
    1.79 +
    1.80 +    return count === 1;
    1.81 +  }
    1.82 +});
    1.83 +
    1.84 +const selectionListener = {
    1.85 +  notifySelectionChanged: function (document, selection, reason) {
    1.86 +    if (!["SELECTALL", "KEYPRESS", "MOUSEUP"].some(function(type) reason &
    1.87 +      Ci.nsISelectionListener[type + "_REASON"]) || selection.toString() == "")
    1.88 +        return;
    1.89 +
    1.90 +    this.onSelect();
    1.91 +  },
    1.92 +
    1.93 +  onSelect: function() {
    1.94 +    emit(module.exports, "select");
    1.95 +  }
    1.96 +}
    1.97 +
    1.98 +/**
    1.99 + * Defines iterators so that discontiguous selections can be iterated.
   1.100 + * Empty selections are skipped - see `safeGetRange` for further details.
   1.101 + *
   1.102 + * If discontiguous selections are in a text field, only the first one
   1.103 + * is returned because the text field selection APIs doesn't support
   1.104 + * multiple selections.
   1.105 + */
   1.106 +function* forOfIterator() {
   1.107 +  let selection = getSelection(DOM);
   1.108 +  let count = 0;
   1.109 +
   1.110 +  if (selection)
   1.111 +    count = selection.rangeCount || (getElementWithSelection() ? 1 : 0);
   1.112 +
   1.113 +  for (let i = 0; i < count; i++) {
   1.114 +    let sel = Selection(i);
   1.115 +
   1.116 +    if (sel.text)
   1.117 +      yield Selection(i);
   1.118 +  }
   1.119 +}
   1.120 +
   1.121 +const selectionIteratorOptions = {
   1.122 +  __iterator__: forInIterator
   1.123 +}
   1.124 +selectionIteratorOptions[iteratorSymbol] = forOfIterator;
   1.125 +const selectionIterator = obscure(selectionIteratorOptions);
   1.126 +
   1.127 +/**
   1.128 + * Returns the most recent focused window.
   1.129 + * if private browsing window is most recent and not supported,
   1.130 + * then ignore it and return `null`, because the focused window
   1.131 + * can't be targeted.
   1.132 + */
   1.133 +function getFocusedWindow() {
   1.134 +  let window = winUtils.getFocusedWindow();
   1.135 +
   1.136 +  return ignoreWindow(window) ? null : window;
   1.137 +}
   1.138 +
   1.139 +/**
   1.140 + * Returns the focused element in the most recent focused window
   1.141 + * if private browsing window is most recent and not supported,
   1.142 + * then ignore it and return `null`, because the focused element
   1.143 + * can't be targeted.
   1.144 + */
   1.145 +function getFocusedElement() {
   1.146 +  let element = winUtils.getFocusedElement();
   1.147 +
   1.148 +  if (!element || ignoreWindow(element.ownerDocument.defaultView))
   1.149 +    return null;
   1.150 +
   1.151 +  return element;
   1.152 +}
   1.153 +
   1.154 +/**
   1.155 + * Returns the current selection from most recent content window. Depending on
   1.156 + * the specified |type|, the value returned can be a string of text, stringified
   1.157 + * HTML, or a DOM selection object as described at
   1.158 + * https://developer.mozilla.org/en/DOM/Selection.
   1.159 + *
   1.160 + * @param type
   1.161 + *        Specifies the return type of the selection. Valid values are the one
   1.162 + *        of the constants HTML, TEXT, or DOM.
   1.163 + *
   1.164 + * @param rangeNumber
   1.165 + *        Specifies the zero-based range index of the returned selection.
   1.166 + */
   1.167 +function getSelection(type, rangeNumber) {
   1.168 +  let window, selection;
   1.169 +  try {
   1.170 +    window = getFocusedWindow();
   1.171 +    selection = window.getSelection();
   1.172 +  }
   1.173 +  catch (e) {
   1.174 +    return null;
   1.175 +  }
   1.176 +
   1.177 +  // Get the selected content as the specified type
   1.178 +  if (type == DOM) {
   1.179 +    return selection;
   1.180 +  }
   1.181 +  else if (type == TEXT) {
   1.182 +    let range = safeGetRange(selection, rangeNumber);
   1.183 +
   1.184 +    if (range)
   1.185 +      return range.toString();
   1.186 +
   1.187 +    let node = getElementWithSelection();
   1.188 +
   1.189 +    if (!node)
   1.190 +      return null;
   1.191 +
   1.192 +    return node.value.substring(node.selectionStart, node.selectionEnd);
   1.193 +  }
   1.194 +  else if (type == HTML) {
   1.195 +    let range = safeGetRange(selection, rangeNumber);
   1.196 +    // Another way, but this includes the xmlns attribute for all elements in
   1.197 +    // Gecko 1.9.2+ :
   1.198 +    // return Cc["@mozilla.org/xmlextras/xmlserializer;1"].
   1.199 +    //   createInstance(Ci.nsIDOMSerializer).serializeToSTring(range.
   1.200 +    //     cloneContents());
   1.201 +    if (!range)
   1.202 +      return null;
   1.203 +
   1.204 +    let node = window.document.createElement("span");
   1.205 +    node.appendChild(range.cloneContents());
   1.206 +    return node.innerHTML;
   1.207 +  }
   1.208 +
   1.209 +  throw new Error("Type " + type + " is unrecognized.");
   1.210 +}
   1.211 +
   1.212 +/**
   1.213 + * Sets the current selection of the most recent content document by changing
   1.214 + * the existing selected text/HTML range to the specified value.
   1.215 + *
   1.216 + * @param val
   1.217 + *        The value for the new selection
   1.218 + *
   1.219 + * @param rangeNumber
   1.220 + *        The zero-based range index of the selection to be set
   1.221 + *
   1.222 + */
   1.223 +function setSelection(type, val, rangeNumber) {
   1.224 +  // Make sure we have a window context & that there is a current selection.
   1.225 +  // Selection cannot be set unless there is an existing selection.
   1.226 +  let window, selection;
   1.227 +
   1.228 +  try {
   1.229 +    window = getFocusedWindow();
   1.230 +    selection = window.getSelection();
   1.231 +  }
   1.232 +  catch (e) {
   1.233 +    throw new Error(ERR_CANNOT_CHANGE_SELECTION);
   1.234 +  }
   1.235 +
   1.236 +  let range = safeGetRange(selection, rangeNumber);
   1.237 +
   1.238 +  if (range) {
   1.239 +    let fragment;
   1.240 +
   1.241 +    if (type === HTML)
   1.242 +      fragment = range.createContextualFragment(val);
   1.243 +    else {
   1.244 +      fragment = range.createContextualFragment("");
   1.245 +      fragment.textContent = val;
   1.246 +    }
   1.247 +
   1.248 +    range.deleteContents();
   1.249 +    range.insertNode(fragment);
   1.250 +  }
   1.251 +  else {
   1.252 +    let node = getElementWithSelection();
   1.253 +
   1.254 +    if (!node)
   1.255 +      throw new Error(ERR_CANNOT_CHANGE_SELECTION);
   1.256 +
   1.257 +    let { value, selectionStart, selectionEnd } = node;
   1.258 +
   1.259 +    let newSelectionEnd = selectionStart + val.length;
   1.260 +
   1.261 +    node.value = value.substring(0, selectionStart) +
   1.262 +                  val +
   1.263 +                  value.substring(selectionEnd, value.length);
   1.264 +
   1.265 +    node.setSelectionRange(selectionStart, newSelectionEnd);
   1.266 +  }
   1.267 +}
   1.268 +
   1.269 +/**
   1.270 + * Returns the specified range in a selection without throwing an exception.
   1.271 + *
   1.272 + * @param selection
   1.273 + *        A selection object as described at
   1.274 + *         https://developer.mozilla.org/en/DOM/Selection
   1.275 + *
   1.276 + * @param [rangeNumber]
   1.277 + *        Specifies the zero-based range index of the returned selection.
   1.278 + *        If it's not provided the function will return the first non empty
   1.279 + *        range, if any.
   1.280 + */
   1.281 +function safeGetRange(selection, rangeNumber) {
   1.282 +  try {
   1.283 +    let { rangeCount } = selection;
   1.284 +    let range = null;
   1.285 +
   1.286 +    if (typeof rangeNumber === "undefined")
   1.287 +      rangeNumber = 0;
   1.288 +    else
   1.289 +      rangeCount = rangeNumber + 1;
   1.290 +
   1.291 +    for (; rangeNumber < rangeCount; rangeNumber++ ) {
   1.292 +      range = selection.getRangeAt(rangeNumber);
   1.293 +
   1.294 +      if (range && range.toString())
   1.295 +        break;
   1.296 +
   1.297 +      range = null;
   1.298 +    }
   1.299 +
   1.300 +    return range;
   1.301 +  }
   1.302 +  catch (e) {
   1.303 +    return null;
   1.304 +  }
   1.305 +}
   1.306 +
   1.307 +/**
   1.308 + * Returns a reference of the DOM's active element for the window given, if it
   1.309 + * supports the text field selection API and has a text selected.
   1.310 + *
   1.311 + * Note:
   1.312 + *   we need this method because window.getSelection doesn't return a selection
   1.313 + *   for text selected in a form field (see bug 85686)
   1.314 + */
   1.315 +function getElementWithSelection() {
   1.316 +  let element = getFocusedElement();
   1.317 +
   1.318 +  if (!element)
   1.319 +    return null;
   1.320 +
   1.321 +  try {
   1.322 +    // Accessing selectionStart and selectionEnd on e.g. a button
   1.323 +    // results in an exception thrown as per the HTML5 spec.  See
   1.324 +    // http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#textFieldSelection
   1.325 +
   1.326 +    let { value, selectionStart, selectionEnd } = element;
   1.327 +
   1.328 +    let hasSelection = typeof value === "string" &&
   1.329 +                      !isNaN(selectionStart) &&
   1.330 +                      !isNaN(selectionEnd) &&
   1.331 +                      selectionStart !== selectionEnd;
   1.332 +
   1.333 +    return hasSelection ? element : null;
   1.334 +  }
   1.335 +  catch (err) {
   1.336 +    return null;
   1.337 +  }
   1.338 +
   1.339 +}
   1.340 +
   1.341 +/**
   1.342 + * Adds the Selection Listener to the content's window given
   1.343 + */
   1.344 +function addSelectionListener(window) {
   1.345 +  let selection = window.getSelection();
   1.346 +
   1.347 +  // Don't add the selection's listener more than once to the same window,
   1.348 +  // if the selection object is the same
   1.349 +  if ("selection" in selections(window) && selections(window).selection === selection)
   1.350 +    return;
   1.351 +
   1.352 +  // We ensure that the current selection is an instance of
   1.353 +  // `nsISelectionPrivate` before working on it, in case is `null`.
   1.354 +  //
   1.355 +  // If it's `null` it's likely too early to add the listener, and we demand
   1.356 +  // that operation to `document-shown` - it can easily happens for frames
   1.357 +  if (selection instanceof Ci.nsISelectionPrivate)
   1.358 +    selection.addSelectionListener(selectionListener);
   1.359 +
   1.360 +  // nsISelectionListener implementation seems not fire a notification if
   1.361 +  // a selection is in a text field, therefore we need to add a listener to
   1.362 +  // window.onselect, that is fired only for text fields.
   1.363 +  // For consistency, we add it only when the nsISelectionListener is added.
   1.364 +  //
   1.365 +  // https://developer.mozilla.org/en/DOM/window.onselect
   1.366 +  window.addEventListener("select", selectionListener.onSelect, true);
   1.367 +
   1.368 +  selections(window).selection = selection;
   1.369 +};
   1.370 +
   1.371 +/**
   1.372 + * Removes the Selection Listener to the content's window given
   1.373 + */
   1.374 +function removeSelectionListener(window) {
   1.375 +  // Don't remove the selection's listener to a window that wasn't handled.
   1.376 +  if (!("selection" in selections(window)))
   1.377 +    return;
   1.378 +
   1.379 +  let selection = window.getSelection();
   1.380 +  let isSameSelection = selection === selections(window).selection;
   1.381 +
   1.382 +  // Before remove the listener, we ensure that the current selection is an
   1.383 +  // instance of `nsISelectionPrivate` (it could be `null`), and that is still
   1.384 +  // the selection we managed for this window (it could be detached).
   1.385 +  if (selection instanceof Ci.nsISelectionPrivate && isSameSelection)
   1.386 +    selection.removeSelectionListener(selectionListener);
   1.387 +
   1.388 +  window.removeEventListener("select", selectionListener.onSelect, true);
   1.389 +
   1.390 +  delete selections(window).selection;
   1.391 +};
   1.392 +
   1.393 +function onContent(event) {
   1.394 +  let window = event.subject.defaultView;
   1.395 +
   1.396 +  // We are not interested in documents without valid defaultView (e.g. XML)
   1.397 +  // that aren't in a tab (e.g. Panel); or in private windows
   1.398 +   if (window && getTabForContentWindow(window) && !ignoreWindow(window)) {
   1.399 +    addSelectionListener(window);
   1.400 +  }
   1.401 +}
   1.402 +
   1.403 +// Adds Selection listener to new documents
   1.404 +// Note that strong reference is needed for documents that are loading slowly or
   1.405 +// where the server didn't close the connection (e.g. "comet").
   1.406 +events.on("document-element-inserted", onContent, true);
   1.407 +
   1.408 +// Adds Selection listeners to existing documents
   1.409 +getAllTabContentWindows().forEach(addSelectionListener);
   1.410 +
   1.411 +// When a document is not visible anymore the selection object is detached, and
   1.412 +// a new selection object is created when it becomes visible again.
   1.413 +// That makes the previous selection's listeners added previously totally
   1.414 +// useless – the listeners are not notified anymore.
   1.415 +// To fix that we're listening for `document-shown` event in order to add
   1.416 +// the listeners to the new selection object created.
   1.417 +//
   1.418 +// See bug 665386 for further details.
   1.419 +
   1.420 +function onShown(event) {
   1.421 +  let window = event.subject.defaultView;
   1.422 +
   1.423 +  // We are not interested in documents without valid defaultView.
   1.424 +  // For example XML documents don't have windows and we don't yet support them.
   1.425 +  if (!window)
   1.426 +    return;
   1.427 +
   1.428 +  // We want to handle only the windows where we added selection's listeners
   1.429 +  if ("selection" in selections(window)) {
   1.430 +    let currentSelection = window.getSelection();
   1.431 +    let { selection } = selections(window);
   1.432 +
   1.433 +    // If the current selection for the window given is different from the one
   1.434 +    // stored in the namespace, we need to add the listeners again, and replace
   1.435 +    // the previous selection in our list with the new one.
   1.436 +    //
   1.437 +    // Notice that we don't have to remove the listeners from the old selection,
   1.438 +    // because is detached. An attempt to remove the listener, will raise an
   1.439 +    // error (see http://mxr.mozilla.org/mozilla-central/source/layout/generic/nsSelection.cpp#5343 )
   1.440 +    //
   1.441 +    // We ensure that the current selection is an instance of
   1.442 +    // `nsISelectionPrivate` before working on it, in case is `null`.
   1.443 +    if (currentSelection instanceof Ci.nsISelectionPrivate &&
   1.444 +      currentSelection !== selection) {
   1.445 +
   1.446 +      window.addEventListener("select", selectionListener.onSelect, true);
   1.447 +      currentSelection.addSelectionListener(selectionListener);
   1.448 +      selections(window).selection = currentSelection;
   1.449 +    }
   1.450 +  }
   1.451 +}
   1.452 +
   1.453 +events.on("document-shown", onShown, true);
   1.454 +
   1.455 +// Removes Selection listeners when the add-on is unloaded
   1.456 +unload(function(){
   1.457 +  getAllTabContentWindows().forEach(removeSelectionListener);
   1.458 +
   1.459 +  events.off("document-element-inserted", onContent);
   1.460 +  events.off("document-shown", onShown);
   1.461 +
   1.462 +  off(exports);
   1.463 +});
   1.464 +
   1.465 +const selection = Class({
   1.466 +  extends: EventTarget,
   1.467 +  implements: [ Selection, selectionIterator ]
   1.468 +})();
   1.469 +
   1.470 +module.exports = selection;

mercurial