diff options
Diffstat (limited to 'addon-sdk/source/lib/sdk/selection.js')
-rw-r--r-- | addon-sdk/source/lib/sdk/selection.js | 470 |
1 files changed, 0 insertions, 470 deletions
diff --git a/addon-sdk/source/lib/sdk/selection.js b/addon-sdk/source/lib/sdk/selection.js deleted file mode 100644 index 8682e8c6d..000000000 --- a/addon-sdk/source/lib/sdk/selection.js +++ /dev/null @@ -1,470 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -module.metadata = { - "stability": "stable", - "engines": { - "Firefox": "*", - "SeaMonkey": "*" - } -}; - -const { Ci, Cc } = require("chrome"), - { setTimeout } = require("./timers"), - { emit, off } = require("./event/core"), - { Class, obscure } = require("./core/heritage"), - { EventTarget } = require("./event/target"), - { ns } = require("./core/namespace"), - { when: unload } = require("./system/unload"), - { ignoreWindow } = require('./private-browsing/utils'), - { getTabs, getTabForContentWindow, - getAllTabContentWindows } = require('./tabs/utils'), - winUtils = require("./window/utils"), - events = require("./system/events"); - -// The selection types -const HTML = 0x01, - TEXT = 0x02, - DOM = 0x03; // internal use only - -// A more developer-friendly message than the caught exception when is not -// possible change a selection. -const ERR_CANNOT_CHANGE_SELECTION = - "It isn't possible to change the selection, as there isn't currently a selection"; - -const selections = ns(); - -const Selection = Class({ - /** - * Creates an object from which a selection can be set, get, etc. Each - * object has an associated with a range number. Range numbers are the - * 0-indexed counter of selection ranges as explained at - * https://developer.mozilla.org/en/DOM/Selection. - * - * @param rangeNumber - * The zero-based range index into the selection - */ - initialize: function initialize(rangeNumber) { - // In order to hide the private `rangeNumber` argument from API consumers - // while still enabling Selection getters/setters to access it, we define - // it as non enumerable, non configurable property. While consumers still - // may discover it they won't be able to do any harm which is good enough - // in this case. - Object.defineProperties(this, { - rangeNumber: { - enumerable: false, - configurable: false, - value: rangeNumber - } - }); - }, - get text() { return getSelection(TEXT, this.rangeNumber); }, - set text(value) { setSelection(TEXT, value, this.rangeNumber); }, - get html() { return getSelection(HTML, this.rangeNumber); }, - set html(value) { setSelection(HTML, value, this.rangeNumber); }, - get isContiguous() { - - // If there are multiple non empty ranges, the selection is definitely - // discontiguous. It returns `false` also if there are no valid selection. - let count = 0; - for (let sel in selectionIterator) - if (++count > 1) - break; - - return count === 1; - } -}); - -const selectionListener = { - notifySelectionChanged: function (document, selection, reason) { - if (!["SELECTALL", "KEYPRESS", "MOUSEUP"].some(type => reason & - Ci.nsISelectionListener[type + "_REASON"]) || selection.toString() == "") - return; - - this.onSelect(); - }, - - onSelect: function() { - emit(module.exports, "select"); - } -} - -/** - * Defines iterators so that discontiguous selections can be iterated. - * Empty selections are skipped - see `safeGetRange` for further details. - * - * If discontiguous selections are in a text field, only the first one - * is returned because the text field selection APIs doesn't support - * multiple selections. - */ -function* forOfIterator() { - let selection = getSelection(DOM); - let count = 0; - - if (selection) - count = selection.rangeCount || (getElementWithSelection() ? 1 : 0); - - for (let i = 0; i < count; i++) { - let sel = Selection(i); - - if (sel.text) - yield Selection(i); - } -} - -const selectionIteratorOptions = { - __iterator__: function() { - for (let item of this) - yield item; - } -} -selectionIteratorOptions[Symbol.iterator] = forOfIterator; -const selectionIterator = obscure(selectionIteratorOptions); - -/** - * Returns the most recent focused window. - * if private browsing window is most recent and not supported, - * then ignore it and return `null`, because the focused window - * can't be targeted. - */ -function getFocusedWindow() { - let window = winUtils.getFocusedWindow(); - - return ignoreWindow(window) ? null : window; -} - -/** - * Returns the focused element in the most recent focused window - * if private browsing window is most recent and not supported, - * then ignore it and return `null`, because the focused element - * can't be targeted. - */ -function getFocusedElement() { - let element = winUtils.getFocusedElement(); - - if (!element || ignoreWindow(element.ownerDocument.defaultView)) - return null; - - return element; -} - -/** - * Returns the current selection from most recent content window. Depending on - * the specified |type|, the value returned can be a string of text, stringified - * HTML, or a DOM selection object as described at - * https://developer.mozilla.org/en/DOM/Selection. - * - * @param type - * Specifies the return type of the selection. Valid values are the one - * of the constants HTML, TEXT, or DOM. - * - * @param rangeNumber - * Specifies the zero-based range index of the returned selection. - */ -function getSelection(type, rangeNumber) { - let window, selection; - try { - window = getFocusedWindow(); - selection = window.getSelection(); - } - catch (e) { - return null; - } - - // Get the selected content as the specified type - if (type == DOM) { - return selection; - } - else if (type == TEXT) { - let range = safeGetRange(selection, rangeNumber); - - if (range) - return range.toString(); - - let node = getElementWithSelection(); - - if (!node) - return null; - - return node.value.substring(node.selectionStart, node.selectionEnd); - } - else if (type == HTML) { - let range = safeGetRange(selection, rangeNumber); - // Another way, but this includes the xmlns attribute for all elements in - // Gecko 1.9.2+ : - // return Cc["@mozilla.org/xmlextras/xmlserializer;1"]. - // createInstance(Ci.nsIDOMSerializer).serializeToSTring(range. - // cloneContents()); - if (!range) - return null; - - let node = window.document.createElement("span"); - node.appendChild(range.cloneContents()); - return node.innerHTML; - } - - throw new Error("Type " + type + " is unrecognized."); -} - -/** - * Sets the current selection of the most recent content document by changing - * the existing selected text/HTML range to the specified value. - * - * @param val - * The value for the new selection - * - * @param rangeNumber - * The zero-based range index of the selection to be set - * - */ -function setSelection(type, val, rangeNumber) { - // Make sure we have a window context & that there is a current selection. - // Selection cannot be set unless there is an existing selection. - let window, selection; - - try { - window = getFocusedWindow(); - selection = window.getSelection(); - } - catch (e) { - throw new Error(ERR_CANNOT_CHANGE_SELECTION); - } - - let range = safeGetRange(selection, rangeNumber); - - if (range) { - let fragment; - - if (type === HTML) - fragment = range.createContextualFragment(val); - else { - fragment = range.createContextualFragment(""); - fragment.textContent = val; - } - - range.deleteContents(); - range.insertNode(fragment); - } - else { - let node = getElementWithSelection(); - - if (!node) - throw new Error(ERR_CANNOT_CHANGE_SELECTION); - - let { value, selectionStart, selectionEnd } = node; - - let newSelectionEnd = selectionStart + val.length; - - node.value = value.substring(0, selectionStart) + - val + - value.substring(selectionEnd, value.length); - - node.setSelectionRange(selectionStart, newSelectionEnd); - } -} - -/** - * Returns the specified range in a selection without throwing an exception. - * - * @param selection - * A selection object as described at - * https://developer.mozilla.org/en/DOM/Selection - * - * @param [rangeNumber] - * Specifies the zero-based range index of the returned selection. - * If it's not provided the function will return the first non empty - * range, if any. - */ -function safeGetRange(selection, rangeNumber) { - try { - let { rangeCount } = selection; - let range = null; - - if (typeof rangeNumber === "undefined") - rangeNumber = 0; - else - rangeCount = rangeNumber + 1; - - for (; rangeNumber < rangeCount; rangeNumber++ ) { - range = selection.getRangeAt(rangeNumber); - - if (range && range.toString()) - break; - - range = null; - } - - return range; - } - catch (e) { - return null; - } -} - -/** - * Returns a reference of the DOM's active element for the window given, if it - * supports the text field selection API and has a text selected. - * - * Note: - * we need this method because window.getSelection doesn't return a selection - * for text selected in a form field (see bug 85686) - */ -function getElementWithSelection() { - let element = getFocusedElement(); - - if (!element) - return null; - - try { - // Accessing selectionStart and selectionEnd on e.g. a button - // results in an exception thrown as per the HTML5 spec. See - // http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#textFieldSelection - - let { value, selectionStart, selectionEnd } = element; - - let hasSelection = typeof value === "string" && - !isNaN(selectionStart) && - !isNaN(selectionEnd) && - selectionStart !== selectionEnd; - - return hasSelection ? element : null; - } - catch (err) { - return null; - } - -} - -/** - * Adds the Selection Listener to the content's window given - */ -function addSelectionListener(window) { - let selection = window.getSelection(); - - // Don't add the selection's listener more than once to the same window, - // if the selection object is the same - if ("selection" in selections(window) && selections(window).selection === selection) - return; - - // We ensure that the current selection is an instance of - // `nsISelectionPrivate` before working on it, in case is `null`. - // - // If it's `null` it's likely too early to add the listener, and we demand - // that operation to `document-shown` - it can easily happens for frames - if (selection instanceof Ci.nsISelectionPrivate) - selection.addSelectionListener(selectionListener); - - // nsISelectionListener implementation seems not fire a notification if - // a selection is in a text field, therefore we need to add a listener to - // window.onselect, that is fired only for text fields. - // For consistency, we add it only when the nsISelectionListener is added. - // - // https://developer.mozilla.org/en/DOM/window.onselect - window.addEventListener("select", selectionListener.onSelect, true); - - selections(window).selection = selection; -}; - -/** - * Removes the Selection Listener to the content's window given - */ -function removeSelectionListener(window) { - // Don't remove the selection's listener to a window that wasn't handled. - if (!("selection" in selections(window))) - return; - - let selection = window.getSelection(); - let isSameSelection = selection === selections(window).selection; - - // Before remove the listener, we ensure that the current selection is an - // instance of `nsISelectionPrivate` (it could be `null`), and that is still - // the selection we managed for this window (it could be detached). - if (selection instanceof Ci.nsISelectionPrivate && isSameSelection) - selection.removeSelectionListener(selectionListener); - - window.removeEventListener("select", selectionListener.onSelect, true); - - delete selections(window).selection; -}; - -function onContent(event) { - let window = event.subject.defaultView; - - // We are not interested in documents without valid defaultView (e.g. XML) - // that aren't in a tab (e.g. Panel); or in private windows - if (window && getTabForContentWindow(window) && !ignoreWindow(window)) { - addSelectionListener(window); - } -} - -// Adds Selection listener to new documents -// Note that strong reference is needed for documents that are loading slowly or -// where the server didn't close the connection (e.g. "comet"). -events.on("document-element-inserted", onContent, true); - -// Adds Selection listeners to existing documents -getAllTabContentWindows().forEach(addSelectionListener); - -// When a document is not visible anymore the selection object is detached, and -// a new selection object is created when it becomes visible again. -// That makes the previous selection's listeners added previously totally -// useless – the listeners are not notified anymore. -// To fix that we're listening for `document-shown` event in order to add -// the listeners to the new selection object created. -// -// See bug 665386 for further details. - -function onShown(event) { - let window = event.subject.defaultView; - - // We are not interested in documents without valid defaultView. - // For example XML documents don't have windows and we don't yet support them. - if (!window) - return; - - // We want to handle only the windows where we added selection's listeners - if ("selection" in selections(window)) { - let currentSelection = window.getSelection(); - let { selection } = selections(window); - - // If the current selection for the window given is different from the one - // stored in the namespace, we need to add the listeners again, and replace - // the previous selection in our list with the new one. - // - // Notice that we don't have to remove the listeners from the old selection, - // because is detached. An attempt to remove the listener, will raise an - // error (see http://mxr.mozilla.org/mozilla-central/source/layout/generic/nsSelection.cpp#5343 ) - // - // We ensure that the current selection is an instance of - // `nsISelectionPrivate` before working on it, in case is `null`. - if (currentSelection instanceof Ci.nsISelectionPrivate && - currentSelection !== selection) { - - window.addEventListener("select", selectionListener.onSelect, true); - currentSelection.addSelectionListener(selectionListener); - selections(window).selection = currentSelection; - } - } -} - -events.on("document-shown", onShown, true); - -// Removes Selection listeners when the add-on is unloaded -unload(function(){ - getAllTabContentWindows().forEach(removeSelectionListener); - - events.off("document-element-inserted", onContent); - events.off("document-shown", onShown); - - off(exports); -}); - -const selection = Class({ - extends: EventTarget, - implements: [ Selection, selectionIterator ] -})(); - -module.exports = selection; |