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