diff options
Diffstat (limited to 'tools/addon-sdk-1.4/packages/addon-kit/lib/selection.js')
-rw-r--r-- | tools/addon-sdk-1.4/packages/addon-kit/lib/selection.js | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.4/packages/addon-kit/lib/selection.js b/tools/addon-sdk-1.4/packages/addon-kit/lib/selection.js new file mode 100644 index 0000000..48db7fc --- /dev/null +++ b/tools/addon-sdk-1.4/packages/addon-kit/lib/selection.js @@ -0,0 +1,448 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Eric H. Jung <eric.jung@yahoo.com> + * Irakli Gozalishivili <gozala@mozilla.com> + * Matteo Ferretti <zer0@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +"use strict"; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The selection module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +let { Ci } = require("chrome"), + { setTimeout } = require("api-utils/timer"), + { EventEmitter } = require("api-utils/events"); + +// The selection type HTML +const HTML = 0x01; + +// The selection type TEXT +const TEXT = 0x02; + +// The selection type DOM (internal use only) +const DOM = 0x03; + +// 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"; + +/** + * 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 + */ +function Selection(rangeNumber) { + + // In order to hide the private rangeNumber argument from API consumers while + // still enabling Selection getters/setters to access it, the getters/setters + // are defined as lexical closures in the Selector constructor. + + this.__defineGetter__("text", function () getSelection(TEXT, rangeNumber)); + this.__defineSetter__("text", function (str) setSelection(str, rangeNumber)); + + this.__defineGetter__("html", function () getSelection(HTML, rangeNumber)); + this.__defineSetter__("html", function (str) setSelection(str, rangeNumber)); + + this.__defineGetter__("isContiguous", function () { + let sel = getSelection(DOM); + + // If there are multiple ranges, the selection is definitely discontiguous. + // It returns `false` also if there are no selection; and `true` if there is + // a single non empty range, or a selection in a text field - contiguous or + // not (text field selection APIs doesn't support multiple selections). + + if (sel.rangeCount > 1) + return false; + + return !!(safeGetRange(sel, 0) || getElementWithSelection()); + }); +} + +require("api-utils/xpcom").utils.defineLazyServiceGetter(this, "windowMediator", + "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"); + +/** + * Returns the most recent content window + */ +function context() { + // Overlay names should probably go into the xul-app module instead of here + return windowMediator.getMostRecentWindow("navigator:browser").document. + commandDispatcher.focusedWindow; +} + +/** + * 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 = context(); + 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(window); + + 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."); +} + +/** + * 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. + */ +function safeGetRange(selection, rangeNumber) { + try { + let range = selection.getRangeAt(rangeNumber); + if (!range || range.toString() == "") + return 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) + * + * @param {nsIWindow} [window] + * A reference to a window + */ +function getElementWithSelection(window) { + let element; + + try { + element = (window || context()).document.activeElement; + } + catch (e) { + element = null; + } + + if (!element) + return null; + + let { value, selectionStart, selectionEnd } = element; + + let hasSelection = typeof value === "string" && + !isNaN(selectionStart) && + !isNaN(selectionEnd) && + selectionStart !== selectionEnd; + + return hasSelection ? element : null; +} +/** + * 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(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 = context(); + selection = window.getSelection(); + } + catch (e) { + throw new Error(ERR_CANNOT_CHANGE_SELECTION); + } + + let range = safeGetRange(selection, rangeNumber); + + if (range) { + // Get rid of the current selection and insert our own + range.deleteContents(); + let node = window.document.createElement("span"); + range.surroundContents(node); + + // Some relevant JEP-111 requirements: + + // Setting the text property replaces the selection with the value to + // which the property is set and sets the html property to the same value + // to which the text property is being set. + + // Setting the html property replaces the selection with the value to + // which the property is set and sets the text property to the text version + // of the HTML value. + + // This sets both the HTML and text properties. + node.innerHTML = val; + } else { + let node = getElementWithSelection(window); + + 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); + } +} + +function onLoad(event) { + SelectionListenerManager.onLoad(event); +} + +function onUnload(event) { + SelectionListenerManager.onUnload(event); +} + +function onSelect() { + SelectionListenerManager.onSelect(); +} + +let SelectionListenerManager = { + QueryInterface: require("api-utils/xpcom").utils. + generateQI([Ci.nsISelectionListener]), + + // The collection of listeners wanting to be notified of selection changes + listeners: EventEmitter.compose({ + emit: function emit(type) this._emitOnObject(exports, type), + off: function() this._removeAllListeners.apply(this, arguments) + })(), + /** + * This is the nsISelectionListener implementation. This function is called + * by Gecko when a selection is changed interactively. + * + * We only pay attention to the SELECTALL, KEYPRESS, and MOUSEUP selection + * reasons. All reasons are listed here: + * + * http://mxr.mozilla.org/mozilla1.9.2/source/content/base/public/ + * nsISelectionListener.idl + * + * The other reasons (NO_REASON, DRAG_REASON, MOUSEDOWN_REASON) aren't + * applicable to us. + */ + notifySelectionChanged: function notifySelectionChanged(document, selection, + reason) { + if (!["SELECTALL", "KEYPRESS", "MOUSEUP"].some(function(type) reason & + Ci.nsISelectionListener[type + "_REASON"]) || selection.toString() == "") + return; + + this.onSelect(); + }, + + onSelect : function onSelect() { + setTimeout(this.listeners.emit, 0, "select"); + }, + + /** + * Part of the Tracker implementation. This function is called by the + * tabs module when a browser is being tracked. Often, that means a new tab + * has been opened, but it can also mean an addon has been installed while + * tabs are already opened. In that case, this function is called for those + * already-opened tabs. + * + * @param browser + * The browser being tracked + */ + onTrack: function onTrack(browser) { + browser.addEventListener("load", onLoad, true); + browser.addEventListener("unload", onUnload, true); + }, + + onLoad: function onLoad(event) { + // Nothing to do without a useful window + let window = event.target.defaultView; + if (!window) + return; + + // Wrap the add selection call with some number of setTimeout 0 because some + // reason it's possible to add a selection listener "too early". 2 sometimes + // works for gmail, and more consistently with 3, so make it 5 to be safe. + let count = 0; + let self = this; + function wrap(count, func) { + if (count-- > 0) + require("api-utils/timer").setTimeout(wrap, 0); + else + self.addSelectionListener(window); + } + wrap(); + }, + + addSelectionListener: function addSelectionListener(window) { + if (window.jetpack_core_selection_listener) + return; + let selection = window.getSelection(); + if (selection instanceof Ci.nsISelectionPrivate) + selection.addSelectionListener(this); + + // 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. + // https://developer.mozilla.org/en/DOM/window.onselect + window.addEventListener("select", onSelect, true); + + window.jetpack_core_selection_listener = true; + }, + + onUnload: function onUnload(event) { + // Nothing to do without a useful window + let window = event.target.defaultView; + if (!window) + return; + this.removeSelectionListener(window); + this.listeners.off('error'); + this.listeners.off('selection'); + }, + + removeSelectionListener: function removeSelectionListener(window) { + if (!window.jetpack_core_selection_listener) + return; + let selection = window.getSelection(); + if (selection instanceof Ci.nsISelectionPrivate) + selection.removeSelectionListener(this); + + window.removeEventListener("select", onSelect); + + window.jetpack_core_selection_listener = false; + }, + + /** + * Part of the TabTracker implementation. This function is called by the + * tabs module when a browser is being untracked. Usually, that means a tab + * has been closed. + * + * @param browser + * The browser being untracked + */ + onUntrack: function onUntrack(browser) { + browser.removeEventListener("load", onLoad, true); + browser.removeEventListener("unload", onUnload, true); + } +}; +SelectionListenerManager.listeners.on('error', console.error); + +/** + * Install |SelectionListenerManager| as tab tracker in order to watch + * tab opening/closing + */ +require("api-utils/tab-browser").Tracker(SelectionListenerManager); + +/** + * Exports an iterator so that discontiguous selections can be iterated. + * + * 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. + */ +exports.__iterator__ = function __iterator__() { + let sel = getSelection(DOM); + let rangeCount = sel.rangeCount || (getElementWithSelection() ? 1 : 0); + + for (let i = 0; i < rangeCount; i++) + yield new Selection(i); +}; + +exports.on = SelectionListenerManager.listeners.on; +exports.removeListener = SelectionListenerManager.listeners.removeListener; + +// Export the Selection singleton. Its rangeNumber is always zero. +Selection.call(exports, 0); + |