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 +};