|
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 /* |
|
6 * Selection handler for chrome text inputs |
|
7 */ |
|
8 |
|
9 let Ci = Components.interfaces; |
|
10 |
|
11 var ChromeSelectionHandler = { |
|
12 _mode: this._SELECTION_MODE, |
|
13 |
|
14 /************************************************* |
|
15 * Messaging wrapper |
|
16 */ |
|
17 |
|
18 sendAsync: function sendAsync(aMsg, aJson) { |
|
19 SelectionHelperUI.receiveMessage({ |
|
20 name: aMsg, |
|
21 json: aJson |
|
22 }); |
|
23 }, |
|
24 |
|
25 /************************************************* |
|
26 * Browser event handlers |
|
27 */ |
|
28 |
|
29 /* |
|
30 * General selection start method for both caret and selection mode. |
|
31 */ |
|
32 _onSelectionAttach: function _onSelectionAttach(aJson) { |
|
33 // Clear previous ChromeSelectionHandler state. |
|
34 this._deactivate(); |
|
35 |
|
36 // Initialize ChromeSelectionHandler state. |
|
37 this._domWinUtils = Util.getWindowUtils(window); |
|
38 this._contentWindow = window; |
|
39 this._targetElement = aJson.target; |
|
40 this._targetIsEditable = Util.isTextInput(this._targetElement) || |
|
41 this._targetElement instanceof Ci.nsIDOMXULTextBoxElement; |
|
42 if (!this._targetIsEditable) { |
|
43 this._onFail("not an editable?", this._targetElement); |
|
44 return; |
|
45 } |
|
46 |
|
47 let selection = this._getSelection(); |
|
48 if (!selection) { |
|
49 this._onFail("no selection."); |
|
50 return; |
|
51 } |
|
52 |
|
53 if (!this._getTargetElementValue()) { |
|
54 this._onFail("Target element does not contain any content to select."); |
|
55 return; |
|
56 } |
|
57 |
|
58 // Add a listener to respond to programmatic selection changes. |
|
59 selection.QueryInterface(Ci.nsISelectionPrivate).addSelectionListener(this); |
|
60 |
|
61 if (!selection.isCollapsed) { |
|
62 this._mode = this._SELECTION_MODE; |
|
63 this._updateSelectionUI("start", true, true); |
|
64 } else { |
|
65 this._mode = this._CARET_MODE; |
|
66 this._updateSelectionUI("caret", false, false, true); |
|
67 } |
|
68 |
|
69 this._targetElement.addEventListener("blur", this, false); |
|
70 }, |
|
71 |
|
72 /* |
|
73 * Selection monocle start move event handler |
|
74 */ |
|
75 _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) { |
|
76 if (!this.targetIsEditable) { |
|
77 this._onFail("_onSelectionMoveStart with bad targetElement."); |
|
78 return; |
|
79 } |
|
80 |
|
81 if (this._selectionMoveActive) { |
|
82 this._onFail("mouse is already down on drag start?"); |
|
83 return; |
|
84 } |
|
85 |
|
86 // We bail if things get out of sync here implying we missed a message. |
|
87 this._selectionMoveActive = true; |
|
88 |
|
89 if (this._targetIsEditable) { |
|
90 // If we're coming out of an out-of-bounds scroll, the node the user is |
|
91 // trying to drag may be hidden (the monocle will be pegged to the edge |
|
92 // of the edit). Make sure the node the user wants to move is visible |
|
93 // and has focus. |
|
94 this._updateInputFocus(aMsg.change); |
|
95 } |
|
96 |
|
97 // Update the position of our selection monocles |
|
98 this._updateSelectionUI("update", true, true); |
|
99 }, |
|
100 |
|
101 /* |
|
102 * Selection monocle move event handler |
|
103 */ |
|
104 _onSelectionMove: function _onSelectionMove(aMsg) { |
|
105 if (!this.targetIsEditable) { |
|
106 this._onFail("_onSelectionMove with bad targetElement."); |
|
107 return; |
|
108 } |
|
109 |
|
110 if (!this._selectionMoveActive) { |
|
111 this._onFail("mouse isn't down for drag move?"); |
|
112 return; |
|
113 } |
|
114 |
|
115 this._handleSelectionPoint(aMsg, false); |
|
116 }, |
|
117 |
|
118 /* |
|
119 * Selection monocle move finished event handler |
|
120 */ |
|
121 _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) { |
|
122 if (!this.targetIsEditable) { |
|
123 this._onFail("_onSelectionMoveEnd with bad targetElement."); |
|
124 return; |
|
125 } |
|
126 |
|
127 if (!this._selectionMoveActive) { |
|
128 this._onFail("mouse isn't down for drag move?"); |
|
129 return; |
|
130 } |
|
131 |
|
132 this._handleSelectionPoint(aMsg, true); |
|
133 this._selectionMoveActive = false; |
|
134 |
|
135 // Clear any existing scroll timers |
|
136 this._clearTimers(); |
|
137 |
|
138 // Update the position of our selection monocles |
|
139 this._updateSelectionUI("end", true, true); |
|
140 }, |
|
141 |
|
142 _onSelectionUpdate: function _onSelectionUpdate() { |
|
143 if (!this._targetHasFocus()) { |
|
144 this._closeSelection(); |
|
145 return; |
|
146 } |
|
147 this._updateSelectionUI("update", |
|
148 this._mode == this._SELECTION_MODE, |
|
149 this._mode == this._SELECTION_MODE, |
|
150 this._mode == this._CARET_MODE); |
|
151 }, |
|
152 |
|
153 /* |
|
154 * Switch selection modes. Currently we only support switching |
|
155 * from "caret" to "selection". |
|
156 */ |
|
157 _onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) { |
|
158 if (aMode != "selection") { |
|
159 this._onFail("unsupported mode switch"); |
|
160 return; |
|
161 } |
|
162 |
|
163 // Sanity check to be sure we are initialized |
|
164 if (!this._targetElement) { |
|
165 this._onFail("not initialized"); |
|
166 return; |
|
167 } |
|
168 |
|
169 // Similar to _onSelectionStart - we need to create initial selection |
|
170 // but without the initialization bits. |
|
171 let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY }); |
|
172 if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos, |
|
173 Ci.nsIDOMWindowUtils.SELECT_CHARACTER)) { |
|
174 this._onFail("failed to set selection at point"); |
|
175 return; |
|
176 } |
|
177 |
|
178 // We bail if things get out of sync here implying we missed a message. |
|
179 this._selectionMoveActive = true; |
|
180 this._mode = this._SELECTION_MODE; |
|
181 |
|
182 // Update the position of the selection marker that is *not* |
|
183 // being dragged. |
|
184 this._updateSelectionUI("update", aMarker == "end", aMarker == "start"); |
|
185 }, |
|
186 |
|
187 /* |
|
188 * Selection close event handler |
|
189 * |
|
190 * @param aClearSelection requests that selection be cleared. |
|
191 */ |
|
192 _onSelectionClose: function _onSelectionClose(aClearSelection) { |
|
193 if (aClearSelection) { |
|
194 this._clearSelection(); |
|
195 } |
|
196 this._closeSelection(); |
|
197 }, |
|
198 |
|
199 /* |
|
200 * Called if for any reason we fail during the selection |
|
201 * process. Cancels the selection. |
|
202 */ |
|
203 _onFail: function _onFail(aDbgMessage) { |
|
204 if (aDbgMessage && aDbgMessage.length > 0) |
|
205 Util.dumpLn(aDbgMessage); |
|
206 this.sendAsync("Content:SelectionFail"); |
|
207 this._clearSelection(); |
|
208 this._closeSelection(); |
|
209 }, |
|
210 |
|
211 /* |
|
212 * Empty conversion routines to match those in |
|
213 * browser. Called by SelectionHelperUI when |
|
214 * sending and receiving coordinates in messages. |
|
215 */ |
|
216 |
|
217 ptClientToBrowser: function ptClientToBrowser(aX, aY, aIgnoreScroll, aIgnoreScale) { |
|
218 return { x: aX, y: aY } |
|
219 }, |
|
220 |
|
221 rectBrowserToClient: function rectBrowserToClient(aRect, aIgnoreScroll, aIgnoreScale) { |
|
222 return { |
|
223 left: aRect.left, |
|
224 right: aRect.right, |
|
225 top: aRect.top, |
|
226 bottom: aRect.bottom |
|
227 } |
|
228 }, |
|
229 |
|
230 ptBrowserToClient: function ptBrowserToClient(aX, aY, aIgnoreScroll, aIgnoreScale) { |
|
231 return { x: aX, y: aY } |
|
232 }, |
|
233 |
|
234 ctobx: function ctobx(aCoord) { |
|
235 return aCoord; |
|
236 }, |
|
237 |
|
238 ctoby: function ctoby(aCoord) { |
|
239 return aCoord; |
|
240 }, |
|
241 |
|
242 btocx: function btocx(aCoord) { |
|
243 return aCoord; |
|
244 }, |
|
245 |
|
246 btocy: function btocy(aCoord) { |
|
247 return aCoord; |
|
248 }, |
|
249 |
|
250 |
|
251 /************************************************* |
|
252 * Selection helpers |
|
253 */ |
|
254 |
|
255 /* |
|
256 * _clearSelection |
|
257 * |
|
258 * Clear existing selection if it exists and reset our internal state. |
|
259 */ |
|
260 _clearSelection: function _clearSelection() { |
|
261 let selection = this._getSelection(); |
|
262 if (selection) { |
|
263 selection.removeAllRanges(); |
|
264 } |
|
265 }, |
|
266 |
|
267 /* |
|
268 * _closeSelection |
|
269 * |
|
270 * Shuts ChromeSelectionHandler and SelectionHelperUI down. |
|
271 */ |
|
272 _closeSelection: function _closeSelection() { |
|
273 this._deactivate(); |
|
274 this.sendAsync("Content:HandlerShutdown", {}); |
|
275 }, |
|
276 |
|
277 /* |
|
278 * _deactivate |
|
279 * |
|
280 * Resets ChromeSelectionHandler state, previously initialized in |
|
281 * general selection start-method |_onSelectionAttach()|. |
|
282 */ |
|
283 _deactivate: function _deactivate() { |
|
284 // Remove our selection notification listener. |
|
285 let selection = this._getSelection(); |
|
286 if (selection) { |
|
287 try { |
|
288 selection.QueryInterface(Ci.nsISelectionPrivate).removeSelectionListener(this); |
|
289 } catch(e) { |
|
290 // Fail safe during multiple _deactivate() calls. |
|
291 } |
|
292 } |
|
293 |
|
294 this._clearTimers(); |
|
295 this._cache = null; |
|
296 this._contentWindow = null; |
|
297 if (this._targetElement) { |
|
298 this._targetElement.removeEventListener("blur", this, true); |
|
299 this._targetElement = null; |
|
300 } |
|
301 this._selectionMoveActive = false; |
|
302 this._domWinUtils = null; |
|
303 this._targetIsEditable = false; |
|
304 this._mode = null; |
|
305 }, |
|
306 |
|
307 get hasSelection() { |
|
308 if (!this._targetElement) { |
|
309 return false; |
|
310 } |
|
311 let selection = this._getSelection(); |
|
312 return (selection && !selection.isCollapsed); |
|
313 }, |
|
314 |
|
315 _targetHasFocus: function() { |
|
316 if (!this._targetElement || !document.commandDispatcher.focusedElement) { |
|
317 return false; |
|
318 } |
|
319 let bindingParent = this._contentWindow.document.getBindingParent(document.commandDispatcher.focusedElement); |
|
320 return (bindingParent && this._targetElement == bindingParent); |
|
321 }, |
|
322 |
|
323 /************************************************* |
|
324 * Events |
|
325 */ |
|
326 |
|
327 /* |
|
328 * Scroll + selection advancement timer when the monocle is |
|
329 * outside the bounds of an input control. |
|
330 */ |
|
331 scrollTimerCallback: function scrollTimerCallback() { |
|
332 let result = ChromeSelectionHandler.updateTextEditSelection(); |
|
333 // Update monocle position and speed if we've dragged off to one side |
|
334 if (result.trigger) { |
|
335 ChromeSelectionHandler._updateSelectionUI("update", result.start, result.end); |
|
336 } |
|
337 }, |
|
338 |
|
339 handleEvent: function handleEvent(aEvent) { |
|
340 if (aEvent.type == "blur" && !this._targetHasFocus()) { |
|
341 this._closeSelection(); |
|
342 } |
|
343 }, |
|
344 |
|
345 msgHandler: function msgHandler(aMsg, aJson) { |
|
346 if (this._debugEvents && "Browser:SelectionMove" != aMsg) { |
|
347 Util.dumpLn("ChromeSelectionHandler:", aMsg); |
|
348 } |
|
349 switch(aMsg) { |
|
350 case "Browser:SelectionDebug": |
|
351 this._onSelectionDebug(aJson); |
|
352 break; |
|
353 |
|
354 case "Browser:SelectionAttach": |
|
355 this._onSelectionAttach(aJson); |
|
356 break; |
|
357 |
|
358 case "Browser:CaretAttach": |
|
359 this._onSelectionAttach(aJson); |
|
360 break; |
|
361 |
|
362 case "Browser:SelectionClose": |
|
363 this._onSelectionClose(aJson.clearSelection); |
|
364 break; |
|
365 |
|
366 case "Browser:SelectionUpdate": |
|
367 this._onSelectionUpdate(); |
|
368 break; |
|
369 |
|
370 case "Browser:SelectionMoveStart": |
|
371 this._onSelectionMoveStart(aJson); |
|
372 break; |
|
373 |
|
374 case "Browser:SelectionMove": |
|
375 this._onSelectionMove(aJson); |
|
376 break; |
|
377 |
|
378 case "Browser:SelectionMoveEnd": |
|
379 this._onSelectionMoveEnd(aJson); |
|
380 break; |
|
381 |
|
382 case "Browser:CaretUpdate": |
|
383 this._onCaretPositionUpdate(aJson.caret.xPos, aJson.caret.yPos); |
|
384 break; |
|
385 |
|
386 case "Browser:CaretMove": |
|
387 this._onCaretMove(aJson.caret.xPos, aJson.caret.yPos); |
|
388 break; |
|
389 |
|
390 case "Browser:SelectionSwitchMode": |
|
391 this._onSwitchMode(aJson.newMode, aJson.change, aJson.xPos, aJson.yPos); |
|
392 break; |
|
393 } |
|
394 }, |
|
395 |
|
396 /************************************************* |
|
397 * Utilities |
|
398 */ |
|
399 |
|
400 _getSelection: function _getSelection() { |
|
401 let targetElementEditor = this._getTargetElementEditor(); |
|
402 |
|
403 return targetElementEditor ? targetElementEditor.selection : null; |
|
404 }, |
|
405 |
|
406 _getTargetElementValue: function _getTargetElementValue() { |
|
407 if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) { |
|
408 return this._targetElement.inputField.value; |
|
409 } else if (Util.isTextInput(this._targetElement)) { |
|
410 return this._targetElement.value; |
|
411 } |
|
412 return null; |
|
413 }, |
|
414 |
|
415 _getSelectController: function _getSelectController() { |
|
416 let targetElementEditor = this._getTargetElementEditor(); |
|
417 |
|
418 return targetElementEditor ? targetElementEditor.selectionController : null; |
|
419 }, |
|
420 |
|
421 _getTargetElementEditor: function() { |
|
422 if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) { |
|
423 return this._targetElement.QueryInterface(Ci.nsIDOMXULTextBoxElement) |
|
424 .editor; |
|
425 } else if (Util.isTextInput(this._targetElement)) { |
|
426 return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement) |
|
427 .editor; |
|
428 } |
|
429 return null; |
|
430 } |
|
431 }; |
|
432 |
|
433 ChromeSelectionHandler.__proto__ = new SelectionPrototype(); |
|
434 ChromeSelectionHandler.type = 1; // kChromeSelector |
|
435 |