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.

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

mercurial