mobile/android/chrome/content/SelectionHandler.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     4  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 var SelectionHandler = {
     8   HANDLE_TYPE_START: "START",
     9   HANDLE_TYPE_MIDDLE: "MIDDLE",
    10   HANDLE_TYPE_END: "END",
    12   TYPE_NONE: 0,
    13   TYPE_CURSOR: 1,
    14   TYPE_SELECTION: 2,
    16   SELECT_ALL: 0,
    17   SELECT_AT_POINT: 1,
    19   // Keeps track of data about the dimensions of the selection. Coordinates
    20   // stored here are relative to the _contentWindow window.
    21   _cache: null,
    22   _activeType: 0, // TYPE_NONE
    23   _draggingHandles: false, // True while user drags text selection handles
    24   _ignoreCompositionChanges: false, // Persist caret during IME composition updates
    25   _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages
    27   // TargetElement changes (text <--> no text) trigger actionbar UI update
    28   _prevTargetElementHasText: null,
    30   // The window that holds the selection (can be a sub-frame)
    31   get _contentWindow() {
    32     if (this._contentWindowRef)
    33       return this._contentWindowRef.get();
    34     return null;
    35   },
    37   set _contentWindow(aContentWindow) {
    38     this._contentWindowRef = Cu.getWeakReference(aContentWindow);
    39   },
    41   get _targetElement() {
    42     if (this._targetElementRef)
    43       return this._targetElementRef.get();
    44     return null;
    45   },
    47   set _targetElement(aTargetElement) {
    48     this._targetElementRef = Cu.getWeakReference(aTargetElement);
    49   },
    51   get _domWinUtils() {
    52     return BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
    53                                                     getInterface(Ci.nsIDOMWindowUtils);
    54   },
    56   _isRTL: false,
    58   _addObservers: function sh_addObservers() {
    59     Services.obs.addObserver(this, "Gesture:SingleTap", false);
    60     Services.obs.addObserver(this, "Tab:Selected", false);
    61     Services.obs.addObserver(this, "after-viewport-change", false);
    62     Services.obs.addObserver(this, "TextSelection:Move", false);
    63     Services.obs.addObserver(this, "TextSelection:Position", false);
    64     Services.obs.addObserver(this, "TextSelection:End", false);
    65     Services.obs.addObserver(this, "TextSelection:Action", false);
    66     Services.obs.addObserver(this, "TextSelection:LayerReflow", false);
    68     BrowserApp.deck.addEventListener("pagehide", this, false);
    69     BrowserApp.deck.addEventListener("blur", this, true);
    70     BrowserApp.deck.addEventListener("scroll", this, true);
    71   },
    73   _removeObservers: function sh_removeObservers() {
    74     Services.obs.removeObserver(this, "Gesture:SingleTap");
    75     Services.obs.removeObserver(this, "Tab:Selected");
    76     Services.obs.removeObserver(this, "after-viewport-change");
    77     Services.obs.removeObserver(this, "TextSelection:Move");
    78     Services.obs.removeObserver(this, "TextSelection:Position");
    79     Services.obs.removeObserver(this, "TextSelection:End");
    80     Services.obs.removeObserver(this, "TextSelection:Action");
    81     Services.obs.removeObserver(this, "TextSelection:LayerReflow");
    83     BrowserApp.deck.removeEventListener("pagehide", this, false);
    84     BrowserApp.deck.removeEventListener("blur", this, true);
    85     BrowserApp.deck.removeEventListener("scroll", this, true);
    86   },
    88   observe: function sh_observe(aSubject, aTopic, aData) {
    89     switch (aTopic) {
    90       // Update handle/caret position on page reflow (keyboard open/close,
    91       // dynamic DOM changes, orientation updates, etc).
    92       case "TextSelection:LayerReflow": {
    93         if (this._activeType == this.TYPE_SELECTION) {
    94           this._updateCacheForSelection();
    95         }
    96         if (this._activeType != this.TYPE_NONE) {
    97           this._positionHandlesOnChange();
    98         }
    99         break;
   100       }
   102       // Update caret position on keyboard activity
   103       case "TextSelection:UpdateCaretPos":
   104         // Generated by IME close, autoCorrection / styling
   105         this._positionHandles();
   106         break;
   108       case "Gesture:SingleTap": {
   109         if (this._activeType == this.TYPE_SELECTION) {
   110           let data = JSON.parse(aData);
   111           if (!this._pointInSelection(data.x, data.y))
   112             this._closeSelection();
   113         } else if (this._activeType == this.TYPE_CURSOR) {
   114           // attachCaret() is called in the "Gesture:SingleTap" handler in BrowserEventHandler
   115           // We're guaranteed to call this first, because this observer was added last
   116           this._deactivate();
   117         }
   118         break;
   119       }
   120       case "Tab:Selected":
   121       case "TextSelection:End":
   122         this._closeSelection();
   123         break;
   124       case "TextSelection:Action":
   125         for (let type in this.actions) {
   126           if (this.actions[type].id == aData) {
   127             this.actions[type].action(this._targetElement);
   128             break;
   129           }
   130         }
   131         break;
   132       case "after-viewport-change": {
   133         if (this._activeType == this.TYPE_SELECTION) {
   134           // Update the cache after the viewport changes (e.g. panning, zooming).
   135           this._updateCacheForSelection();
   136         }
   137         break;
   138       }
   139       case "TextSelection:Move": {
   140         let data = JSON.parse(aData);
   141         if (this._activeType == this.TYPE_SELECTION) {
   142           this._startDraggingHandles();
   143           this._moveSelection(data.handleType == this.HANDLE_TYPE_START, data.x, data.y);
   145         } else if (this._activeType == this.TYPE_CURSOR) {
   146           this._startDraggingHandles();
   148           // Ignore IMM composition notifications when caret movement starts
   149           this._ignoreCompositionChanges = true;
   151           // Send a click event to the text box, which positions the caret
   152           this._sendMouseEvents(data.x, data.y);
   154           // Move the handle directly under the caret
   155           this._positionHandles();
   156         }
   157         break;
   158       }
   159       case "TextSelection:Position": {
   160         if (this._activeType == this.TYPE_SELECTION) {
   161           this._startDraggingHandles();
   163           // Check to see if the handles should be reversed.
   164           let isStartHandle = JSON.parse(aData).handleType == this.HANDLE_TYPE_START;
   165           try {
   166             let selectionReversed = this._updateCacheForSelection(isStartHandle);
   167             if (selectionReversed) {
   168               // Reverse the anchor and focus to correspond to the new start and end handles.
   169               let selection = this._getSelection();
   170               let anchorNode = selection.anchorNode;
   171               let anchorOffset = selection.anchorOffset;
   172               selection.collapse(selection.focusNode, selection.focusOffset);
   173               selection.extend(anchorNode, anchorOffset);
   174             }
   175           } catch (e) {
   176             // User finished handle positioning with one end off the screen
   177             this._closeSelection();
   178             break;
   179           }
   181           this._stopDraggingHandles();
   182           this._positionHandles();
   183           // Changes to handle position can affect selection context and actionbar display
   184           this._updateMenu();
   186         } else if (this._activeType == this.TYPE_CURSOR) {
   187           // Act on IMM composition notifications after caret movement ends
   188           this._ignoreCompositionChanges = false;
   189           this._stopDraggingHandles();
   190           this._positionHandles();
   192         } else {
   193           Cu.reportError("Ignored \"TextSelection:Position\" message during invalid selection status");
   194         }
   196         break;
   197       }
   199       case "TextSelection:Get":
   200         sendMessageToJava({
   201           type: "TextSelection:Data",
   202           requestId: aData,
   203           text: this._getSelectedText()
   204         });
   205         break;
   206     }
   207   },
   209   // Ignore selectionChange notifications during handle dragging, disable dynamic
   210   // IME text compositions (autoSuggest, autoCorrect, etc)
   211   _startDraggingHandles: function sh_startDraggingHandles() {
   212     if (!this._draggingHandles) {
   213       this._draggingHandles = true;
   214       sendMessageToJava({ type: "TextSelection:DraggingHandle", dragging: true });
   215     }
   216   },
   218   // Act on selectionChange notifications when not dragging handles, allow dynamic
   219   // IME text compositions (autoSuggest, autoCorrect, etc)
   220   _stopDraggingHandles: function sh_stopDraggingHandles() {
   221     if (this._draggingHandles) {
   222       this._draggingHandles = false;
   223       sendMessageToJava({ type: "TextSelection:DraggingHandle", dragging: false });
   224     }
   225   },
   227   handleEvent: function sh_handleEvent(aEvent) {
   228     switch (aEvent.type) {
   229       case "scroll":
   230         // Maintain position when top-level document is scrolled
   231         this._positionHandlesOnChange();
   232         break;
   234       case "pagehide":
   235       case "blur":
   236         this._closeSelection();
   237         break;
   239       // Update caret position on keyboard activity
   240       case "keyup":
   241         // Not generated by Swiftkeyboard
   242       case "compositionupdate":
   243       case "compositionend":
   244         // Generated by SwiftKeyboard, et. al.
   245         if (!this._ignoreCompositionChanges) {
   246           this._positionHandles();
   247         }
   248         break;
   249     }
   250   },
   252   /** Returns true if the provided element can be selected in text selection, false otherwise. */
   253   canSelect: function sh_canSelect(aElement) {
   254     return !(aElement instanceof Ci.nsIDOMHTMLButtonElement ||
   255              aElement instanceof Ci.nsIDOMHTMLEmbedElement ||
   256              aElement instanceof Ci.nsIDOMHTMLImageElement ||
   257              aElement instanceof Ci.nsIDOMHTMLMediaElement) &&
   258              aElement.style.MozUserSelect != 'none';
   259   },
   261   _getScrollPos: function sh_getScrollPos() {
   262     // Get the current display position
   263     let scrollX = {}, scrollY = {};
   264     this._contentWindow.top.QueryInterface(Ci.nsIInterfaceRequestor).
   265                             getInterface(Ci.nsIDOMWindowUtils).getScrollXY(false, scrollX, scrollY);
   266     return {
   267       X: scrollX.value,
   268       Y: scrollY.value
   269     };
   270   },
   272   notifySelectionChanged: function sh_notifySelectionChanged(aDocument, aSelection, aReason) {
   273     // Ignore selectionChange notifications during handle movements
   274     if (this._draggingHandles) {
   275       return;
   276     }
   278     // If the selection was collapsed to Start or to End, always close it
   279     if ((aReason & Ci.nsISelectionListener.COLLAPSETOSTART_REASON) ||
   280         (aReason & Ci.nsISelectionListener.COLLAPSETOEND_REASON)) {
   281       this._closeSelection();
   282       return;
   283     }
   285     // If selected text no longer exists, close
   286     if (!aSelection.toString()) {
   287       this._closeSelection();
   288     }
   289   },
   291   /*
   292    * Called from browser.js when the user long taps on text or chooses
   293    * the "Select Word" context menu item. Initializes SelectionHandler,
   294    * starts a selection, and positions the text selection handles.
   295    *
   296    * @param aOptions list of options describing how to start selection
   297    *                 Options include:
   298    *                   mode - SELECT_ALL to select everything in the target
   299    *                          element, or SELECT_AT_POINT to select a word.
   300    *                   x    - The x-coordinate for SELECT_AT_POINT.
   301    *                   y    - The y-coordinate for SELECT_AT_POINT.
   302    */
   303   startSelection: function sh_startSelection(aElement, aOptions = { mode: SelectionHandler.SELECT_ALL }) {
   304     // Clear out any existing active selection
   305     this._closeSelection();
   307     this._initTargetInfo(aElement, this.TYPE_SELECTION);
   309     // Clear any existing selection from the document
   310     this._contentWindow.getSelection().removeAllRanges();
   312     // Perform the appropriate selection method, if we can't determine method, or it fails, return
   313     if (!this._performSelection(aOptions)) {
   314       this._deactivate();
   315       return false;
   316     }
   318     // Double check results of successful selection operation
   319     let selection = this._getSelection();
   320     if (!selection || selection.rangeCount == 0 || selection.getRangeAt(0).collapsed) {
   321       this._deactivate();
   322       return false;
   323     }
   325     if (this._isPhoneNumber(selection.toString())) {
   326       let anchorNode = selection.anchorNode;
   327       let anchorOffset = selection.anchorOffset;
   328       let focusNode = null;
   329       let focusOffset = null;
   330       while (this._isPhoneNumber(selection.toString().trim())) {
   331         focusNode = selection.focusNode;
   332         focusOffset = selection.focusOffset;
   333         selection.modify("extend", "forward", "word");
   334         // if we hit the end of the text on the page, we can't advance the selection
   335         if (focusNode == selection.focusNode && focusOffset == selection.focusOffset) {
   336           break;
   337         }
   338       }
   340       // reverse selection
   341       selection.collapse(focusNode, focusOffset);
   342       selection.extend(anchorNode, anchorOffset);
   344       anchorNode = focusNode;
   345       anchorOffset = focusOffset
   347       while (this._isPhoneNumber(selection.toString().trim())) {
   348         focusNode = selection.focusNode;
   349         focusOffset = selection.focusOffset;
   350         selection.modify("extend", "backward", "word");
   351         // if we hit the end of the text on the page, we can't advance the selection
   352         if (focusNode == selection.focusNode && focusOffset == selection.focusOffset) {
   353           break;
   354         }
   355       }
   357       selection.collapse(focusNode, focusOffset);
   358       selection.extend(anchorNode, anchorOffset);
   359     }
   361     // Add a listener to end the selection if it's removed programatically
   362     selection.QueryInterface(Ci.nsISelectionPrivate).addSelectionListener(this);
   363     this._activeType = this.TYPE_SELECTION;
   365     // Initialize the cache
   366     this._cache = { start: {}, end: {}};
   367     this._updateCacheForSelection();
   369     let scroll = this._getScrollPos();
   370     // Figure out the distance between the selection and the click
   371     let positions = this._getHandlePositions(scroll);
   373     if (aOptions.mode == this.SELECT_AT_POINT && !this._selectionNearClick(scroll.X + aOptions.x,
   374                                                                       scroll.Y + aOptions.y,
   375                                                                       positions)) {
   376         this._closeSelection();
   377         return false;
   378     }
   380     // Determine position and show handles, open actionbar
   381     this._positionHandles(positions);
   382     sendMessageToJava({
   383       type: "TextSelection:ShowHandles",
   384       handles: [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]
   385     });
   386     this._updateMenu();
   387     return true;
   388   },
   390   /*
   391    * Called to perform a selection operation, given a target element, selection method, starting point etc.
   392    */
   393   _performSelection: function sh_performSelection(aOptions) {
   394     if (aOptions.mode == this.SELECT_AT_POINT) {
   395       return this._domWinUtils.selectAtPoint(aOptions.x, aOptions.y, Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE);
   396     }
   398     if (aOptions.mode != this.SELECT_ALL) {
   399       Cu.reportError("SelectionHandler.js: _performSelection() Invalid selection mode " + aOptions.mode);
   400       return false;
   401     }
   403     // HTMLPreElement is a #text node, SELECT_ALL implies entire paragraph
   404     if (this._targetElement instanceof HTMLPreElement)  {
   405       return this._domWinUtils.selectAtPoint(1, 1, Ci.nsIDOMWindowUtils.SELECT_PARAGRAPH);
   406     }
   408     // Else default to selectALL Document
   409     this._getSelectionController().selectAll();
   411     // Selection is entire HTMLHtmlElement, remove any trailing document whitespace
   412     let selection = this._getSelection();
   413     let lastNode = selection.focusNode;
   414     while (lastNode && lastNode.lastChild) {
   415       lastNode = lastNode.lastChild;
   416     }
   418     if (lastNode instanceof Text) {
   419       try {
   420         selection.extend(lastNode, lastNode.length);
   421       } catch (e) {
   422         Cu.reportError("SelectionHandler.js: _performSelection() whitespace trim fails: lastNode[" + lastNode +
   423           "] lastNode.length[" + lastNode.length + "]");
   424       }
   425     }
   427     return true;
   428   },
   430   /* Return true if the current selection (given by aPositions) is near to where the coordinates passed in */
   431   _selectionNearClick: function(aX, aY, aPositions) {
   432       let distance = 0;
   434       // Check if the click was in the bounding box of the selection handles
   435       if (aPositions[0].left < aX && aX < aPositions[1].left
   436           && aPositions[0].top < aY && aY < aPositions[1].top) {
   437         distance = 0;
   438       } else {
   439         // If it was outside, check the distance to the center of the selection
   440         let selectposX = (aPositions[0].left + aPositions[1].left) / 2;
   441         let selectposY = (aPositions[0].top + aPositions[1].top) / 2;
   443         let dx = Math.abs(selectposX - aX);
   444         let dy = Math.abs(selectposY - aY);
   445         distance = dx + dy;
   446       }
   448       let maxSelectionDistance = Services.prefs.getIntPref("browser.ui.selection.distance");
   449       return (distance < maxSelectionDistance);
   450   },
   452   /* Reads a value from an action. If the action defines the value as a function, will return the result of calling
   453      the function. Otherwise, will return the value itself. If the value isn't defined for this action, will return a default */
   454   _getValue: function(obj, name, defaultValue) {
   455     if (!(name in obj))
   456       return defaultValue;
   458     if (typeof obj[name] == "function")
   459       return obj[name](this._targetElement);
   461     return obj[name];
   462   },
   464   addAction: function(action) {
   465     if (!action.id)
   466       action.id = uuidgen.generateUUID().toString()
   468     if (this.actions[action.id])
   469       throw "Action with id " + action.id + " already added";
   471     // Update actions list and actionbar UI if active.
   472     this.actions[action.id] = action;
   473     this._updateMenu();
   474     return action.id;
   475   },
   477   removeAction: function(id) {
   478     // Update actions list and actionbar UI if active.
   479     delete this.actions[id];
   480     this._updateMenu();
   481   },
   483   _updateMenu: function() {
   484     if (this._activeType == this.TYPE_NONE) {
   485       return;
   486     }
   488     // Update actionbar UI.
   489     let actions = [];
   490     for (let type in this.actions) {
   491       let action = this.actions[type];
   492       if (action.selector.matches(this._targetElement)) {
   493         let a = {
   494           id: action.id,
   495           label: this._getValue(action, "label", ""),
   496           icon: this._getValue(action, "icon", "drawable://ic_status_logo"),
   497           showAsAction: this._getValue(action, "showAsAction", true),
   498           order: this._getValue(action, "order", 0)
   499         };
   500         actions.push(a);
   501       }
   502     }
   504     actions.sort((a, b) => b.order - a.order);
   506     sendMessageToJava({
   507       type: "TextSelection:Update",
   508       actions: actions
   509     });
   510   },
   512   /*
   513    * Actionbar methods.
   514    */
   515   actions: {
   516     SELECT_ALL: {
   517       label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
   518       id: "selectall_action",
   519       icon: "drawable://ab_select_all",
   520       action: function(aElement) {
   521         SelectionHandler.startSelection(aElement);
   522         UITelemetry.addEvent("action.1", "actionbar", null, "select_all");
   523       },
   524       order: 5,
   525       selector: {
   526         matches: function(aElement) {
   527           return (aElement.textLength != 0);
   528         }
   529       }
   530     },
   532     CUT: {
   533       label: Strings.browser.GetStringFromName("contextmenu.cut"),
   534       id: "cut_action",
   535       icon: "drawable://ab_cut",
   536       action: function(aElement) {
   537         let start = aElement.selectionStart;
   538         let end   = aElement.selectionEnd;
   540         SelectionHandler.copySelection();
   541         aElement.value = aElement.value.substring(0, start) + aElement.value.substring(end)
   543         // copySelection closes the selection. Show a caret where we just cut the text.
   544         SelectionHandler.attachCaret(aElement);
   545         UITelemetry.addEvent("action.1", "actionbar", null, "cut");
   546       },
   547       order: 4,
   548       selector: {
   549         matches: function(aElement) {
   550           return SelectionHandler.isElementEditableText(aElement) ?
   551             SelectionHandler.isSelectionActive() : false;
   552         }
   553       }
   554     },
   556     COPY: {
   557       label: Strings.browser.GetStringFromName("contextmenu.copy"),
   558       id: "copy_action",
   559       icon: "drawable://ab_copy",
   560       action: function() {
   561         SelectionHandler.copySelection();
   562         UITelemetry.addEvent("action.1", "actionbar", null, "copy");
   563       },
   564       order: 3,
   565       selector: {
   566         matches: function(aElement) {
   567           // Don't include "copy" for password fields.
   568           // mozIsTextField(true) tests for only non-password fields.
   569           if (aElement instanceof Ci.nsIDOMHTMLInputElement && !aElement.mozIsTextField(true)) {
   570             return false;
   571           }
   572           return SelectionHandler.isSelectionActive();
   573         }
   574       }
   575     },
   577     PASTE: {
   578       label: Strings.browser.GetStringFromName("contextmenu.paste"),
   579       id: "paste_action",
   580       icon: "drawable://ab_paste",
   581       action: function(aElement) {
   582         if (aElement && (aElement instanceof Ci.nsIDOMNSEditableElement)) {
   583           let target = aElement.QueryInterface(Ci.nsIDOMNSEditableElement);
   584           target.editor.paste(Ci.nsIClipboard.kGlobalClipboard);
   585           target.focus();
   586           SelectionHandler._closeSelection();
   587           UITelemetry.addEvent("action.1", "actionbar", null, "paste");
   588         }
   589       },
   590       order: 2,
   591       selector: {
   592         matches: function(aElement) {
   593           if (SelectionHandler.isElementEditableText(aElement)) {
   594             let flavors = ["text/unicode"];
   595             return Services.clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard);
   596           }
   597           return false;
   598         }
   599       }
   600     },
   602     SHARE: {
   603       label: Strings.browser.GetStringFromName("contextmenu.share"),
   604       id: "share_action",
   605       icon: "drawable://ic_menu_share",
   606       action: function() {
   607         SelectionHandler.shareSelection();
   608         UITelemetry.addEvent("action.1", "actionbar", null, "share");
   609       },
   610       selector: {
   611         matches: function() {
   612           return SelectionHandler.isSelectionActive();
   613         }
   614       }
   615     },
   617     SEARCH: {
   618       label: function() {
   619         return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
   620       },
   621       id: "search_action",
   622       icon: "drawable://ab_search",
   623       action: function() {
   624         SelectionHandler.searchSelection();
   625         SelectionHandler._closeSelection();
   626         UITelemetry.addEvent("action.1", "actionbar", null, "search");
   627       },
   628       order: 1,
   629       selector: {
   630         matches: function() {
   631           return SelectionHandler.isSelectionActive();
   632         }
   633       }
   634     },
   636     CALL: {
   637       label: Strings.browser.GetStringFromName("contextmenu.call"),
   638       id: "call_action",
   639       icon: "drawable://phone",
   640       action: function() {
   641         SelectionHandler.callSelection();
   642         UITelemetry.addEvent("action.1", "actionbar", null, "call");
   643       },
   644       order: 1,
   645       selector: {
   646         matches: function () {
   647           return SelectionHandler._getSelectedPhoneNumber() != null;
   648         }
   649       }
   650     }
   651   },
   653   /*
   654    * Called by BrowserEventHandler when the user taps in a form input.
   655    * Initializes SelectionHandler and positions the caret handle.
   656    *
   657    * @param aX, aY tap location in client coordinates.
   658    */
   659   attachCaret: function sh_attachCaret(aElement) {
   660     // Ensure it isn't disabled, isn't handled by Android native dialog, and is editable text element
   661     if (aElement.disabled || InputWidgetHelper.hasInputWidget(aElement) || !this.isElementEditableText(aElement)) {
   662       return;
   663     }
   665     this._initTargetInfo(aElement, this.TYPE_CURSOR);
   667     // Caret-specific observer/listeners
   668     Services.obs.addObserver(this, "TextSelection:UpdateCaretPos", false);
   669     BrowserApp.deck.addEventListener("keyup", this, false);
   670     BrowserApp.deck.addEventListener("compositionupdate", this, false);
   671     BrowserApp.deck.addEventListener("compositionend", this, false);
   673     this._activeType = this.TYPE_CURSOR;
   675     // Determine position and show caret, open actionbar
   676     this._positionHandles();
   677     sendMessageToJava({
   678       type: "TextSelection:ShowHandles",
   679       handles: [this.HANDLE_TYPE_MIDDLE]
   680     });
   681     this._updateMenu();
   682   },
   684   // Target initialization for both TYPE_CURSOR and TYPE_SELECTION
   685   _initTargetInfo: function sh_initTargetInfo(aElement, aSelectionType) {
   686     this._targetElement = aElement;
   687     if (aElement instanceof Ci.nsIDOMNSEditableElement) {
   688       if (aSelectionType === this.TYPE_SELECTION) {
   689         // Blur the targetElement to force IME code to undo previous style compositions
   690         // (visible underlines / etc generated by autoCorrection, autoSuggestion)
   691         aElement.blur();
   692       }
   693       // Ensure targetElement is now focused normally
   694       aElement.focus();
   695     }
   697     this._stopDraggingHandles();
   698     this._contentWindow = aElement.ownerDocument.defaultView;
   699     this._isRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");
   701     this._addObservers();
   702   },
   704   _getSelection: function sh_getSelection() {
   705     if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
   706       return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selection;
   707     else
   708       return this._contentWindow.getSelection();
   709   },
   711   _getSelectedText: function sh_getSelectedText() {
   712     if (!this._contentWindow)
   713       return "";
   715     let selection = this._getSelection();
   716     if (!selection)
   717       return "";
   719     if (this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
   720       return selection.QueryInterface(Ci.nsISelectionPrivate).
   721         toStringWithFormat("text/plain", Ci.nsIDocumentEncoder.OutputPreformatted | Ci.nsIDocumentEncoder.OutputRaw, 0);
   722     }
   724     return selection.toString().trim();
   725   },
   727   _getSelectionController: function sh_getSelectionController() {
   728     if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
   729       return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selectionController;
   730     else
   731       return this._contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
   732                                  getInterface(Ci.nsIWebNavigation).
   733                                  QueryInterface(Ci.nsIInterfaceRequestor).
   734                                  getInterface(Ci.nsISelectionDisplay).
   735                                  QueryInterface(Ci.nsISelectionController);
   736   },
   738   // Used by the contextmenu "matches" functions in ClipboardHelper
   739   isSelectionActive: function sh_isSelectionActive() {
   740     return (this._activeType == this.TYPE_SELECTION);
   741   },
   743   isElementEditableText: function (aElement) {
   744     return ((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
   745             (aElement instanceof HTMLTextAreaElement));
   746   },
   748   /*
   749    * Helper function for moving the selection inside an editable element.
   750    *
   751    * @param aAnchorX the stationary handle's x-coordinate in client coordinates
   752    * @param aX the moved handle's x-coordinate in client coordinates
   753    * @param aCaretPos the current position of the caret
   754    */
   755   _moveSelectionInEditable: function sh_moveSelectionInEditable(aAnchorX, aX, aCaretPos) {
   756     let anchorOffset = aX < aAnchorX ? this._targetElement.selectionEnd
   757                                      : this._targetElement.selectionStart;
   758     let newOffset = aCaretPos.offset;
   759     let [start, end] = anchorOffset <= newOffset ?
   760                        [anchorOffset, newOffset] :
   761                        [newOffset, anchorOffset];
   762     this._targetElement.setSelectionRange(start, end);
   763   },
   765   /*
   766    * Moves the selection as the user drags a selection handle.
   767    *
   768    * @param aIsStartHandle whether the user is moving the start handle (as opposed to the end handle)
   769    * @param aX, aY selection point in client coordinates
   770    */
   771   _moveSelection: function sh_moveSelection(aIsStartHandle, aX, aY) {
   772     // XXX We should be smarter about the coordinates we pass to caretPositionFromPoint, especially
   773     // in editable targets. We should factor out the logic that's currently in _sendMouseEvents.
   774     let viewOffset = this._getViewOffset();
   775     let caretPos = this._contentWindow.document.caretPositionFromPoint(aX - viewOffset.x, aY - viewOffset.y);
   776     if (!caretPos) {
   777       // User moves handle offscreen while positioning
   778       return;
   779     }
   781     // Constrain text selection within editable elements.
   782     let targetIsEditable = this._targetElement instanceof Ci.nsIDOMNSEditableElement;
   783     if (targetIsEditable && (caretPos.offsetNode != this._targetElement)) {
   784       return;
   785     }
   787     // Update the cache as the handle is dragged (keep the cache in client coordinates).
   788     if (aIsStartHandle) {
   789       this._cache.start.x = aX;
   790       this._cache.start.y = aY;
   791     } else {
   792       this._cache.end.x = aX;
   793       this._cache.end.y = aY;
   794     }
   796     let selection = this._getSelection();
   798     // The handles work the same on both LTR and RTL pages, but the anchor/focus nodes
   799     // are reversed, so we need to reverse the logic to extend the selection.
   800     if ((aIsStartHandle && !this._isRTL) || (!aIsStartHandle && this._isRTL)) {
   801       if (targetIsEditable) {
   802         let anchorX = this._isRTL ? this._cache.start.x : this._cache.end.x;
   803         this._moveSelectionInEditable(anchorX, aX, caretPos);
   804       } else {
   805         let focusNode = selection.focusNode;
   806         let focusOffset = selection.focusOffset;
   807         selection.collapse(caretPos.offsetNode, caretPos.offset);
   808         selection.extend(focusNode, focusOffset);
   809       }
   810     } else {
   811       if (targetIsEditable) {
   812         let anchorX = this._isRTL ? this._cache.end.x : this._cache.start.x;
   813         this._moveSelectionInEditable(anchorX, aX, caretPos);
   814       } else {
   815         selection.extend(caretPos.offsetNode, caretPos.offset);
   816       }
   817     }
   818   },
   820   _sendMouseEvents: function sh_sendMouseEvents(aX, aY, useShift) {
   821     // If we're positioning a cursor in an input field, make sure the handle
   822     // stays within the bounds of the field
   823     if (this._activeType == this.TYPE_CURSOR) {
   824       // Get rect of text inside element
   825       let range = document.createRange();
   826       range.selectNodeContents(this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.rootElement);
   827       let textBounds = range.getBoundingClientRect();
   829       // Get rect of editor
   830       let editorBounds = this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_EDITOR_RECT, 0, 0, 0, 0,
   831                                                                  this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
   832       // the return value from sendQueryContentEvent is in LayoutDevice pixels and we want CSS pixels, so
   833       // divide by the pixel ratio
   834       let editorRect = new Rect(editorBounds.left / window.devicePixelRatio,
   835                                 editorBounds.top / window.devicePixelRatio,
   836                                 editorBounds.width / window.devicePixelRatio,
   837                                 editorBounds.height / window.devicePixelRatio);
   839       // Use intersection of the text rect and the editor rect
   840       let rect = new Rect(textBounds.left, textBounds.top, textBounds.width, textBounds.height);
   841       rect.restrictTo(editorRect);
   843       // Clamp vertically and scroll if handle is at bounds. The top and bottom
   844       // must be restricted by an additional pixel since clicking on the top
   845       // edge of an input field moves the cursor to the beginning of that
   846       // field's text (and clicking the bottom moves the cursor to the end).
   847       if (aY < rect.y + 1) {
   848         aY = rect.y + 1;
   849         this._getSelectionController().scrollLine(false);
   850       } else if (aY > rect.y + rect.height - 1) {
   851         aY = rect.y + rect.height - 1;
   852         this._getSelectionController().scrollLine(true);
   853       }
   855       // Clamp horizontally and scroll if handle is at bounds
   856       if (aX < rect.x) {
   857         aX = rect.x;
   858         this._getSelectionController().scrollCharacter(false);
   859       } else if (aX > rect.x + rect.width) {
   860         aX = rect.x + rect.width;
   861         this._getSelectionController().scrollCharacter(true);
   862       }
   863     } else if (this._activeType == this.TYPE_SELECTION) {
   864       // Send mouse event 1px too high to prevent selection from entering the line below where it should be
   865       aY -= 1;
   866     }
   868     this._domWinUtils.sendMouseEventToWindow("mousedown", aX, aY, 0, 0, useShift ? Ci.nsIDOMNSEvent.SHIFT_MASK : 0, true);
   869     this._domWinUtils.sendMouseEventToWindow("mouseup", aX, aY, 0, 0, useShift ? Ci.nsIDOMNSEvent.SHIFT_MASK : 0, true);
   870   },
   872   copySelection: function sh_copySelection() {
   873     let selectedText = this._getSelectedText();
   874     if (selectedText.length) {
   875       let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
   876       clipboard.copyString(selectedText, this._contentWindow.document);
   877       NativeWindow.toast.show(Strings.browser.GetStringFromName("selectionHelper.textCopied"), "short");
   878     }
   879     this._closeSelection();
   880   },
   882   shareSelection: function sh_shareSelection() {
   883     let selectedText = this._getSelectedText();
   884     if (selectedText.length) {
   885       sendMessageToJava({
   886         type: "Share:Text",
   887         text: selectedText
   888       });
   889     }
   890     this._closeSelection();
   891   },
   893   searchSelection: function sh_searchSelection() {
   894     let selectedText = this._getSelectedText();
   895     if (selectedText.length) {
   896       let req = Services.search.defaultEngine.getSubmission(selectedText);
   897       let parent = BrowserApp.selectedTab;
   898       let isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
   899       // Set current tab as parent of new tab, and set new tab as private if the parent is.
   900       BrowserApp.addTab(req.uri.spec, {parentId: parent.id,
   901                                        selected: true,
   902                                        isPrivate: isPrivate});
   903     }
   904     this._closeSelection();
   905   },
   907   _phoneRegex: /^\+?[0-9\s,-.\(\)*#pw]{1,30}$/,
   909   _getSelectedPhoneNumber: function sh_getSelectedPhoneNumber() {
   910     return this._isPhoneNumber(this._getSelectedText().trim());
   911   },
   913   _isPhoneNumber: function sh_isPhoneNumber(selectedText) {
   914     return (this._phoneRegex.test(selectedText) ? selectedText : null);
   915   },
   917   callSelection: function sh_callSelection() {
   918     let selectedText = this._getSelectedPhoneNumber();
   919     if (selectedText) {
   920       BrowserApp.loadURI("tel:" + selectedText);
   921     }
   922     this._closeSelection();
   923   },
   925   /*
   926    * Shuts SelectionHandler down.
   927    */
   928   _closeSelection: function sh_closeSelection() {
   929     // Bail if there's no active selection
   930     if (this._activeType == this.TYPE_NONE)
   931       return;
   933     if (this._activeType == this.TYPE_SELECTION)
   934       this._clearSelection();
   936     this._deactivate();
   937   },
   939   _clearSelection: function sh_clearSelection() {
   940     let selection = this._getSelection();
   941     if (selection) {
   942       // Remove our listener before we clear the selection
   943       selection.QueryInterface(Ci.nsISelectionPrivate).removeSelectionListener(this);
   944       // Clear selection without clearing the anchorNode or focusNode
   945       if (selection.rangeCount != 0) {
   946         selection.collapseToStart();
   947       }
   948     }
   949   },
   951   _deactivate: function sh_deactivate() {
   952     this._stopDraggingHandles();
   953     // Hide handle/caret, close actionbar
   954     sendMessageToJava({ type: "TextSelection:HideHandles" });
   956     this._removeObservers();
   958     // Only observed for caret positioning
   959     if (this._activeType == this.TYPE_CURSOR) {
   960       Services.obs.removeObserver(this, "TextSelection:UpdateCaretPos");
   961       BrowserApp.deck.removeEventListener("keyup", this);
   962       BrowserApp.deck.removeEventListener("compositionupdate", this);
   963       BrowserApp.deck.removeEventListener("compositionend", this);
   964     }
   966     this._contentWindow = null;
   967     this._targetElement = null;
   968     this._isRTL = false;
   969     this._cache = null;
   970     this._ignoreCompositionChanges = false;
   971     this._prevHandlePositions = [];
   972     this._prevTargetElementHasText = null;
   974     this._activeType = this.TYPE_NONE;
   975   },
   977   _getViewOffset: function sh_getViewOffset() {
   978     let offset = { x: 0, y: 0 };
   979     let win = this._contentWindow;
   981     // Recursively look through frames to compute the total position offset.
   982     while (win.frameElement) {
   983       let rect = win.frameElement.getBoundingClientRect();
   984       offset.x += rect.left;
   985       offset.y += rect.top;
   987       win = win.parent;
   988     }
   990     return offset;
   991   },
   993   _pointInSelection: function sh_pointInSelection(aX, aY) {
   994     let offset = this._getViewOffset();
   995     let rangeRect = this._getSelection().getRangeAt(0).getBoundingClientRect();
   996     let radius = ElementTouchHelper.getTouchRadius();
   997     return (aX - offset.x > rangeRect.left - radius.left &&
   998             aX - offset.x < rangeRect.right + radius.right &&
   999             aY - offset.y > rangeRect.top - radius.top &&
  1000             aY - offset.y < rangeRect.bottom + radius.bottom);
  1001   },
  1003   // Returns true if the selection has been reversed. Takes optional aIsStartHandle
  1004   // param to decide whether the selection has been reversed.
  1005   _updateCacheForSelection: function sh_updateCacheForSelection(aIsStartHandle) {
  1006     let rects = this._getSelection().getRangeAt(0).getClientRects();
  1007     if (!rects[0]) {
  1008       // nsISelection object exists, but there's nothing actually selected
  1009       throw "Failed to update cache for invalid selection";
  1012     let start = { x: this._isRTL ? rects[0].right : rects[0].left, y: rects[0].bottom };
  1013     let end = { x: this._isRTL ? rects[rects.length - 1].left : rects[rects.length - 1].right, y: rects[rects.length - 1].bottom };
  1015     let selectionReversed = false;
  1016     if (this._cache.start) {
  1017       // 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)
  1018       selectionReversed = (aIsStartHandle && (end.y > this._cache.end.y || (end.y == this._cache.end.y && end.x > this._cache.end.x))) ||
  1019                           (!aIsStartHandle && (start.y < this._cache.start.y || (start.y == this._cache.start.y && start.x < this._cache.start.x)));
  1022     this._cache.start = start;
  1023     this._cache.end = end;
  1025     return selectionReversed;
  1026   },
  1028   _getHandlePositions: function sh_getHandlePositions(scroll) {
  1029     // the checkHidden function tests to see if the given point is hidden inside an
  1030     // iframe/subdocument. this is so that if we select some text inside an iframe and
  1031     // scroll the iframe so the selection is out of view, we hide the handles rather
  1032     // than having them float on top of the main page content.
  1033     let checkHidden = function(x, y) {
  1034       return false;
  1035     };
  1036     if (this._contentWindow.frameElement) {
  1037       let bounds = this._contentWindow.frameElement.getBoundingClientRect();
  1038       checkHidden = function(x, y) {
  1039         return x < 0 || y < 0 || x > bounds.width || y > bounds.height;
  1040       };
  1043     let positions = null;
  1044     if (this._activeType == this.TYPE_CURSOR) {
  1045       // The left and top properties returned are relative to the client area
  1046       // of the window, so we don't need to account for a sub-frame offset.
  1047       let cursor = this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_CARET_RECT, this._targetElement.selectionEnd, 0, 0, 0,
  1048                                                            this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
  1049       // the return value from sendQueryContentEvent is in LayoutDevice pixels and we want CSS pixels, so
  1050       // divide by the pixel ratio
  1051       let x = cursor.left / window.devicePixelRatio;
  1052       let y = (cursor.top + cursor.height) / window.devicePixelRatio;
  1053       return [{ handle: this.HANDLE_TYPE_MIDDLE,
  1054                 left: x + scroll.X,
  1055                 top: y + scroll.Y,
  1056                 hidden: checkHidden(x, y) }];
  1057     } else {
  1058       let sx = this._cache.start.x;
  1059       let sy = this._cache.start.y;
  1060       let ex = this._cache.end.x;
  1061       let ey = this._cache.end.y;
  1063       // Translate coordinates to account for selections in sub-frames. We can't cache
  1064       // this because the top-level page may have scrolled since selection started.
  1065       let offset = this._getViewOffset();
  1067       return  [{ handle: this.HANDLE_TYPE_START,
  1068                  left: sx + offset.x + scroll.X,
  1069                  top: sy + offset.y + scroll.Y,
  1070                  hidden: checkHidden(sx, sy) },
  1071                { handle: this.HANDLE_TYPE_END,
  1072                  left: ex + offset.x + scroll.X,
  1073                  top: ey + offset.y + scroll.Y,
  1074                  hidden: checkHidden(ex, ey) }];
  1076   },
  1078   // Position handles, but avoid superfluous re-positioning (helps during
  1079   // "TextSelection:LayerReflow", "scroll" of top-level document, etc).
  1080   _positionHandlesOnChange: function() {
  1081     // Helper function to compare position messages
  1082     let samePositions = function(aPrev, aCurr) {
  1083       if (aPrev.length != aCurr.length) {
  1084         return false;
  1086       for (let i = 0; i < aPrev.length; i++) {
  1087         if (aPrev[i].left != aCurr[i].left ||
  1088             aPrev[i].top != aCurr[i].top ||
  1089             aPrev[i].hidden != aCurr[i].hidden) {
  1090           return false;
  1093       return true;
  1096     let positions = this._getHandlePositions(this._getScrollPos());
  1097     if (!samePositions(this._prevHandlePositions, positions)) {
  1098       this._positionHandles(positions);
  1100   },
  1102   // Position handles, allow for re-position, in case user drags handle
  1103   // to invalid position, then releases, we can put it back where it started
  1104   // positions is an array of objects with data about handle positions,
  1105   // which we get from _getHandlePositions.
  1106   _positionHandles: function sh_positionHandles(positions) {
  1107     if (!positions) {
  1108       positions = this._getHandlePositions(this._getScrollPos());
  1110     sendMessageToJava({
  1111       type: "TextSelection:PositionHandles",
  1112       positions: positions,
  1113       rtl: this._isRTL
  1114     });
  1115     this._prevHandlePositions = positions;
  1117     // Text state transitions (text <--> no text) will affect selection context and actionbar display
  1118     let currTargetElementHasText = (this._targetElement.textLength > 0);
  1119     if (currTargetElementHasText != this._prevTargetElementHasText) {
  1120       this._prevTargetElementHasText = currTargetElementHasText;
  1121       this._updateMenu();
  1123   },
  1125   subdocumentScrolled: function sh_subdocumentScrolled(aElement) {
  1126     if (this._activeType == this.TYPE_NONE) {
  1127       return;
  1129     let scrollView = aElement.ownerDocument.defaultView;
  1130     let view = this._contentWindow;
  1131     while (true) {
  1132       if (view == scrollView) {
  1133         // The selection is in a view (or sub-view) of the view that scrolled.
  1134         // So we need to reposition the handles.
  1135         if (this._activeType == this.TYPE_SELECTION) {
  1136           this._updateCacheForSelection();
  1138         this._positionHandles();
  1139         break;
  1141       if (view == view.parent) {
  1142         break;
  1144       view = view.parent;
  1147 };

mercurial