|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 let Ci = Components.interfaces; |
|
6 let Cc = Components.classes; |
|
7 |
|
8 dump("### SelectionHandler.js loaded\n"); |
|
9 |
|
10 var SelectionHandler = { |
|
11 init: function init() { |
|
12 this.type = kContentSelector; |
|
13 this.snap = true; |
|
14 this.lastYPos = this.lastXPos = null; |
|
15 addMessageListener("Browser:SelectionStart", this); |
|
16 addMessageListener("Browser:SelectionAttach", this); |
|
17 addMessageListener("Browser:SelectionEnd", this); |
|
18 addMessageListener("Browser:SelectionMoveStart", this); |
|
19 addMessageListener("Browser:SelectionMove", this); |
|
20 addMessageListener("Browser:SelectionMoveEnd", this); |
|
21 addMessageListener("Browser:SelectionUpdate", this); |
|
22 addMessageListener("Browser:SelectionClose", this); |
|
23 addMessageListener("Browser:SelectionCopy", this); |
|
24 addMessageListener("Browser:SelectionDebug", this); |
|
25 addMessageListener("Browser:CaretAttach", this); |
|
26 addMessageListener("Browser:CaretMove", this); |
|
27 addMessageListener("Browser:CaretUpdate", this); |
|
28 addMessageListener("Browser:SelectionSwitchMode", this); |
|
29 addMessageListener("Browser:RepositionInfoRequest", this); |
|
30 addMessageListener("Browser:SelectionHandlerPing", this); |
|
31 addMessageListener("Browser:ResetLastPos", this); |
|
32 }, |
|
33 |
|
34 shutdown: function shutdown() { |
|
35 removeMessageListener("Browser:SelectionStart", this); |
|
36 removeMessageListener("Browser:SelectionAttach", this); |
|
37 removeMessageListener("Browser:SelectionEnd", this); |
|
38 removeMessageListener("Browser:SelectionMoveStart", this); |
|
39 removeMessageListener("Browser:SelectionMove", this); |
|
40 removeMessageListener("Browser:SelectionMoveEnd", this); |
|
41 removeMessageListener("Browser:SelectionUpdate", this); |
|
42 removeMessageListener("Browser:SelectionClose", this); |
|
43 removeMessageListener("Browser:SelectionCopy", this); |
|
44 removeMessageListener("Browser:SelectionDebug", this); |
|
45 removeMessageListener("Browser:CaretAttach", this); |
|
46 removeMessageListener("Browser:CaretMove", this); |
|
47 removeMessageListener("Browser:CaretUpdate", this); |
|
48 removeMessageListener("Browser:SelectionSwitchMode", this); |
|
49 removeMessageListener("Browser:RepositionInfoRequest", this); |
|
50 removeMessageListener("Browser:SelectionHandlerPing", this); |
|
51 removeMessageListener("Browser:ResetLastPos", this); |
|
52 }, |
|
53 |
|
54 sendAsync: function sendAsync(aMsg, aJson) { |
|
55 sendAsyncMessage(aMsg, aJson); |
|
56 }, |
|
57 |
|
58 /************************************************* |
|
59 * Browser event handlers |
|
60 */ |
|
61 |
|
62 /* |
|
63 * Selection start event handler |
|
64 */ |
|
65 _onSelectionStart: function _onSelectionStart(aJson) { |
|
66 // Init content window information |
|
67 if (!this._initTargetInfo(aJson.xPos, aJson.yPos)) { |
|
68 this._onFail("failed to get target information"); |
|
69 return; |
|
70 } |
|
71 |
|
72 // for context menu select command, which doesn't trigger |
|
73 // form input focus changes. |
|
74 if (aJson.setFocus && this._targetIsEditable) { |
|
75 this._targetElement.focus(); |
|
76 } |
|
77 |
|
78 // Clear any existing selection from the document |
|
79 let selection = this._contentWindow.getSelection(); |
|
80 selection.removeAllRanges(); |
|
81 |
|
82 // Set our initial selection, aX and aY should be in client coordinates. |
|
83 let framePoint = this._clientPointToFramePoint({ xPos: aJson.xPos, yPos: aJson.yPos }); |
|
84 if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos, |
|
85 Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) { |
|
86 this._onFail("failed to set selection at point"); |
|
87 return; |
|
88 } |
|
89 |
|
90 // Update the position of our selection monocles |
|
91 this._updateSelectionUI("start", true, true); |
|
92 }, |
|
93 |
|
94 _onSelectionAttach: function _onSelectionAttach(aX, aY) { |
|
95 // Init content window information |
|
96 if (!this._initTargetInfo(aX, aY)) { |
|
97 this._onFail("failed to get frame offset"); |
|
98 return; |
|
99 } |
|
100 |
|
101 // Update the position of our selection monocles |
|
102 this._updateSelectionUI("start", true, true); |
|
103 }, |
|
104 |
|
105 /* |
|
106 * Switch selection modes. Currently we only support switching |
|
107 * from "caret" to "selection". |
|
108 */ |
|
109 _onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) { |
|
110 if (aMode != "selection") { |
|
111 this._onFail("unsupported mode switch"); |
|
112 return; |
|
113 } |
|
114 |
|
115 // Sanity check to be sure we are initialized |
|
116 if (!this._targetElement) { |
|
117 this._onFail("not initialized"); |
|
118 return; |
|
119 } |
|
120 |
|
121 // Only use selectAtPoint for editable content and avoid that for inputs, |
|
122 // as we can expand caret to selection manually more precisely. We can use |
|
123 // selectAtPoint for inputs too though, but only once bug 881938 is fully |
|
124 // resolved. |
|
125 if(Util.isEditableContent(this._targetElement)) { |
|
126 // Similar to _onSelectionStart - we need to create initial selection |
|
127 // but without the initialization bits. |
|
128 let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY }); |
|
129 if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos, |
|
130 Ci.nsIDOMWindowUtils.SELECT_CHARACTER)) { |
|
131 this._onFail("failed to set selection at point"); |
|
132 return; |
|
133 } |
|
134 } else if (this._targetElement.selectionStart == 0 || aMarker == "end") { |
|
135 // Expand caret forward or backward depending on direction |
|
136 this._targetElement.selectionEnd++; |
|
137 } else { |
|
138 this._targetElement.selectionStart--; |
|
139 } |
|
140 |
|
141 // We bail if things get out of sync here implying we missed a message. |
|
142 this._selectionMoveActive = true; |
|
143 |
|
144 // Update the position of the selection marker that is *not* |
|
145 // being dragged. |
|
146 this._updateSelectionUI("update", aMarker == "end", aMarker == "start"); |
|
147 }, |
|
148 |
|
149 /* |
|
150 * Selection monocle start move event handler |
|
151 */ |
|
152 _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) { |
|
153 if (!this._contentWindow) { |
|
154 this._onFail("_onSelectionMoveStart was called without proper view set up"); |
|
155 return; |
|
156 } |
|
157 |
|
158 if (this._selectionMoveActive) { |
|
159 this._onFail("mouse is already down on drag start?"); |
|
160 return; |
|
161 } |
|
162 |
|
163 // We bail if things get out of sync here implying we missed a message. |
|
164 this._selectionMoveActive = true; |
|
165 |
|
166 if (this._targetIsEditable) { |
|
167 // If we're coming out of an out-of-bounds scroll, the node the user is |
|
168 // trying to drag may be hidden (the monocle will be pegged to the edge |
|
169 // of the edit). Make sure the node the user wants to move is visible |
|
170 // and has focus. |
|
171 this._updateInputFocus(aMsg.change); |
|
172 } |
|
173 |
|
174 // Update the position of our selection monocles |
|
175 this._updateSelectionUI("update", true, true); |
|
176 }, |
|
177 |
|
178 /* |
|
179 * Selection monocle move event handler |
|
180 */ |
|
181 _onSelectionMove: function _onSelectionMove(aMsg) { |
|
182 if (!this._contentWindow) { |
|
183 this._onFail("_onSelectionMove was called without proper view set up"); |
|
184 return; |
|
185 } |
|
186 |
|
187 if (!this._selectionMoveActive) { |
|
188 this._onFail("mouse isn't down for drag move?"); |
|
189 return; |
|
190 } |
|
191 |
|
192 this._handleSelectionPoint(aMsg, false); |
|
193 }, |
|
194 |
|
195 /* |
|
196 * Selection monocle move finished event handler |
|
197 */ |
|
198 _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) { |
|
199 if (!this._contentWindow) { |
|
200 this._onFail("_onSelectionMove was called without proper view set up"); |
|
201 return; |
|
202 } |
|
203 |
|
204 if (!this._selectionMoveActive) { |
|
205 this._onFail("mouse isn't down for drag move?"); |
|
206 return; |
|
207 } |
|
208 |
|
209 this._handleSelectionPoint(aMsg, true); |
|
210 this._selectionMoveActive = false; |
|
211 |
|
212 // _handleSelectionPoint may set a scroll timer, so this must |
|
213 // be reset after the last call. |
|
214 this._clearTimers(); |
|
215 |
|
216 // Update the position of our selection monocles |
|
217 this._updateSelectionUI("end", true, true); |
|
218 }, |
|
219 |
|
220 /* |
|
221 * _onCaretAttach - called by SelectionHelperUI when the user taps in a |
|
222 * form input. Initializes SelectionHandler, updates the location of the |
|
223 * caret, and messages back with current monocle position information. |
|
224 * |
|
225 * @param aX, aY tap location in client coordinates. |
|
226 */ |
|
227 _onCaretAttach: function _onCaretAttach(aX, aY) { |
|
228 // Init content window information |
|
229 if (!this._initTargetInfo(aX, aY)) { |
|
230 this._onFail("failed to get target information"); |
|
231 return; |
|
232 } |
|
233 |
|
234 // This should never happen, but we check to make sure |
|
235 if (!this._targetIsEditable) { |
|
236 this._onFail("Coordiates didn't find a text input element."); |
|
237 return; |
|
238 } |
|
239 |
|
240 // Locate and sanity check the caret position |
|
241 let selection = this._getSelection(); |
|
242 if (!selection || !selection.isCollapsed) { |
|
243 this._onFail("No selection or selection is not collapsed."); |
|
244 return; |
|
245 } |
|
246 |
|
247 // Update the position of our selection monocles |
|
248 this._updateSelectionUI("caret", false, false, true); |
|
249 }, |
|
250 |
|
251 /* |
|
252 * Selection copy event handler |
|
253 * |
|
254 * Check to see if the incoming click was on our selection rect. |
|
255 * if it was, copy to the clipboard. Incoming coordinates are |
|
256 * content values. |
|
257 */ |
|
258 _onSelectionCopy: function _onSelectionCopy(aMsg) { |
|
259 let tap = { |
|
260 xPos: aMsg.xPos, |
|
261 yPos: aMsg.yPos, |
|
262 }; |
|
263 |
|
264 let tapInSelection = (tap.xPos > this._cache.selection.left && |
|
265 tap.xPos < this._cache.selection.right) && |
|
266 (tap.yPos > this._cache.selection.top && |
|
267 tap.yPos < this._cache.selection.bottom); |
|
268 // Util.dumpLn(tapInSelection, |
|
269 // tap.xPos, tap.yPos, "|", this._cache.selection.left, |
|
270 // this._cache.selection.right, this._cache.selection.top, |
|
271 // this._cache.selection.bottom); |
|
272 let success = false; |
|
273 let selectedText = this._getSelectedText(); |
|
274 if (tapInSelection && selectedText.length) { |
|
275 let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] |
|
276 .getService(Ci.nsIClipboardHelper); |
|
277 clipboard.copyString(selectedText, this._contentWindow.document); |
|
278 success = true; |
|
279 } |
|
280 sendSyncMessage("Content:SelectionCopied", { succeeded: success }); |
|
281 }, |
|
282 |
|
283 /* |
|
284 * Selection close event handler |
|
285 * |
|
286 * @param aClearSelection requests that selection be cleared. |
|
287 */ |
|
288 _onSelectionClose: function _onSelectionClose(aClearSelection) { |
|
289 if (aClearSelection) { |
|
290 this._clearSelection(); |
|
291 } |
|
292 this.closeSelection(); |
|
293 }, |
|
294 |
|
295 /* |
|
296 * Called any time SelectionHelperUI would like us to |
|
297 * recalculate the selection bounds. |
|
298 */ |
|
299 _onSelectionUpdate: function _onSelectionUpdate(aMsg) { |
|
300 if (!this._contentWindow) { |
|
301 this._onFail("_onSelectionUpdate was called without proper view set up"); |
|
302 return; |
|
303 } |
|
304 |
|
305 if (aMsg && aMsg.isInitiatedByAPZC) { |
|
306 let {offset: offset} = Content.getCurrentWindowAndOffset( |
|
307 this._targetCoordinates.x, this._targetCoordinates.y); |
|
308 this._contentOffset = offset; |
|
309 } |
|
310 |
|
311 // Update the position of our selection monocles |
|
312 this._updateSelectionUI("update", true, true); |
|
313 }, |
|
314 |
|
315 /* |
|
316 * Called if for any reason we fail during the selection |
|
317 * process. Cancels the selection. |
|
318 */ |
|
319 _onFail: function _onFail(aDbgMessage) { |
|
320 if (aDbgMessage && aDbgMessage.length > 0) |
|
321 Util.dumpLn(aDbgMessage); |
|
322 this.sendAsync("Content:SelectionFail"); |
|
323 this._clearSelection(); |
|
324 this.closeSelection(); |
|
325 }, |
|
326 |
|
327 /* |
|
328 * _repositionInfoRequest - fired at us by ContentAreaObserver when the |
|
329 * soft keyboard is being displayed. CAO wants to make a decision about |
|
330 * whether the browser deck needs repositioning. |
|
331 */ |
|
332 _repositionInfoRequest: function _repositionInfoRequest(aJsonMsg) { |
|
333 let result = this._calcNewContentPosition(aJsonMsg.viewHeight); |
|
334 |
|
335 // no repositioning needed |
|
336 if (result == 0) { |
|
337 this.sendAsync("Content:RepositionInfoResponse", { reposition: false }); |
|
338 return; |
|
339 } |
|
340 |
|
341 this.sendAsync("Content:RepositionInfoResponse", { |
|
342 reposition: true, |
|
343 raiseContent: result, |
|
344 }); |
|
345 }, |
|
346 |
|
347 _onPing: function _onPing(aId) { |
|
348 this.sendAsync("Content:SelectionHandlerPong", { id: aId }); |
|
349 }, |
|
350 |
|
351 onClickCoords: function (xPos, yPos) { |
|
352 this.lastXPos = xPos; |
|
353 this.lastYPos = yPos; |
|
354 }, |
|
355 |
|
356 /************************************************* |
|
357 * Selection helpers |
|
358 */ |
|
359 |
|
360 /* |
|
361 * _clearSelection |
|
362 * |
|
363 * Clear existing selection if it exists and reset our internla state. |
|
364 */ |
|
365 _clearSelection: function _clearSelection() { |
|
366 this._clearTimers(); |
|
367 if (this._contentWindow) { |
|
368 let selection = this._getSelection(); |
|
369 if (selection) |
|
370 selection.removeAllRanges(); |
|
371 } else { |
|
372 let selection = content.getSelection(); |
|
373 if (selection) |
|
374 selection.removeAllRanges(); |
|
375 } |
|
376 }, |
|
377 |
|
378 /* |
|
379 * closeSelection |
|
380 * |
|
381 * Shuts SelectionHandler down. |
|
382 */ |
|
383 closeSelection: function closeSelection() { |
|
384 this._clearTimers(); |
|
385 this._cache = null; |
|
386 this._contentWindow = null; |
|
387 this._targetElement = null; |
|
388 this._selectionMoveActive = false; |
|
389 this._contentOffset = null; |
|
390 this._domWinUtils = null; |
|
391 this._targetIsEditable = false; |
|
392 this._targetCoordinates = null; |
|
393 sendSyncMessage("Content:HandlerShutdown", {}); |
|
394 }, |
|
395 |
|
396 /* |
|
397 * Find content within frames - cache the target nsIDOMWindow, |
|
398 * client coordinate offset, target element, and dom utils interface. |
|
399 */ |
|
400 _initTargetInfo: function _initTargetInfo(aX, aY) { |
|
401 // getCurrentWindowAndOffset takes client coordinates |
|
402 let { element: element, |
|
403 contentWindow: contentWindow, |
|
404 offset: offset, |
|
405 utils: utils } = |
|
406 Content.getCurrentWindowAndOffset(aX, aY); |
|
407 if (!contentWindow) { |
|
408 return false; |
|
409 } |
|
410 this._targetElement = element; |
|
411 this._contentWindow = contentWindow; |
|
412 this._contentOffset = offset; |
|
413 this._domWinUtils = utils; |
|
414 this._targetIsEditable = Util.isEditable(this._targetElement); |
|
415 this._targetCoordinates = { |
|
416 x: aX, |
|
417 y: aY |
|
418 }; |
|
419 |
|
420 return true; |
|
421 }, |
|
422 |
|
423 /* |
|
424 * _calcNewContentPosition - calculates the distance the browser should be |
|
425 * raised to move the focused form input out of the way of the soft |
|
426 * keyboard. |
|
427 * |
|
428 * @param aNewViewHeight the new content view height |
|
429 * @return 0 if no positioning is required or a positive val equal to the |
|
430 * distance content should be raised to center the target element. |
|
431 */ |
|
432 _calcNewContentPosition: function _calcNewContentPosition(aNewViewHeight) { |
|
433 // We have no target element but the keyboard is up |
|
434 // so lets not cover content that is below the keyboard |
|
435 if (!this._cache || !this._cache.element) { |
|
436 if (this.lastYPos != null && this.lastYPos > aNewViewHeight) { |
|
437 return Services.metro.keyboardHeight; |
|
438 } |
|
439 return 0; |
|
440 } |
|
441 |
|
442 let position = Util.centerElementInView(aNewViewHeight, this._cache.element); |
|
443 if (position !== undefined) { |
|
444 return position; |
|
445 } |
|
446 |
|
447 // Special case: we are dealing with an input that is taller than the |
|
448 // desired height of content. We need to center on the caret location. |
|
449 let rect = |
|
450 this._domWinUtils.sendQueryContentEvent( |
|
451 this._domWinUtils.QUERY_CARET_RECT, |
|
452 this._targetElement.selectionEnd, |
|
453 0, 0, 0, |
|
454 this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK); |
|
455 if (!rect || !rect.succeeded) { |
|
456 Util.dumpLn("no caret was present, unexpected."); |
|
457 return 0; |
|
458 } |
|
459 |
|
460 // Note sendQueryContentEvent with QUERY_CARET_RECT is really buggy. If it |
|
461 // can't find the exact location of the caret position it will "guess". |
|
462 // Sometimes this can put the result in unexpected locations. |
|
463 let caretLocation = Math.max(Math.min(Math.round(rect.top + (rect.height * .5)), |
|
464 viewBottom), 0); |
|
465 |
|
466 // Caret is above the bottom of the new view bounds, no need to shift. |
|
467 if (caretLocation <= aNewViewHeight) { |
|
468 return 0; |
|
469 } |
|
470 |
|
471 // distance from the top of the keyboard down to the caret location |
|
472 return caretLocation - aNewViewHeight; |
|
473 }, |
|
474 |
|
475 /************************************************* |
|
476 * Events |
|
477 */ |
|
478 |
|
479 /* |
|
480 * Scroll + selection advancement timer when the monocle is |
|
481 * outside the bounds of an input control. |
|
482 */ |
|
483 scrollTimerCallback: function scrollTimerCallback() { |
|
484 let result = SelectionHandler.updateTextEditSelection(); |
|
485 // Update monocle position and speed if we've dragged off to one side |
|
486 if (result.trigger) { |
|
487 SelectionHandler._updateSelectionUI("update", result.start, result.end); |
|
488 } |
|
489 }, |
|
490 |
|
491 receiveMessage: function sh_receiveMessage(aMessage) { |
|
492 if (this._debugEvents && aMessage.name != "Browser:SelectionMove") { |
|
493 Util.dumpLn("SelectionHandler:", aMessage.name); |
|
494 } |
|
495 let json = aMessage.json; |
|
496 switch (aMessage.name) { |
|
497 case "Browser:SelectionStart": |
|
498 this._onSelectionStart(json); |
|
499 break; |
|
500 |
|
501 case "Browser:SelectionAttach": |
|
502 this._onSelectionAttach(json.xPos, json.yPos); |
|
503 break; |
|
504 |
|
505 case "Browser:CaretAttach": |
|
506 this._onCaretAttach(json.xPos, json.yPos); |
|
507 break; |
|
508 |
|
509 case "Browser:CaretMove": |
|
510 this._onCaretMove(json.caret.xPos, json.caret.yPos); |
|
511 break; |
|
512 |
|
513 case "Browser:CaretUpdate": |
|
514 this._onCaretPositionUpdate(json.caret.xPos, json.caret.yPos); |
|
515 break; |
|
516 |
|
517 case "Browser:SelectionSwitchMode": |
|
518 this._onSwitchMode(json.newMode, json.change, json.xPos, json.yPos); |
|
519 break; |
|
520 |
|
521 case "Browser:SelectionClose": |
|
522 this._onSelectionClose(json.clearSelection); |
|
523 break; |
|
524 |
|
525 case "Browser:SelectionMoveStart": |
|
526 this._onSelectionMoveStart(json); |
|
527 break; |
|
528 |
|
529 case "Browser:SelectionMove": |
|
530 this._onSelectionMove(json); |
|
531 break; |
|
532 |
|
533 case "Browser:SelectionMoveEnd": |
|
534 this._onSelectionMoveEnd(json); |
|
535 break; |
|
536 |
|
537 case "Browser:SelectionCopy": |
|
538 this._onSelectionCopy(json); |
|
539 break; |
|
540 |
|
541 case "Browser:SelectionDebug": |
|
542 this._onSelectionDebug(json); |
|
543 break; |
|
544 |
|
545 case "Browser:SelectionUpdate": |
|
546 this._onSelectionUpdate(json); |
|
547 break; |
|
548 |
|
549 case "Browser:RepositionInfoRequest": |
|
550 // This message is sent simultaneously with a tap event. |
|
551 // Wait a bit to make sure we have the most up-to-date tap co-ordinates |
|
552 // before a call to _calcNewContentPosition() which accesses them. |
|
553 content.setTimeout (function () { |
|
554 SelectionHandler._repositionInfoRequest(json); |
|
555 }, 50); |
|
556 break; |
|
557 |
|
558 case "Browser:SelectionHandlerPing": |
|
559 this._onPing(json.id); |
|
560 break; |
|
561 |
|
562 case "Browser:ResetLastPos": |
|
563 this.onClickCoords(json.xPos, json.yPos); |
|
564 break; |
|
565 } |
|
566 }, |
|
567 |
|
568 /************************************************* |
|
569 * Utilities |
|
570 */ |
|
571 |
|
572 _getDocShell: function _getDocShell(aWindow) { |
|
573 if (aWindow == null) |
|
574 return null; |
|
575 return aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
576 .getInterface(Ci.nsIWebNavigation) |
|
577 .QueryInterface(Ci.nsIDocShell); |
|
578 }, |
|
579 |
|
580 _getSelectedText: function _getSelectedText() { |
|
581 let selection = this._getSelection(); |
|
582 if (selection) |
|
583 return selection.toString(); |
|
584 return ""; |
|
585 }, |
|
586 |
|
587 _getSelection: function _getSelection() { |
|
588 if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) { |
|
589 return this._targetElement |
|
590 .QueryInterface(Ci.nsIDOMNSEditableElement) |
|
591 .editor.selection; |
|
592 } else if (this._contentWindow) |
|
593 return this._contentWindow.getSelection(); |
|
594 return null; |
|
595 }, |
|
596 |
|
597 _getSelectController: function _getSelectController() { |
|
598 if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) { |
|
599 return this._targetElement |
|
600 .QueryInterface(Ci.nsIDOMNSEditableElement) |
|
601 .editor.selectionController; |
|
602 } else { |
|
603 let docShell = this._getDocShell(this._contentWindow); |
|
604 if (docShell == null) |
|
605 return null; |
|
606 return docShell.QueryInterface(Ci.nsIInterfaceRequestor) |
|
607 .getInterface(Ci.nsISelectionDisplay) |
|
608 .QueryInterface(Ci.nsISelectionController); |
|
609 } |
|
610 }, |
|
611 }; |
|
612 this.SelectionHandler = SelectionHandler; |
|
613 |
|
614 SelectionHandler.__proto__ = new SelectionPrototype(); |
|
615 SelectionHandler.init(); |
|
616 |