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

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 module.metadata = {
     8   "stability": "stable",
     9   "engines": {
    10     "Firefox": "*"
    11   }
    12 };
    14 const { Ci, Cc } = require("chrome"),
    15     { setTimeout } = require("./timers"),
    16     { emit, off } = require("./event/core"),
    17     { Class, obscure } = require("./core/heritage"),
    18     { EventTarget } = require("./event/target"),
    19     { ns } = require("./core/namespace"),
    20     { when: unload } = require("./system/unload"),
    21     { ignoreWindow } = require('./private-browsing/utils'),
    22     { getTabs, getTabContentWindow, getTabForContentWindow,
    23       getAllTabContentWindows } = require('./tabs/utils'),
    24     winUtils = require("./window/utils"),
    25     events = require("./system/events"),
    26     { iteratorSymbol, forInIterator } = require("./util/iteration");
    28 // The selection types
    29 const HTML = 0x01,
    30       TEXT = 0x02,
    31       DOM  = 0x03; // internal use only
    33 // A more developer-friendly message than the caught exception when is not
    34 // possible change a selection.
    35 const ERR_CANNOT_CHANGE_SELECTION =
    36   "It isn't possible to change the selection, as there isn't currently a selection";
    38 const selections = ns();
    40 const Selection = Class({
    41   /**
    42    * Creates an object from which a selection can be set, get, etc. Each
    43    * object has an associated with a range number. Range numbers are the
    44    * 0-indexed counter of selection ranges as explained at
    45    * https://developer.mozilla.org/en/DOM/Selection.
    46    *
    47    * @param rangeNumber
    48    *        The zero-based range index into the selection
    49    */
    50   initialize: function initialize(rangeNumber) {
    51     // In order to hide the private `rangeNumber` argument from API consumers
    52     // while still enabling Selection getters/setters to access it, we define
    53     // it as non enumerable, non configurable property. While consumers still
    54     // may discover it they won't be able to do any harm which is good enough
    55     // in this case.
    56     Object.defineProperties(this, {
    57       rangeNumber: {
    58         enumerable: false,
    59         configurable: false,
    60         value: rangeNumber
    61       }
    62     });
    63   },
    64   get text() { return getSelection(TEXT, this.rangeNumber); },
    65   set text(value) { setSelection(TEXT, value, this.rangeNumber); },
    66   get html() { return getSelection(HTML, this.rangeNumber); },
    67   set html(value) { setSelection(HTML, value, this.rangeNumber); },
    68   get isContiguous() {
    70     // If there are multiple non empty ranges, the selection is definitely
    71     // discontiguous. It returns `false` also if there are no valid selection.
    72     let count = 0;
    73     for (let sel in selectionIterator)
    74       if (++count > 1)
    75         break;
    77     return count === 1;
    78   }
    79 });
    81 const selectionListener = {
    82   notifySelectionChanged: function (document, selection, reason) {
    83     if (!["SELECTALL", "KEYPRESS", "MOUSEUP"].some(function(type) reason &
    84       Ci.nsISelectionListener[type + "_REASON"]) || selection.toString() == "")
    85         return;
    87     this.onSelect();
    88   },
    90   onSelect: function() {
    91     emit(module.exports, "select");
    92   }
    93 }
    95 /**
    96  * Defines iterators so that discontiguous selections can be iterated.
    97  * Empty selections are skipped - see `safeGetRange` for further details.
    98  *
    99  * If discontiguous selections are in a text field, only the first one
   100  * is returned because the text field selection APIs doesn't support
   101  * multiple selections.
   102  */
   103 function* forOfIterator() {
   104   let selection = getSelection(DOM);
   105   let count = 0;
   107   if (selection)
   108     count = selection.rangeCount || (getElementWithSelection() ? 1 : 0);
   110   for (let i = 0; i < count; i++) {
   111     let sel = Selection(i);
   113     if (sel.text)
   114       yield Selection(i);
   115   }
   116 }
   118 const selectionIteratorOptions = {
   119   __iterator__: forInIterator
   120 }
   121 selectionIteratorOptions[iteratorSymbol] = forOfIterator;
   122 const selectionIterator = obscure(selectionIteratorOptions);
   124 /**
   125  * Returns the most recent focused window.
   126  * if private browsing window is most recent and not supported,
   127  * then ignore it and return `null`, because the focused window
   128  * can't be targeted.
   129  */
   130 function getFocusedWindow() {
   131   let window = winUtils.getFocusedWindow();
   133   return ignoreWindow(window) ? null : window;
   134 }
   136 /**
   137  * Returns the focused element in the most recent focused window
   138  * if private browsing window is most recent and not supported,
   139  * then ignore it and return `null`, because the focused element
   140  * can't be targeted.
   141  */
   142 function getFocusedElement() {
   143   let element = winUtils.getFocusedElement();
   145   if (!element || ignoreWindow(element.ownerDocument.defaultView))
   146     return null;
   148   return element;
   149 }
   151 /**
   152  * Returns the current selection from most recent content window. Depending on
   153  * the specified |type|, the value returned can be a string of text, stringified
   154  * HTML, or a DOM selection object as described at
   155  * https://developer.mozilla.org/en/DOM/Selection.
   156  *
   157  * @param type
   158  *        Specifies the return type of the selection. Valid values are the one
   159  *        of the constants HTML, TEXT, or DOM.
   160  *
   161  * @param rangeNumber
   162  *        Specifies the zero-based range index of the returned selection.
   163  */
   164 function getSelection(type, rangeNumber) {
   165   let window, selection;
   166   try {
   167     window = getFocusedWindow();
   168     selection = window.getSelection();
   169   }
   170   catch (e) {
   171     return null;
   172   }
   174   // Get the selected content as the specified type
   175   if (type == DOM) {
   176     return selection;
   177   }
   178   else if (type == TEXT) {
   179     let range = safeGetRange(selection, rangeNumber);
   181     if (range)
   182       return range.toString();
   184     let node = getElementWithSelection();
   186     if (!node)
   187       return null;
   189     return node.value.substring(node.selectionStart, node.selectionEnd);
   190   }
   191   else if (type == HTML) {
   192     let range = safeGetRange(selection, rangeNumber);
   193     // Another way, but this includes the xmlns attribute for all elements in
   194     // Gecko 1.9.2+ :
   195     // return Cc["@mozilla.org/xmlextras/xmlserializer;1"].
   196     //   createInstance(Ci.nsIDOMSerializer).serializeToSTring(range.
   197     //     cloneContents());
   198     if (!range)
   199       return null;
   201     let node = window.document.createElement("span");
   202     node.appendChild(range.cloneContents());
   203     return node.innerHTML;
   204   }
   206   throw new Error("Type " + type + " is unrecognized.");
   207 }
   209 /**
   210  * Sets the current selection of the most recent content document by changing
   211  * the existing selected text/HTML range to the specified value.
   212  *
   213  * @param val
   214  *        The value for the new selection
   215  *
   216  * @param rangeNumber
   217  *        The zero-based range index of the selection to be set
   218  *
   219  */
   220 function setSelection(type, val, rangeNumber) {
   221   // Make sure we have a window context & that there is a current selection.
   222   // Selection cannot be set unless there is an existing selection.
   223   let window, selection;
   225   try {
   226     window = getFocusedWindow();
   227     selection = window.getSelection();
   228   }
   229   catch (e) {
   230     throw new Error(ERR_CANNOT_CHANGE_SELECTION);
   231   }
   233   let range = safeGetRange(selection, rangeNumber);
   235   if (range) {
   236     let fragment;
   238     if (type === HTML)
   239       fragment = range.createContextualFragment(val);
   240     else {
   241       fragment = range.createContextualFragment("");
   242       fragment.textContent = val;
   243     }
   245     range.deleteContents();
   246     range.insertNode(fragment);
   247   }
   248   else {
   249     let node = getElementWithSelection();
   251     if (!node)
   252       throw new Error(ERR_CANNOT_CHANGE_SELECTION);
   254     let { value, selectionStart, selectionEnd } = node;
   256     let newSelectionEnd = selectionStart + val.length;
   258     node.value = value.substring(0, selectionStart) +
   259                   val +
   260                   value.substring(selectionEnd, value.length);
   262     node.setSelectionRange(selectionStart, newSelectionEnd);
   263   }
   264 }
   266 /**
   267  * Returns the specified range in a selection without throwing an exception.
   268  *
   269  * @param selection
   270  *        A selection object as described at
   271  *         https://developer.mozilla.org/en/DOM/Selection
   272  *
   273  * @param [rangeNumber]
   274  *        Specifies the zero-based range index of the returned selection.
   275  *        If it's not provided the function will return the first non empty
   276  *        range, if any.
   277  */
   278 function safeGetRange(selection, rangeNumber) {
   279   try {
   280     let { rangeCount } = selection;
   281     let range = null;
   283     if (typeof rangeNumber === "undefined")
   284       rangeNumber = 0;
   285     else
   286       rangeCount = rangeNumber + 1;
   288     for (; rangeNumber < rangeCount; rangeNumber++ ) {
   289       range = selection.getRangeAt(rangeNumber);
   291       if (range && range.toString())
   292         break;
   294       range = null;
   295     }
   297     return range;
   298   }
   299   catch (e) {
   300     return null;
   301   }
   302 }
   304 /**
   305  * Returns a reference of the DOM's active element for the window given, if it
   306  * supports the text field selection API and has a text selected.
   307  *
   308  * Note:
   309  *   we need this method because window.getSelection doesn't return a selection
   310  *   for text selected in a form field (see bug 85686)
   311  */
   312 function getElementWithSelection() {
   313   let element = getFocusedElement();
   315   if (!element)
   316     return null;
   318   try {
   319     // Accessing selectionStart and selectionEnd on e.g. a button
   320     // results in an exception thrown as per the HTML5 spec.  See
   321     // http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#textFieldSelection
   323     let { value, selectionStart, selectionEnd } = element;
   325     let hasSelection = typeof value === "string" &&
   326                       !isNaN(selectionStart) &&
   327                       !isNaN(selectionEnd) &&
   328                       selectionStart !== selectionEnd;
   330     return hasSelection ? element : null;
   331   }
   332   catch (err) {
   333     return null;
   334   }
   336 }
   338 /**
   339  * Adds the Selection Listener to the content's window given
   340  */
   341 function addSelectionListener(window) {
   342   let selection = window.getSelection();
   344   // Don't add the selection's listener more than once to the same window,
   345   // if the selection object is the same
   346   if ("selection" in selections(window) && selections(window).selection === selection)
   347     return;
   349   // We ensure that the current selection is an instance of
   350   // `nsISelectionPrivate` before working on it, in case is `null`.
   351   //
   352   // If it's `null` it's likely too early to add the listener, and we demand
   353   // that operation to `document-shown` - it can easily happens for frames
   354   if (selection instanceof Ci.nsISelectionPrivate)
   355     selection.addSelectionListener(selectionListener);
   357   // nsISelectionListener implementation seems not fire a notification if
   358   // a selection is in a text field, therefore we need to add a listener to
   359   // window.onselect, that is fired only for text fields.
   360   // For consistency, we add it only when the nsISelectionListener is added.
   361   //
   362   // https://developer.mozilla.org/en/DOM/window.onselect
   363   window.addEventListener("select", selectionListener.onSelect, true);
   365   selections(window).selection = selection;
   366 };
   368 /**
   369  * Removes the Selection Listener to the content's window given
   370  */
   371 function removeSelectionListener(window) {
   372   // Don't remove the selection's listener to a window that wasn't handled.
   373   if (!("selection" in selections(window)))
   374     return;
   376   let selection = window.getSelection();
   377   let isSameSelection = selection === selections(window).selection;
   379   // Before remove the listener, we ensure that the current selection is an
   380   // instance of `nsISelectionPrivate` (it could be `null`), and that is still
   381   // the selection we managed for this window (it could be detached).
   382   if (selection instanceof Ci.nsISelectionPrivate && isSameSelection)
   383     selection.removeSelectionListener(selectionListener);
   385   window.removeEventListener("select", selectionListener.onSelect, true);
   387   delete selections(window).selection;
   388 };
   390 function onContent(event) {
   391   let window = event.subject.defaultView;
   393   // We are not interested in documents without valid defaultView (e.g. XML)
   394   // that aren't in a tab (e.g. Panel); or in private windows
   395    if (window && getTabForContentWindow(window) && !ignoreWindow(window)) {
   396     addSelectionListener(window);
   397   }
   398 }
   400 // Adds Selection listener to new documents
   401 // Note that strong reference is needed for documents that are loading slowly or
   402 // where the server didn't close the connection (e.g. "comet").
   403 events.on("document-element-inserted", onContent, true);
   405 // Adds Selection listeners to existing documents
   406 getAllTabContentWindows().forEach(addSelectionListener);
   408 // When a document is not visible anymore the selection object is detached, and
   409 // a new selection object is created when it becomes visible again.
   410 // That makes the previous selection's listeners added previously totally
   411 // useless – the listeners are not notified anymore.
   412 // To fix that we're listening for `document-shown` event in order to add
   413 // the listeners to the new selection object created.
   414 //
   415 // See bug 665386 for further details.
   417 function onShown(event) {
   418   let window = event.subject.defaultView;
   420   // We are not interested in documents without valid defaultView.
   421   // For example XML documents don't have windows and we don't yet support them.
   422   if (!window)
   423     return;
   425   // We want to handle only the windows where we added selection's listeners
   426   if ("selection" in selections(window)) {
   427     let currentSelection = window.getSelection();
   428     let { selection } = selections(window);
   430     // If the current selection for the window given is different from the one
   431     // stored in the namespace, we need to add the listeners again, and replace
   432     // the previous selection in our list with the new one.
   433     //
   434     // Notice that we don't have to remove the listeners from the old selection,
   435     // because is detached. An attempt to remove the listener, will raise an
   436     // error (see http://mxr.mozilla.org/mozilla-central/source/layout/generic/nsSelection.cpp#5343 )
   437     //
   438     // We ensure that the current selection is an instance of
   439     // `nsISelectionPrivate` before working on it, in case is `null`.
   440     if (currentSelection instanceof Ci.nsISelectionPrivate &&
   441       currentSelection !== selection) {
   443       window.addEventListener("select", selectionListener.onSelect, true);
   444       currentSelection.addSelectionListener(selectionListener);
   445       selections(window).selection = currentSelection;
   446     }
   447   }
   448 }
   450 events.on("document-shown", onShown, true);
   452 // Removes Selection listeners when the add-on is unloaded
   453 unload(function(){
   454   getAllTabContentWindows().forEach(removeSelectionListener);
   456   events.off("document-element-inserted", onContent);
   457   events.off("document-shown", onShown);
   459   off(exports);
   460 });
   462 const selection = Class({
   463   extends: EventTarget,
   464   implements: [ Selection, selectionIterator ]
   465 })();
   467 module.exports = selection;

mercurial