Sat, 03 Jan 2015 20:18:00 +0100
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; |