mobile/android/chrome/content/SelectionHandler.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/chrome/content/SelectionHandler.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1147 @@
     1.4 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
     1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +"use strict";
     1.9 +
    1.10 +var SelectionHandler = {
    1.11 +  HANDLE_TYPE_START: "START",
    1.12 +  HANDLE_TYPE_MIDDLE: "MIDDLE",
    1.13 +  HANDLE_TYPE_END: "END",
    1.14 +
    1.15 +  TYPE_NONE: 0,
    1.16 +  TYPE_CURSOR: 1,
    1.17 +  TYPE_SELECTION: 2,
    1.18 +
    1.19 +  SELECT_ALL: 0,
    1.20 +  SELECT_AT_POINT: 1,
    1.21 +
    1.22 +  // Keeps track of data about the dimensions of the selection. Coordinates
    1.23 +  // stored here are relative to the _contentWindow window.
    1.24 +  _cache: null,
    1.25 +  _activeType: 0, // TYPE_NONE
    1.26 +  _draggingHandles: false, // True while user drags text selection handles
    1.27 +  _ignoreCompositionChanges: false, // Persist caret during IME composition updates
    1.28 +  _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages
    1.29 +
    1.30 +  // TargetElement changes (text <--> no text) trigger actionbar UI update
    1.31 +  _prevTargetElementHasText: null,
    1.32 +
    1.33 +  // The window that holds the selection (can be a sub-frame)
    1.34 +  get _contentWindow() {
    1.35 +    if (this._contentWindowRef)
    1.36 +      return this._contentWindowRef.get();
    1.37 +    return null;
    1.38 +  },
    1.39 +
    1.40 +  set _contentWindow(aContentWindow) {
    1.41 +    this._contentWindowRef = Cu.getWeakReference(aContentWindow);
    1.42 +  },
    1.43 +
    1.44 +  get _targetElement() {
    1.45 +    if (this._targetElementRef)
    1.46 +      return this._targetElementRef.get();
    1.47 +    return null;
    1.48 +  },
    1.49 +
    1.50 +  set _targetElement(aTargetElement) {
    1.51 +    this._targetElementRef = Cu.getWeakReference(aTargetElement);
    1.52 +  },
    1.53 +
    1.54 +  get _domWinUtils() {
    1.55 +    return BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
    1.56 +                                                    getInterface(Ci.nsIDOMWindowUtils);
    1.57 +  },
    1.58 +
    1.59 +  _isRTL: false,
    1.60 +
    1.61 +  _addObservers: function sh_addObservers() {
    1.62 +    Services.obs.addObserver(this, "Gesture:SingleTap", false);
    1.63 +    Services.obs.addObserver(this, "Tab:Selected", false);
    1.64 +    Services.obs.addObserver(this, "after-viewport-change", false);
    1.65 +    Services.obs.addObserver(this, "TextSelection:Move", false);
    1.66 +    Services.obs.addObserver(this, "TextSelection:Position", false);
    1.67 +    Services.obs.addObserver(this, "TextSelection:End", false);
    1.68 +    Services.obs.addObserver(this, "TextSelection:Action", false);
    1.69 +    Services.obs.addObserver(this, "TextSelection:LayerReflow", false);
    1.70 +
    1.71 +    BrowserApp.deck.addEventListener("pagehide", this, false);
    1.72 +    BrowserApp.deck.addEventListener("blur", this, true);
    1.73 +    BrowserApp.deck.addEventListener("scroll", this, true);
    1.74 +  },
    1.75 +
    1.76 +  _removeObservers: function sh_removeObservers() {
    1.77 +    Services.obs.removeObserver(this, "Gesture:SingleTap");
    1.78 +    Services.obs.removeObserver(this, "Tab:Selected");
    1.79 +    Services.obs.removeObserver(this, "after-viewport-change");
    1.80 +    Services.obs.removeObserver(this, "TextSelection:Move");
    1.81 +    Services.obs.removeObserver(this, "TextSelection:Position");
    1.82 +    Services.obs.removeObserver(this, "TextSelection:End");
    1.83 +    Services.obs.removeObserver(this, "TextSelection:Action");
    1.84 +    Services.obs.removeObserver(this, "TextSelection:LayerReflow");
    1.85 +
    1.86 +    BrowserApp.deck.removeEventListener("pagehide", this, false);
    1.87 +    BrowserApp.deck.removeEventListener("blur", this, true);
    1.88 +    BrowserApp.deck.removeEventListener("scroll", this, true);
    1.89 +  },
    1.90 +
    1.91 +  observe: function sh_observe(aSubject, aTopic, aData) {
    1.92 +    switch (aTopic) {
    1.93 +      // Update handle/caret position on page reflow (keyboard open/close,
    1.94 +      // dynamic DOM changes, orientation updates, etc).
    1.95 +      case "TextSelection:LayerReflow": {
    1.96 +        if (this._activeType == this.TYPE_SELECTION) {
    1.97 +          this._updateCacheForSelection();
    1.98 +        }
    1.99 +        if (this._activeType != this.TYPE_NONE) {
   1.100 +          this._positionHandlesOnChange();
   1.101 +        }
   1.102 +        break;
   1.103 +      }
   1.104 +
   1.105 +      // Update caret position on keyboard activity
   1.106 +      case "TextSelection:UpdateCaretPos":
   1.107 +        // Generated by IME close, autoCorrection / styling
   1.108 +        this._positionHandles();
   1.109 +        break;
   1.110 +
   1.111 +      case "Gesture:SingleTap": {
   1.112 +        if (this._activeType == this.TYPE_SELECTION) {
   1.113 +          let data = JSON.parse(aData);
   1.114 +          if (!this._pointInSelection(data.x, data.y))
   1.115 +            this._closeSelection();
   1.116 +        } else if (this._activeType == this.TYPE_CURSOR) {
   1.117 +          // attachCaret() is called in the "Gesture:SingleTap" handler in BrowserEventHandler
   1.118 +          // We're guaranteed to call this first, because this observer was added last
   1.119 +          this._deactivate();
   1.120 +        }
   1.121 +        break;
   1.122 +      }
   1.123 +      case "Tab:Selected":
   1.124 +      case "TextSelection:End":
   1.125 +        this._closeSelection();
   1.126 +        break;
   1.127 +      case "TextSelection:Action":
   1.128 +        for (let type in this.actions) {
   1.129 +          if (this.actions[type].id == aData) {
   1.130 +            this.actions[type].action(this._targetElement);
   1.131 +            break;
   1.132 +          }
   1.133 +        }
   1.134 +        break;
   1.135 +      case "after-viewport-change": {
   1.136 +        if (this._activeType == this.TYPE_SELECTION) {
   1.137 +          // Update the cache after the viewport changes (e.g. panning, zooming).
   1.138 +          this._updateCacheForSelection();
   1.139 +        }
   1.140 +        break;
   1.141 +      }
   1.142 +      case "TextSelection:Move": {
   1.143 +        let data = JSON.parse(aData);
   1.144 +        if (this._activeType == this.TYPE_SELECTION) {
   1.145 +          this._startDraggingHandles();
   1.146 +          this._moveSelection(data.handleType == this.HANDLE_TYPE_START, data.x, data.y);
   1.147 +
   1.148 +        } else if (this._activeType == this.TYPE_CURSOR) {
   1.149 +          this._startDraggingHandles();
   1.150 +
   1.151 +          // Ignore IMM composition notifications when caret movement starts
   1.152 +          this._ignoreCompositionChanges = true;
   1.153 +
   1.154 +          // Send a click event to the text box, which positions the caret
   1.155 +          this._sendMouseEvents(data.x, data.y);
   1.156 +
   1.157 +          // Move the handle directly under the caret
   1.158 +          this._positionHandles();
   1.159 +        }
   1.160 +        break;
   1.161 +      }
   1.162 +      case "TextSelection:Position": {
   1.163 +        if (this._activeType == this.TYPE_SELECTION) {
   1.164 +          this._startDraggingHandles();
   1.165 +
   1.166 +          // Check to see if the handles should be reversed.
   1.167 +          let isStartHandle = JSON.parse(aData).handleType == this.HANDLE_TYPE_START;
   1.168 +          try {
   1.169 +            let selectionReversed = this._updateCacheForSelection(isStartHandle);
   1.170 +            if (selectionReversed) {
   1.171 +              // Reverse the anchor and focus to correspond to the new start and end handles.
   1.172 +              let selection = this._getSelection();
   1.173 +              let anchorNode = selection.anchorNode;
   1.174 +              let anchorOffset = selection.anchorOffset;
   1.175 +              selection.collapse(selection.focusNode, selection.focusOffset);
   1.176 +              selection.extend(anchorNode, anchorOffset);
   1.177 +            }
   1.178 +          } catch (e) {
   1.179 +            // User finished handle positioning with one end off the screen
   1.180 +            this._closeSelection();
   1.181 +            break;
   1.182 +          }
   1.183 +
   1.184 +          this._stopDraggingHandles();
   1.185 +          this._positionHandles();
   1.186 +          // Changes to handle position can affect selection context and actionbar display
   1.187 +          this._updateMenu();
   1.188 +
   1.189 +        } else if (this._activeType == this.TYPE_CURSOR) {
   1.190 +          // Act on IMM composition notifications after caret movement ends
   1.191 +          this._ignoreCompositionChanges = false;
   1.192 +          this._stopDraggingHandles();
   1.193 +          this._positionHandles();
   1.194 +
   1.195 +        } else {
   1.196 +          Cu.reportError("Ignored \"TextSelection:Position\" message during invalid selection status");
   1.197 +        }
   1.198 +
   1.199 +        break;
   1.200 +      }
   1.201 +
   1.202 +      case "TextSelection:Get":
   1.203 +        sendMessageToJava({
   1.204 +          type: "TextSelection:Data",
   1.205 +          requestId: aData,
   1.206 +          text: this._getSelectedText()
   1.207 +        });
   1.208 +        break;
   1.209 +    }
   1.210 +  },
   1.211 +
   1.212 +  // Ignore selectionChange notifications during handle dragging, disable dynamic
   1.213 +  // IME text compositions (autoSuggest, autoCorrect, etc)
   1.214 +  _startDraggingHandles: function sh_startDraggingHandles() {
   1.215 +    if (!this._draggingHandles) {
   1.216 +      this._draggingHandles = true;
   1.217 +      sendMessageToJava({ type: "TextSelection:DraggingHandle", dragging: true });
   1.218 +    }
   1.219 +  },
   1.220 +
   1.221 +  // Act on selectionChange notifications when not dragging handles, allow dynamic
   1.222 +  // IME text compositions (autoSuggest, autoCorrect, etc)
   1.223 +  _stopDraggingHandles: function sh_stopDraggingHandles() {
   1.224 +    if (this._draggingHandles) {
   1.225 +      this._draggingHandles = false;
   1.226 +      sendMessageToJava({ type: "TextSelection:DraggingHandle", dragging: false });
   1.227 +    }
   1.228 +  },
   1.229 +
   1.230 +  handleEvent: function sh_handleEvent(aEvent) {
   1.231 +    switch (aEvent.type) {
   1.232 +      case "scroll":
   1.233 +        // Maintain position when top-level document is scrolled
   1.234 +        this._positionHandlesOnChange();
   1.235 +        break;
   1.236 +
   1.237 +      case "pagehide":
   1.238 +      case "blur":
   1.239 +        this._closeSelection();
   1.240 +        break;
   1.241 +
   1.242 +      // Update caret position on keyboard activity
   1.243 +      case "keyup":
   1.244 +        // Not generated by Swiftkeyboard
   1.245 +      case "compositionupdate":
   1.246 +      case "compositionend":
   1.247 +        // Generated by SwiftKeyboard, et. al.
   1.248 +        if (!this._ignoreCompositionChanges) {
   1.249 +          this._positionHandles();
   1.250 +        }
   1.251 +        break;
   1.252 +    }
   1.253 +  },
   1.254 +
   1.255 +  /** Returns true if the provided element can be selected in text selection, false otherwise. */
   1.256 +  canSelect: function sh_canSelect(aElement) {
   1.257 +    return !(aElement instanceof Ci.nsIDOMHTMLButtonElement ||
   1.258 +             aElement instanceof Ci.nsIDOMHTMLEmbedElement ||
   1.259 +             aElement instanceof Ci.nsIDOMHTMLImageElement ||
   1.260 +             aElement instanceof Ci.nsIDOMHTMLMediaElement) &&
   1.261 +             aElement.style.MozUserSelect != 'none';
   1.262 +  },
   1.263 +
   1.264 +  _getScrollPos: function sh_getScrollPos() {
   1.265 +    // Get the current display position
   1.266 +    let scrollX = {}, scrollY = {};
   1.267 +    this._contentWindow.top.QueryInterface(Ci.nsIInterfaceRequestor).
   1.268 +                            getInterface(Ci.nsIDOMWindowUtils).getScrollXY(false, scrollX, scrollY);
   1.269 +    return {
   1.270 +      X: scrollX.value,
   1.271 +      Y: scrollY.value
   1.272 +    };
   1.273 +  },
   1.274 +
   1.275 +  notifySelectionChanged: function sh_notifySelectionChanged(aDocument, aSelection, aReason) {
   1.276 +    // Ignore selectionChange notifications during handle movements
   1.277 +    if (this._draggingHandles) {
   1.278 +      return;
   1.279 +    }
   1.280 +
   1.281 +    // If the selection was collapsed to Start or to End, always close it
   1.282 +    if ((aReason & Ci.nsISelectionListener.COLLAPSETOSTART_REASON) ||
   1.283 +        (aReason & Ci.nsISelectionListener.COLLAPSETOEND_REASON)) {
   1.284 +      this._closeSelection();
   1.285 +      return;
   1.286 +    }
   1.287 +
   1.288 +    // If selected text no longer exists, close
   1.289 +    if (!aSelection.toString()) {
   1.290 +      this._closeSelection();
   1.291 +    }
   1.292 +  },
   1.293 +
   1.294 +  /*
   1.295 +   * Called from browser.js when the user long taps on text or chooses
   1.296 +   * the "Select Word" context menu item. Initializes SelectionHandler,
   1.297 +   * starts a selection, and positions the text selection handles.
   1.298 +   *
   1.299 +   * @param aOptions list of options describing how to start selection
   1.300 +   *                 Options include:
   1.301 +   *                   mode - SELECT_ALL to select everything in the target
   1.302 +   *                          element, or SELECT_AT_POINT to select a word.
   1.303 +   *                   x    - The x-coordinate for SELECT_AT_POINT.
   1.304 +   *                   y    - The y-coordinate for SELECT_AT_POINT.
   1.305 +   */
   1.306 +  startSelection: function sh_startSelection(aElement, aOptions = { mode: SelectionHandler.SELECT_ALL }) {
   1.307 +    // Clear out any existing active selection
   1.308 +    this._closeSelection();
   1.309 +
   1.310 +    this._initTargetInfo(aElement, this.TYPE_SELECTION);
   1.311 +
   1.312 +    // Clear any existing selection from the document
   1.313 +    this._contentWindow.getSelection().removeAllRanges();
   1.314 +
   1.315 +    // Perform the appropriate selection method, if we can't determine method, or it fails, return
   1.316 +    if (!this._performSelection(aOptions)) {
   1.317 +      this._deactivate();
   1.318 +      return false;
   1.319 +    }
   1.320 +
   1.321 +    // Double check results of successful selection operation
   1.322 +    let selection = this._getSelection();
   1.323 +    if (!selection || selection.rangeCount == 0 || selection.getRangeAt(0).collapsed) {
   1.324 +      this._deactivate();
   1.325 +      return false;
   1.326 +    }
   1.327 +
   1.328 +    if (this._isPhoneNumber(selection.toString())) {
   1.329 +      let anchorNode = selection.anchorNode;
   1.330 +      let anchorOffset = selection.anchorOffset;
   1.331 +      let focusNode = null;
   1.332 +      let focusOffset = null;
   1.333 +      while (this._isPhoneNumber(selection.toString().trim())) {
   1.334 +        focusNode = selection.focusNode;
   1.335 +        focusOffset = selection.focusOffset;
   1.336 +        selection.modify("extend", "forward", "word");
   1.337 +        // if we hit the end of the text on the page, we can't advance the selection
   1.338 +        if (focusNode == selection.focusNode && focusOffset == selection.focusOffset) {
   1.339 +          break;
   1.340 +        }
   1.341 +      }
   1.342 +
   1.343 +      // reverse selection
   1.344 +      selection.collapse(focusNode, focusOffset);
   1.345 +      selection.extend(anchorNode, anchorOffset);
   1.346 +
   1.347 +      anchorNode = focusNode;
   1.348 +      anchorOffset = focusOffset
   1.349 +
   1.350 +      while (this._isPhoneNumber(selection.toString().trim())) {
   1.351 +        focusNode = selection.focusNode;
   1.352 +        focusOffset = selection.focusOffset;
   1.353 +        selection.modify("extend", "backward", "word");
   1.354 +        // if we hit the end of the text on the page, we can't advance the selection
   1.355 +        if (focusNode == selection.focusNode && focusOffset == selection.focusOffset) {
   1.356 +          break;
   1.357 +        }
   1.358 +      }
   1.359 +
   1.360 +      selection.collapse(focusNode, focusOffset);
   1.361 +      selection.extend(anchorNode, anchorOffset);
   1.362 +    }
   1.363 +
   1.364 +    // Add a listener to end the selection if it's removed programatically
   1.365 +    selection.QueryInterface(Ci.nsISelectionPrivate).addSelectionListener(this);
   1.366 +    this._activeType = this.TYPE_SELECTION;
   1.367 +
   1.368 +    // Initialize the cache
   1.369 +    this._cache = { start: {}, end: {}};
   1.370 +    this._updateCacheForSelection();
   1.371 +
   1.372 +    let scroll = this._getScrollPos();
   1.373 +    // Figure out the distance between the selection and the click
   1.374 +    let positions = this._getHandlePositions(scroll);
   1.375 +
   1.376 +    if (aOptions.mode == this.SELECT_AT_POINT && !this._selectionNearClick(scroll.X + aOptions.x,
   1.377 +                                                                      scroll.Y + aOptions.y,
   1.378 +                                                                      positions)) {
   1.379 +        this._closeSelection();
   1.380 +        return false;
   1.381 +    }
   1.382 +
   1.383 +    // Determine position and show handles, open actionbar
   1.384 +    this._positionHandles(positions);
   1.385 +    sendMessageToJava({
   1.386 +      type: "TextSelection:ShowHandles",
   1.387 +      handles: [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]
   1.388 +    });
   1.389 +    this._updateMenu();
   1.390 +    return true;
   1.391 +  },
   1.392 +
   1.393 +  /*
   1.394 +   * Called to perform a selection operation, given a target element, selection method, starting point etc.
   1.395 +   */
   1.396 +  _performSelection: function sh_performSelection(aOptions) {
   1.397 +    if (aOptions.mode == this.SELECT_AT_POINT) {
   1.398 +      return this._domWinUtils.selectAtPoint(aOptions.x, aOptions.y, Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE);
   1.399 +    }
   1.400 +
   1.401 +    if (aOptions.mode != this.SELECT_ALL) {
   1.402 +      Cu.reportError("SelectionHandler.js: _performSelection() Invalid selection mode " + aOptions.mode);
   1.403 +      return false;
   1.404 +    }
   1.405 +
   1.406 +    // HTMLPreElement is a #text node, SELECT_ALL implies entire paragraph
   1.407 +    if (this._targetElement instanceof HTMLPreElement)  {
   1.408 +      return this._domWinUtils.selectAtPoint(1, 1, Ci.nsIDOMWindowUtils.SELECT_PARAGRAPH);
   1.409 +    }
   1.410 +
   1.411 +    // Else default to selectALL Document
   1.412 +    this._getSelectionController().selectAll();
   1.413 +
   1.414 +    // Selection is entire HTMLHtmlElement, remove any trailing document whitespace
   1.415 +    let selection = this._getSelection();
   1.416 +    let lastNode = selection.focusNode;
   1.417 +    while (lastNode && lastNode.lastChild) {
   1.418 +      lastNode = lastNode.lastChild;
   1.419 +    }
   1.420 +
   1.421 +    if (lastNode instanceof Text) {
   1.422 +      try {
   1.423 +        selection.extend(lastNode, lastNode.length);
   1.424 +      } catch (e) {
   1.425 +        Cu.reportError("SelectionHandler.js: _performSelection() whitespace trim fails: lastNode[" + lastNode +
   1.426 +          "] lastNode.length[" + lastNode.length + "]");
   1.427 +      }
   1.428 +    }
   1.429 +
   1.430 +    return true;
   1.431 +  },
   1.432 +
   1.433 +  /* Return true if the current selection (given by aPositions) is near to where the coordinates passed in */
   1.434 +  _selectionNearClick: function(aX, aY, aPositions) {
   1.435 +      let distance = 0;
   1.436 +
   1.437 +      // Check if the click was in the bounding box of the selection handles
   1.438 +      if (aPositions[0].left < aX && aX < aPositions[1].left
   1.439 +          && aPositions[0].top < aY && aY < aPositions[1].top) {
   1.440 +        distance = 0;
   1.441 +      } else {
   1.442 +        // If it was outside, check the distance to the center of the selection
   1.443 +        let selectposX = (aPositions[0].left + aPositions[1].left) / 2;
   1.444 +        let selectposY = (aPositions[0].top + aPositions[1].top) / 2;
   1.445 +
   1.446 +        let dx = Math.abs(selectposX - aX);
   1.447 +        let dy = Math.abs(selectposY - aY);
   1.448 +        distance = dx + dy;
   1.449 +      }
   1.450 +
   1.451 +      let maxSelectionDistance = Services.prefs.getIntPref("browser.ui.selection.distance");
   1.452 +      return (distance < maxSelectionDistance);
   1.453 +  },
   1.454 +
   1.455 +  /* Reads a value from an action. If the action defines the value as a function, will return the result of calling
   1.456 +     the function. Otherwise, will return the value itself. If the value isn't defined for this action, will return a default */
   1.457 +  _getValue: function(obj, name, defaultValue) {
   1.458 +    if (!(name in obj))
   1.459 +      return defaultValue;
   1.460 +
   1.461 +    if (typeof obj[name] == "function")
   1.462 +      return obj[name](this._targetElement);
   1.463 +
   1.464 +    return obj[name];
   1.465 +  },
   1.466 +
   1.467 +  addAction: function(action) {
   1.468 +    if (!action.id)
   1.469 +      action.id = uuidgen.generateUUID().toString()
   1.470 +
   1.471 +    if (this.actions[action.id])
   1.472 +      throw "Action with id " + action.id + " already added";
   1.473 +
   1.474 +    // Update actions list and actionbar UI if active.
   1.475 +    this.actions[action.id] = action;
   1.476 +    this._updateMenu();
   1.477 +    return action.id;
   1.478 +  },
   1.479 +
   1.480 +  removeAction: function(id) {
   1.481 +    // Update actions list and actionbar UI if active.
   1.482 +    delete this.actions[id];
   1.483 +    this._updateMenu();
   1.484 +  },
   1.485 +
   1.486 +  _updateMenu: function() {
   1.487 +    if (this._activeType == this.TYPE_NONE) {
   1.488 +      return;
   1.489 +    }
   1.490 +
   1.491 +    // Update actionbar UI.
   1.492 +    let actions = [];
   1.493 +    for (let type in this.actions) {
   1.494 +      let action = this.actions[type];
   1.495 +      if (action.selector.matches(this._targetElement)) {
   1.496 +        let a = {
   1.497 +          id: action.id,
   1.498 +          label: this._getValue(action, "label", ""),
   1.499 +          icon: this._getValue(action, "icon", "drawable://ic_status_logo"),
   1.500 +          showAsAction: this._getValue(action, "showAsAction", true),
   1.501 +          order: this._getValue(action, "order", 0)
   1.502 +        };
   1.503 +        actions.push(a);
   1.504 +      }
   1.505 +    }
   1.506 +
   1.507 +    actions.sort((a, b) => b.order - a.order);
   1.508 +
   1.509 +    sendMessageToJava({
   1.510 +      type: "TextSelection:Update",
   1.511 +      actions: actions
   1.512 +    });
   1.513 +  },
   1.514 +
   1.515 +  /*
   1.516 +   * Actionbar methods.
   1.517 +   */
   1.518 +  actions: {
   1.519 +    SELECT_ALL: {
   1.520 +      label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
   1.521 +      id: "selectall_action",
   1.522 +      icon: "drawable://ab_select_all",
   1.523 +      action: function(aElement) {
   1.524 +        SelectionHandler.startSelection(aElement);
   1.525 +        UITelemetry.addEvent("action.1", "actionbar", null, "select_all");
   1.526 +      },
   1.527 +      order: 5,
   1.528 +      selector: {
   1.529 +        matches: function(aElement) {
   1.530 +          return (aElement.textLength != 0);
   1.531 +        }
   1.532 +      }
   1.533 +    },
   1.534 +
   1.535 +    CUT: {
   1.536 +      label: Strings.browser.GetStringFromName("contextmenu.cut"),
   1.537 +      id: "cut_action",
   1.538 +      icon: "drawable://ab_cut",
   1.539 +      action: function(aElement) {
   1.540 +        let start = aElement.selectionStart;
   1.541 +        let end   = aElement.selectionEnd;
   1.542 +
   1.543 +        SelectionHandler.copySelection();
   1.544 +        aElement.value = aElement.value.substring(0, start) + aElement.value.substring(end)
   1.545 +
   1.546 +        // copySelection closes the selection. Show a caret where we just cut the text.
   1.547 +        SelectionHandler.attachCaret(aElement);
   1.548 +        UITelemetry.addEvent("action.1", "actionbar", null, "cut");
   1.549 +      },
   1.550 +      order: 4,
   1.551 +      selector: {
   1.552 +        matches: function(aElement) {
   1.553 +          return SelectionHandler.isElementEditableText(aElement) ?
   1.554 +            SelectionHandler.isSelectionActive() : false;
   1.555 +        }
   1.556 +      }
   1.557 +    },
   1.558 +
   1.559 +    COPY: {
   1.560 +      label: Strings.browser.GetStringFromName("contextmenu.copy"),
   1.561 +      id: "copy_action",
   1.562 +      icon: "drawable://ab_copy",
   1.563 +      action: function() {
   1.564 +        SelectionHandler.copySelection();
   1.565 +        UITelemetry.addEvent("action.1", "actionbar", null, "copy");
   1.566 +      },
   1.567 +      order: 3,
   1.568 +      selector: {
   1.569 +        matches: function(aElement) {
   1.570 +          // Don't include "copy" for password fields.
   1.571 +          // mozIsTextField(true) tests for only non-password fields.
   1.572 +          if (aElement instanceof Ci.nsIDOMHTMLInputElement && !aElement.mozIsTextField(true)) {
   1.573 +            return false;
   1.574 +          }
   1.575 +          return SelectionHandler.isSelectionActive();
   1.576 +        }
   1.577 +      }
   1.578 +    },
   1.579 +
   1.580 +    PASTE: {
   1.581 +      label: Strings.browser.GetStringFromName("contextmenu.paste"),
   1.582 +      id: "paste_action",
   1.583 +      icon: "drawable://ab_paste",
   1.584 +      action: function(aElement) {
   1.585 +        if (aElement && (aElement instanceof Ci.nsIDOMNSEditableElement)) {
   1.586 +          let target = aElement.QueryInterface(Ci.nsIDOMNSEditableElement);
   1.587 +          target.editor.paste(Ci.nsIClipboard.kGlobalClipboard);
   1.588 +          target.focus();
   1.589 +          SelectionHandler._closeSelection();
   1.590 +          UITelemetry.addEvent("action.1", "actionbar", null, "paste");
   1.591 +        }
   1.592 +      },
   1.593 +      order: 2,
   1.594 +      selector: {
   1.595 +        matches: function(aElement) {
   1.596 +          if (SelectionHandler.isElementEditableText(aElement)) {
   1.597 +            let flavors = ["text/unicode"];
   1.598 +            return Services.clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard);
   1.599 +          }
   1.600 +          return false;
   1.601 +        }
   1.602 +      }
   1.603 +    },
   1.604 +
   1.605 +    SHARE: {
   1.606 +      label: Strings.browser.GetStringFromName("contextmenu.share"),
   1.607 +      id: "share_action",
   1.608 +      icon: "drawable://ic_menu_share",
   1.609 +      action: function() {
   1.610 +        SelectionHandler.shareSelection();
   1.611 +        UITelemetry.addEvent("action.1", "actionbar", null, "share");
   1.612 +      },
   1.613 +      selector: {
   1.614 +        matches: function() {
   1.615 +          return SelectionHandler.isSelectionActive();
   1.616 +        }
   1.617 +      }
   1.618 +    },
   1.619 +
   1.620 +    SEARCH: {
   1.621 +      label: function() {
   1.622 +        return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
   1.623 +      },
   1.624 +      id: "search_action",
   1.625 +      icon: "drawable://ab_search",
   1.626 +      action: function() {
   1.627 +        SelectionHandler.searchSelection();
   1.628 +        SelectionHandler._closeSelection();
   1.629 +        UITelemetry.addEvent("action.1", "actionbar", null, "search");
   1.630 +      },
   1.631 +      order: 1,
   1.632 +      selector: {
   1.633 +        matches: function() {
   1.634 +          return SelectionHandler.isSelectionActive();
   1.635 +        }
   1.636 +      }
   1.637 +    },
   1.638 +
   1.639 +    CALL: {
   1.640 +      label: Strings.browser.GetStringFromName("contextmenu.call"),
   1.641 +      id: "call_action",
   1.642 +      icon: "drawable://phone",
   1.643 +      action: function() {
   1.644 +        SelectionHandler.callSelection();
   1.645 +        UITelemetry.addEvent("action.1", "actionbar", null, "call");
   1.646 +      },
   1.647 +      order: 1,
   1.648 +      selector: {
   1.649 +        matches: function () {
   1.650 +          return SelectionHandler._getSelectedPhoneNumber() != null;
   1.651 +        }
   1.652 +      }
   1.653 +    }
   1.654 +  },
   1.655 +
   1.656 +  /*
   1.657 +   * Called by BrowserEventHandler when the user taps in a form input.
   1.658 +   * Initializes SelectionHandler and positions the caret handle.
   1.659 +   *
   1.660 +   * @param aX, aY tap location in client coordinates.
   1.661 +   */
   1.662 +  attachCaret: function sh_attachCaret(aElement) {
   1.663 +    // Ensure it isn't disabled, isn't handled by Android native dialog, and is editable text element
   1.664 +    if (aElement.disabled || InputWidgetHelper.hasInputWidget(aElement) || !this.isElementEditableText(aElement)) {
   1.665 +      return;
   1.666 +    }
   1.667 +
   1.668 +    this._initTargetInfo(aElement, this.TYPE_CURSOR);
   1.669 +
   1.670 +    // Caret-specific observer/listeners
   1.671 +    Services.obs.addObserver(this, "TextSelection:UpdateCaretPos", false);
   1.672 +    BrowserApp.deck.addEventListener("keyup", this, false);
   1.673 +    BrowserApp.deck.addEventListener("compositionupdate", this, false);
   1.674 +    BrowserApp.deck.addEventListener("compositionend", this, false);
   1.675 +
   1.676 +    this._activeType = this.TYPE_CURSOR;
   1.677 +
   1.678 +    // Determine position and show caret, open actionbar
   1.679 +    this._positionHandles();
   1.680 +    sendMessageToJava({
   1.681 +      type: "TextSelection:ShowHandles",
   1.682 +      handles: [this.HANDLE_TYPE_MIDDLE]
   1.683 +    });
   1.684 +    this._updateMenu();
   1.685 +  },
   1.686 +
   1.687 +  // Target initialization for both TYPE_CURSOR and TYPE_SELECTION
   1.688 +  _initTargetInfo: function sh_initTargetInfo(aElement, aSelectionType) {
   1.689 +    this._targetElement = aElement;
   1.690 +    if (aElement instanceof Ci.nsIDOMNSEditableElement) {
   1.691 +      if (aSelectionType === this.TYPE_SELECTION) {
   1.692 +        // Blur the targetElement to force IME code to undo previous style compositions
   1.693 +        // (visible underlines / etc generated by autoCorrection, autoSuggestion)
   1.694 +        aElement.blur();
   1.695 +      }
   1.696 +      // Ensure targetElement is now focused normally
   1.697 +      aElement.focus();
   1.698 +    }
   1.699 +
   1.700 +    this._stopDraggingHandles();
   1.701 +    this._contentWindow = aElement.ownerDocument.defaultView;
   1.702 +    this._isRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");
   1.703 +
   1.704 +    this._addObservers();
   1.705 +  },
   1.706 +
   1.707 +  _getSelection: function sh_getSelection() {
   1.708 +    if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
   1.709 +      return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selection;
   1.710 +    else
   1.711 +      return this._contentWindow.getSelection();
   1.712 +  },
   1.713 +
   1.714 +  _getSelectedText: function sh_getSelectedText() {
   1.715 +    if (!this._contentWindow)
   1.716 +      return "";
   1.717 +
   1.718 +    let selection = this._getSelection();
   1.719 +    if (!selection)
   1.720 +      return "";
   1.721 +
   1.722 +    if (this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
   1.723 +      return selection.QueryInterface(Ci.nsISelectionPrivate).
   1.724 +        toStringWithFormat("text/plain", Ci.nsIDocumentEncoder.OutputPreformatted | Ci.nsIDocumentEncoder.OutputRaw, 0);
   1.725 +    }
   1.726 +
   1.727 +    return selection.toString().trim();
   1.728 +  },
   1.729 +
   1.730 +  _getSelectionController: function sh_getSelectionController() {
   1.731 +    if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
   1.732 +      return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selectionController;
   1.733 +    else
   1.734 +      return this._contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
   1.735 +                                 getInterface(Ci.nsIWebNavigation).
   1.736 +                                 QueryInterface(Ci.nsIInterfaceRequestor).
   1.737 +                                 getInterface(Ci.nsISelectionDisplay).
   1.738 +                                 QueryInterface(Ci.nsISelectionController);
   1.739 +  },
   1.740 +
   1.741 +  // Used by the contextmenu "matches" functions in ClipboardHelper
   1.742 +  isSelectionActive: function sh_isSelectionActive() {
   1.743 +    return (this._activeType == this.TYPE_SELECTION);
   1.744 +  },
   1.745 +
   1.746 +  isElementEditableText: function (aElement) {
   1.747 +    return ((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
   1.748 +            (aElement instanceof HTMLTextAreaElement));
   1.749 +  },
   1.750 +
   1.751 +  /*
   1.752 +   * Helper function for moving the selection inside an editable element.
   1.753 +   *
   1.754 +   * @param aAnchorX the stationary handle's x-coordinate in client coordinates
   1.755 +   * @param aX the moved handle's x-coordinate in client coordinates
   1.756 +   * @param aCaretPos the current position of the caret
   1.757 +   */
   1.758 +  _moveSelectionInEditable: function sh_moveSelectionInEditable(aAnchorX, aX, aCaretPos) {
   1.759 +    let anchorOffset = aX < aAnchorX ? this._targetElement.selectionEnd
   1.760 +                                     : this._targetElement.selectionStart;
   1.761 +    let newOffset = aCaretPos.offset;
   1.762 +    let [start, end] = anchorOffset <= newOffset ?
   1.763 +                       [anchorOffset, newOffset] :
   1.764 +                       [newOffset, anchorOffset];
   1.765 +    this._targetElement.setSelectionRange(start, end);
   1.766 +  },
   1.767 +
   1.768 +  /*
   1.769 +   * Moves the selection as the user drags a selection handle.
   1.770 +   *
   1.771 +   * @param aIsStartHandle whether the user is moving the start handle (as opposed to the end handle)
   1.772 +   * @param aX, aY selection point in client coordinates
   1.773 +   */
   1.774 +  _moveSelection: function sh_moveSelection(aIsStartHandle, aX, aY) {
   1.775 +    // XXX We should be smarter about the coordinates we pass to caretPositionFromPoint, especially
   1.776 +    // in editable targets. We should factor out the logic that's currently in _sendMouseEvents.
   1.777 +    let viewOffset = this._getViewOffset();
   1.778 +    let caretPos = this._contentWindow.document.caretPositionFromPoint(aX - viewOffset.x, aY - viewOffset.y);
   1.779 +    if (!caretPos) {
   1.780 +      // User moves handle offscreen while positioning
   1.781 +      return;
   1.782 +    }
   1.783 +
   1.784 +    // Constrain text selection within editable elements.
   1.785 +    let targetIsEditable = this._targetElement instanceof Ci.nsIDOMNSEditableElement;
   1.786 +    if (targetIsEditable && (caretPos.offsetNode != this._targetElement)) {
   1.787 +      return;
   1.788 +    }
   1.789 +
   1.790 +    // Update the cache as the handle is dragged (keep the cache in client coordinates).
   1.791 +    if (aIsStartHandle) {
   1.792 +      this._cache.start.x = aX;
   1.793 +      this._cache.start.y = aY;
   1.794 +    } else {
   1.795 +      this._cache.end.x = aX;
   1.796 +      this._cache.end.y = aY;
   1.797 +    }
   1.798 +
   1.799 +    let selection = this._getSelection();
   1.800 +
   1.801 +    // The handles work the same on both LTR and RTL pages, but the anchor/focus nodes
   1.802 +    // are reversed, so we need to reverse the logic to extend the selection.
   1.803 +    if ((aIsStartHandle && !this._isRTL) || (!aIsStartHandle && this._isRTL)) {
   1.804 +      if (targetIsEditable) {
   1.805 +        let anchorX = this._isRTL ? this._cache.start.x : this._cache.end.x;
   1.806 +        this._moveSelectionInEditable(anchorX, aX, caretPos);
   1.807 +      } else {
   1.808 +        let focusNode = selection.focusNode;
   1.809 +        let focusOffset = selection.focusOffset;
   1.810 +        selection.collapse(caretPos.offsetNode, caretPos.offset);
   1.811 +        selection.extend(focusNode, focusOffset);
   1.812 +      }
   1.813 +    } else {
   1.814 +      if (targetIsEditable) {
   1.815 +        let anchorX = this._isRTL ? this._cache.end.x : this._cache.start.x;
   1.816 +        this._moveSelectionInEditable(anchorX, aX, caretPos);
   1.817 +      } else {
   1.818 +        selection.extend(caretPos.offsetNode, caretPos.offset);
   1.819 +      }
   1.820 +    }
   1.821 +  },
   1.822 +
   1.823 +  _sendMouseEvents: function sh_sendMouseEvents(aX, aY, useShift) {
   1.824 +    // If we're positioning a cursor in an input field, make sure the handle
   1.825 +    // stays within the bounds of the field
   1.826 +    if (this._activeType == this.TYPE_CURSOR) {
   1.827 +      // Get rect of text inside element
   1.828 +      let range = document.createRange();
   1.829 +      range.selectNodeContents(this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.rootElement);
   1.830 +      let textBounds = range.getBoundingClientRect();
   1.831 +
   1.832 +      // Get rect of editor
   1.833 +      let editorBounds = this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_EDITOR_RECT, 0, 0, 0, 0,
   1.834 +                                                                 this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
   1.835 +      // the return value from sendQueryContentEvent is in LayoutDevice pixels and we want CSS pixels, so
   1.836 +      // divide by the pixel ratio
   1.837 +      let editorRect = new Rect(editorBounds.left / window.devicePixelRatio,
   1.838 +                                editorBounds.top / window.devicePixelRatio,
   1.839 +                                editorBounds.width / window.devicePixelRatio,
   1.840 +                                editorBounds.height / window.devicePixelRatio);
   1.841 +
   1.842 +      // Use intersection of the text rect and the editor rect
   1.843 +      let rect = new Rect(textBounds.left, textBounds.top, textBounds.width, textBounds.height);
   1.844 +      rect.restrictTo(editorRect);
   1.845 +
   1.846 +      // Clamp vertically and scroll if handle is at bounds. The top and bottom
   1.847 +      // must be restricted by an additional pixel since clicking on the top
   1.848 +      // edge of an input field moves the cursor to the beginning of that
   1.849 +      // field's text (and clicking the bottom moves the cursor to the end).
   1.850 +      if (aY < rect.y + 1) {
   1.851 +        aY = rect.y + 1;
   1.852 +        this._getSelectionController().scrollLine(false);
   1.853 +      } else if (aY > rect.y + rect.height - 1) {
   1.854 +        aY = rect.y + rect.height - 1;
   1.855 +        this._getSelectionController().scrollLine(true);
   1.856 +      }
   1.857 +
   1.858 +      // Clamp horizontally and scroll if handle is at bounds
   1.859 +      if (aX < rect.x) {
   1.860 +        aX = rect.x;
   1.861 +        this._getSelectionController().scrollCharacter(false);
   1.862 +      } else if (aX > rect.x + rect.width) {
   1.863 +        aX = rect.x + rect.width;
   1.864 +        this._getSelectionController().scrollCharacter(true);
   1.865 +      }
   1.866 +    } else if (this._activeType == this.TYPE_SELECTION) {
   1.867 +      // Send mouse event 1px too high to prevent selection from entering the line below where it should be
   1.868 +      aY -= 1;
   1.869 +    }
   1.870 +
   1.871 +    this._domWinUtils.sendMouseEventToWindow("mousedown", aX, aY, 0, 0, useShift ? Ci.nsIDOMNSEvent.SHIFT_MASK : 0, true);
   1.872 +    this._domWinUtils.sendMouseEventToWindow("mouseup", aX, aY, 0, 0, useShift ? Ci.nsIDOMNSEvent.SHIFT_MASK : 0, true);
   1.873 +  },
   1.874 +
   1.875 +  copySelection: function sh_copySelection() {
   1.876 +    let selectedText = this._getSelectedText();
   1.877 +    if (selectedText.length) {
   1.878 +      let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
   1.879 +      clipboard.copyString(selectedText, this._contentWindow.document);
   1.880 +      NativeWindow.toast.show(Strings.browser.GetStringFromName("selectionHelper.textCopied"), "short");
   1.881 +    }
   1.882 +    this._closeSelection();
   1.883 +  },
   1.884 +
   1.885 +  shareSelection: function sh_shareSelection() {
   1.886 +    let selectedText = this._getSelectedText();
   1.887 +    if (selectedText.length) {
   1.888 +      sendMessageToJava({
   1.889 +        type: "Share:Text",
   1.890 +        text: selectedText
   1.891 +      });
   1.892 +    }
   1.893 +    this._closeSelection();
   1.894 +  },
   1.895 +
   1.896 +  searchSelection: function sh_searchSelection() {
   1.897 +    let selectedText = this._getSelectedText();
   1.898 +    if (selectedText.length) {
   1.899 +      let req = Services.search.defaultEngine.getSubmission(selectedText);
   1.900 +      let parent = BrowserApp.selectedTab;
   1.901 +      let isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
   1.902 +      // Set current tab as parent of new tab, and set new tab as private if the parent is.
   1.903 +      BrowserApp.addTab(req.uri.spec, {parentId: parent.id,
   1.904 +                                       selected: true,
   1.905 +                                       isPrivate: isPrivate});
   1.906 +    }
   1.907 +    this._closeSelection();
   1.908 +  },
   1.909 +
   1.910 +  _phoneRegex: /^\+?[0-9\s,-.\(\)*#pw]{1,30}$/,
   1.911 +
   1.912 +  _getSelectedPhoneNumber: function sh_getSelectedPhoneNumber() {
   1.913 +    return this._isPhoneNumber(this._getSelectedText().trim());
   1.914 +  },
   1.915 +
   1.916 +  _isPhoneNumber: function sh_isPhoneNumber(selectedText) {
   1.917 +    return (this._phoneRegex.test(selectedText) ? selectedText : null);
   1.918 +  },
   1.919 +
   1.920 +  callSelection: function sh_callSelection() {
   1.921 +    let selectedText = this._getSelectedPhoneNumber();
   1.922 +    if (selectedText) {
   1.923 +      BrowserApp.loadURI("tel:" + selectedText);
   1.924 +    }
   1.925 +    this._closeSelection();
   1.926 +  },
   1.927 +
   1.928 +  /*
   1.929 +   * Shuts SelectionHandler down.
   1.930 +   */
   1.931 +  _closeSelection: function sh_closeSelection() {
   1.932 +    // Bail if there's no active selection
   1.933 +    if (this._activeType == this.TYPE_NONE)
   1.934 +      return;
   1.935 +
   1.936 +    if (this._activeType == this.TYPE_SELECTION)
   1.937 +      this._clearSelection();
   1.938 +
   1.939 +    this._deactivate();
   1.940 +  },
   1.941 +
   1.942 +  _clearSelection: function sh_clearSelection() {
   1.943 +    let selection = this._getSelection();
   1.944 +    if (selection) {
   1.945 +      // Remove our listener before we clear the selection
   1.946 +      selection.QueryInterface(Ci.nsISelectionPrivate).removeSelectionListener(this);
   1.947 +      // Clear selection without clearing the anchorNode or focusNode
   1.948 +      if (selection.rangeCount != 0) {
   1.949 +        selection.collapseToStart();
   1.950 +      }
   1.951 +    }
   1.952 +  },
   1.953 +
   1.954 +  _deactivate: function sh_deactivate() {
   1.955 +    this._stopDraggingHandles();
   1.956 +    // Hide handle/caret, close actionbar
   1.957 +    sendMessageToJava({ type: "TextSelection:HideHandles" });
   1.958 +
   1.959 +    this._removeObservers();
   1.960 +
   1.961 +    // Only observed for caret positioning
   1.962 +    if (this._activeType == this.TYPE_CURSOR) {
   1.963 +      Services.obs.removeObserver(this, "TextSelection:UpdateCaretPos");
   1.964 +      BrowserApp.deck.removeEventListener("keyup", this);
   1.965 +      BrowserApp.deck.removeEventListener("compositionupdate", this);
   1.966 +      BrowserApp.deck.removeEventListener("compositionend", this);
   1.967 +    }
   1.968 +
   1.969 +    this._contentWindow = null;
   1.970 +    this._targetElement = null;
   1.971 +    this._isRTL = false;
   1.972 +    this._cache = null;
   1.973 +    this._ignoreCompositionChanges = false;
   1.974 +    this._prevHandlePositions = [];
   1.975 +    this._prevTargetElementHasText = null;
   1.976 +
   1.977 +    this._activeType = this.TYPE_NONE;
   1.978 +  },
   1.979 +
   1.980 +  _getViewOffset: function sh_getViewOffset() {
   1.981 +    let offset = { x: 0, y: 0 };
   1.982 +    let win = this._contentWindow;
   1.983 +
   1.984 +    // Recursively look through frames to compute the total position offset.
   1.985 +    while (win.frameElement) {
   1.986 +      let rect = win.frameElement.getBoundingClientRect();
   1.987 +      offset.x += rect.left;
   1.988 +      offset.y += rect.top;
   1.989 +
   1.990 +      win = win.parent;
   1.991 +    }
   1.992 +
   1.993 +    return offset;
   1.994 +  },
   1.995 +
   1.996 +  _pointInSelection: function sh_pointInSelection(aX, aY) {
   1.997 +    let offset = this._getViewOffset();
   1.998 +    let rangeRect = this._getSelection().getRangeAt(0).getBoundingClientRect();
   1.999 +    let radius = ElementTouchHelper.getTouchRadius();
  1.1000 +    return (aX - offset.x > rangeRect.left - radius.left &&
  1.1001 +            aX - offset.x < rangeRect.right + radius.right &&
  1.1002 +            aY - offset.y > rangeRect.top - radius.top &&
  1.1003 +            aY - offset.y < rangeRect.bottom + radius.bottom);
  1.1004 +  },
  1.1005 +
  1.1006 +  // Returns true if the selection has been reversed. Takes optional aIsStartHandle
  1.1007 +  // param to decide whether the selection has been reversed.
  1.1008 +  _updateCacheForSelection: function sh_updateCacheForSelection(aIsStartHandle) {
  1.1009 +    let rects = this._getSelection().getRangeAt(0).getClientRects();
  1.1010 +    if (!rects[0]) {
  1.1011 +      // nsISelection object exists, but there's nothing actually selected
  1.1012 +      throw "Failed to update cache for invalid selection";
  1.1013 +    }
  1.1014 +
  1.1015 +    let start = { x: this._isRTL ? rects[0].right : rects[0].left, y: rects[0].bottom };
  1.1016 +    let end = { x: this._isRTL ? rects[rects.length - 1].left : rects[rects.length - 1].right, y: rects[rects.length - 1].bottom };
  1.1017 +
  1.1018 +    let selectionReversed = false;
  1.1019 +    if (this._cache.start) {
  1.1020 +      // If the end moved past the old end, but we're dragging the start handle, then that handle should become the end handle (and vice versa)
  1.1021 +      selectionReversed = (aIsStartHandle && (end.y > this._cache.end.y || (end.y == this._cache.end.y && end.x > this._cache.end.x))) ||
  1.1022 +                          (!aIsStartHandle && (start.y < this._cache.start.y || (start.y == this._cache.start.y && start.x < this._cache.start.x)));
  1.1023 +    }
  1.1024 +
  1.1025 +    this._cache.start = start;
  1.1026 +    this._cache.end = end;
  1.1027 +
  1.1028 +    return selectionReversed;
  1.1029 +  },
  1.1030 +
  1.1031 +  _getHandlePositions: function sh_getHandlePositions(scroll) {
  1.1032 +    // the checkHidden function tests to see if the given point is hidden inside an
  1.1033 +    // iframe/subdocument. this is so that if we select some text inside an iframe and
  1.1034 +    // scroll the iframe so the selection is out of view, we hide the handles rather
  1.1035 +    // than having them float on top of the main page content.
  1.1036 +    let checkHidden = function(x, y) {
  1.1037 +      return false;
  1.1038 +    };
  1.1039 +    if (this._contentWindow.frameElement) {
  1.1040 +      let bounds = this._contentWindow.frameElement.getBoundingClientRect();
  1.1041 +      checkHidden = function(x, y) {
  1.1042 +        return x < 0 || y < 0 || x > bounds.width || y > bounds.height;
  1.1043 +      };
  1.1044 +    }
  1.1045 +
  1.1046 +    let positions = null;
  1.1047 +    if (this._activeType == this.TYPE_CURSOR) {
  1.1048 +      // The left and top properties returned are relative to the client area
  1.1049 +      // of the window, so we don't need to account for a sub-frame offset.
  1.1050 +      let cursor = this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_CARET_RECT, this._targetElement.selectionEnd, 0, 0, 0,
  1.1051 +                                                           this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
  1.1052 +      // the return value from sendQueryContentEvent is in LayoutDevice pixels and we want CSS pixels, so
  1.1053 +      // divide by the pixel ratio
  1.1054 +      let x = cursor.left / window.devicePixelRatio;
  1.1055 +      let y = (cursor.top + cursor.height) / window.devicePixelRatio;
  1.1056 +      return [{ handle: this.HANDLE_TYPE_MIDDLE,
  1.1057 +                left: x + scroll.X,
  1.1058 +                top: y + scroll.Y,
  1.1059 +                hidden: checkHidden(x, y) }];
  1.1060 +    } else {
  1.1061 +      let sx = this._cache.start.x;
  1.1062 +      let sy = this._cache.start.y;
  1.1063 +      let ex = this._cache.end.x;
  1.1064 +      let ey = this._cache.end.y;
  1.1065 +
  1.1066 +      // Translate coordinates to account for selections in sub-frames. We can't cache
  1.1067 +      // this because the top-level page may have scrolled since selection started.
  1.1068 +      let offset = this._getViewOffset();
  1.1069 +
  1.1070 +      return  [{ handle: this.HANDLE_TYPE_START,
  1.1071 +                 left: sx + offset.x + scroll.X,
  1.1072 +                 top: sy + offset.y + scroll.Y,
  1.1073 +                 hidden: checkHidden(sx, sy) },
  1.1074 +               { handle: this.HANDLE_TYPE_END,
  1.1075 +                 left: ex + offset.x + scroll.X,
  1.1076 +                 top: ey + offset.y + scroll.Y,
  1.1077 +                 hidden: checkHidden(ex, ey) }];
  1.1078 +    }
  1.1079 +  },
  1.1080 +
  1.1081 +  // Position handles, but avoid superfluous re-positioning (helps during
  1.1082 +  // "TextSelection:LayerReflow", "scroll" of top-level document, etc).
  1.1083 +  _positionHandlesOnChange: function() {
  1.1084 +    // Helper function to compare position messages
  1.1085 +    let samePositions = function(aPrev, aCurr) {
  1.1086 +      if (aPrev.length != aCurr.length) {
  1.1087 +        return false;
  1.1088 +      }
  1.1089 +      for (let i = 0; i < aPrev.length; i++) {
  1.1090 +        if (aPrev[i].left != aCurr[i].left ||
  1.1091 +            aPrev[i].top != aCurr[i].top ||
  1.1092 +            aPrev[i].hidden != aCurr[i].hidden) {
  1.1093 +          return false;
  1.1094 +        }
  1.1095 +      }
  1.1096 +      return true;
  1.1097 +    }
  1.1098 +
  1.1099 +    let positions = this._getHandlePositions(this._getScrollPos());
  1.1100 +    if (!samePositions(this._prevHandlePositions, positions)) {
  1.1101 +      this._positionHandles(positions);
  1.1102 +    }
  1.1103 +  },
  1.1104 +
  1.1105 +  // Position handles, allow for re-position, in case user drags handle
  1.1106 +  // to invalid position, then releases, we can put it back where it started
  1.1107 +  // positions is an array of objects with data about handle positions,
  1.1108 +  // which we get from _getHandlePositions.
  1.1109 +  _positionHandles: function sh_positionHandles(positions) {
  1.1110 +    if (!positions) {
  1.1111 +      positions = this._getHandlePositions(this._getScrollPos());
  1.1112 +    }
  1.1113 +    sendMessageToJava({
  1.1114 +      type: "TextSelection:PositionHandles",
  1.1115 +      positions: positions,
  1.1116 +      rtl: this._isRTL
  1.1117 +    });
  1.1118 +    this._prevHandlePositions = positions;
  1.1119 +
  1.1120 +    // Text state transitions (text <--> no text) will affect selection context and actionbar display
  1.1121 +    let currTargetElementHasText = (this._targetElement.textLength > 0);
  1.1122 +    if (currTargetElementHasText != this._prevTargetElementHasText) {
  1.1123 +      this._prevTargetElementHasText = currTargetElementHasText;
  1.1124 +      this._updateMenu();
  1.1125 +    }
  1.1126 +  },
  1.1127 +
  1.1128 +  subdocumentScrolled: function sh_subdocumentScrolled(aElement) {
  1.1129 +    if (this._activeType == this.TYPE_NONE) {
  1.1130 +      return;
  1.1131 +    }
  1.1132 +    let scrollView = aElement.ownerDocument.defaultView;
  1.1133 +    let view = this._contentWindow;
  1.1134 +    while (true) {
  1.1135 +      if (view == scrollView) {
  1.1136 +        // The selection is in a view (or sub-view) of the view that scrolled.
  1.1137 +        // So we need to reposition the handles.
  1.1138 +        if (this._activeType == this.TYPE_SELECTION) {
  1.1139 +          this._updateCacheForSelection();
  1.1140 +        }
  1.1141 +        this._positionHandles();
  1.1142 +        break;
  1.1143 +      }
  1.1144 +      if (view == view.parent) {
  1.1145 +        break;
  1.1146 +      }
  1.1147 +      view = view.parent;
  1.1148 +    }
  1.1149 +  }
  1.1150 +};

mercurial