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

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

mercurial