Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 "use strict";
9 dump("###################################### forms.js loaded\n");
11 let Ci = Components.interfaces;
12 let Cc = Components.classes;
13 let Cu = Components.utils;
15 Cu.import("resource://gre/modules/Services.jsm");
16 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
17 XPCOMUtils.defineLazyServiceGetter(Services, "fm",
18 "@mozilla.org/focus-manager;1",
19 "nsIFocusManager");
21 XPCOMUtils.defineLazyGetter(this, "domWindowUtils", function () {
22 return content.QueryInterface(Ci.nsIInterfaceRequestor)
23 .getInterface(Ci.nsIDOMWindowUtils);
24 });
26 const RESIZE_SCROLL_DELAY = 20;
27 // In content editable node, when there are hidden elements such as <br>, it
28 // may need more than one (usually less than 3 times) move/extend operations
29 // to change the selection range. If we cannot change the selection range
30 // with more than 20 opertations, we are likely being blocked and cannot change
31 // the selection range any more.
32 const MAX_BLOCKED_COUNT = 20;
34 let HTMLDocument = Ci.nsIDOMHTMLDocument;
35 let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
36 let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement;
37 let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
38 let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
39 let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
40 let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
41 let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
42 let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
44 let FormVisibility = {
45 /**
46 * Searches upwards in the DOM for an element that has been scrolled.
47 *
48 * @param {HTMLElement} node element to start search at.
49 * @return {Window|HTMLElement|Null} null when none are found window/element otherwise.
50 */
51 findScrolled: function fv_findScrolled(node) {
52 let win = node.ownerDocument.defaultView;
54 while (!(node instanceof HTMLBodyElement)) {
56 // We can skip elements that have not been scrolled.
57 // We only care about top now remember to add the scrollLeft
58 // check if we decide to care about the X axis.
59 if (node.scrollTop !== 0) {
60 // the element has been scrolled so we may need to adjust
61 // where we think the root element is located.
62 //
63 // Otherwise it may seem visible but be scrolled out of the viewport
64 // inside this scrollable node.
65 return node;
66 } else {
67 // this node does not effect where we think
68 // the node is even if it is scrollable it has not hidden
69 // the element we are looking for.
70 node = node.parentNode;
71 continue;
72 }
73 }
75 // we also care about the window this is the more
76 // common case where the content is larger then
77 // the viewport/screen.
78 if (win.scrollMaxX || win.scrollMaxY) {
79 return win;
80 }
82 return null;
83 },
85 /**
86 * Checks if "top and "bottom" points of the position is visible.
87 *
88 * @param {Number} top position.
89 * @param {Number} height of the element.
90 * @param {Number} maxHeight of the window.
91 * @return {Boolean} true when visible.
92 */
93 yAxisVisible: function fv_yAxisVisible(top, height, maxHeight) {
94 return (top > 0 && (top + height) < maxHeight);
95 },
97 /**
98 * Searches up through the dom for scrollable elements
99 * which are not currently visible (relative to the viewport).
100 *
101 * @param {HTMLElement} element to start search at.
102 * @param {Object} pos .top, .height and .width of element.
103 */
104 scrollablesVisible: function fv_scrollablesVisible(element, pos) {
105 while ((element = this.findScrolled(element))) {
106 if (element.window && element.self === element)
107 break;
109 // remember getBoundingClientRect does not care
110 // about scrolling only where the element starts
111 // in the document.
112 let offset = element.getBoundingClientRect();
114 // the top of both the scrollable area and
115 // the form element itself are in the same document.
116 // We adjust the "top" so if the elements coordinates
117 // are relative to the viewport in the current document.
118 let adjustedTop = pos.top - offset.top;
120 let visible = this.yAxisVisible(
121 adjustedTop,
122 pos.height,
123 pos.width
124 );
126 if (!visible)
127 return false;
129 element = element.parentNode;
130 }
132 return true;
133 },
135 /**
136 * Verifies the element is visible in the viewport.
137 * Handles scrollable areas, frames and scrollable viewport(s) (windows).
138 *
139 * @param {HTMLElement} element to verify.
140 * @return {Boolean} true when visible.
141 */
142 isVisible: function fv_isVisible(element) {
143 // scrollable frames can be ignored we just care about iframes...
144 let rect = element.getBoundingClientRect();
145 let parent = element.ownerDocument.defaultView;
147 // used to calculate the inner position of frames / scrollables.
148 // The intent was to use this information to scroll either up or down.
149 // scrollIntoView(true) will _break_ some web content so we can't do
150 // this today. If we want that functionality we need to manually scroll
151 // the individual elements.
152 let pos = {
153 top: rect.top,
154 height: rect.height,
155 width: rect.width
156 };
158 let visible = true;
160 do {
161 let frame = parent.frameElement;
162 visible = visible &&
163 this.yAxisVisible(pos.top, pos.height, parent.innerHeight) &&
164 this.scrollablesVisible(element, pos);
166 // nothing we can do about this now...
167 // In the future we can use this information to scroll
168 // only the elements we need to at this point as we should
169 // have all the details we need to figure out how to scroll.
170 if (!visible)
171 return false;
173 if (frame) {
174 let frameRect = frame.getBoundingClientRect();
176 pos.top += frameRect.top + frame.clientTop;
177 }
178 } while (
179 (parent !== parent.parent) &&
180 (parent = parent.parent)
181 );
183 return visible;
184 }
185 };
187 let FormAssistant = {
188 init: function fa_init() {
189 addEventListener("focus", this, true, false);
190 addEventListener("blur", this, true, false);
191 addEventListener("resize", this, true, false);
192 addEventListener("submit", this, true, false);
193 addEventListener("pagehide", this, true, false);
194 addEventListener("beforeunload", this, true, false);
195 addEventListener("input", this, true, false);
196 addEventListener("keydown", this, true, false);
197 addEventListener("keyup", this, true, false);
198 addMessageListener("Forms:Select:Choice", this);
199 addMessageListener("Forms:Input:Value", this);
200 addMessageListener("Forms:Select:Blur", this);
201 addMessageListener("Forms:SetSelectionRange", this);
202 addMessageListener("Forms:ReplaceSurroundingText", this);
203 addMessageListener("Forms:GetText", this);
204 addMessageListener("Forms:Input:SendKey", this);
205 addMessageListener("Forms:GetContext", this);
206 addMessageListener("Forms:SetComposition", this);
207 addMessageListener("Forms:EndComposition", this);
208 },
210 ignoredInputTypes: new Set([
211 'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image',
212 'range'
213 ]),
215 isKeyboardOpened: false,
216 selectionStart: -1,
217 selectionEnd: -1,
218 textBeforeCursor: "",
219 textAfterCursor: "",
220 scrollIntoViewTimeout: null,
221 _focusedElement: null,
222 _focusCounter: 0, // up one for every time we focus a new element
223 _observer: null,
224 _documentEncoder: null,
225 _editor: null,
226 _editing: false,
228 get focusedElement() {
229 if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
230 this._focusedElement = null;
232 return this._focusedElement;
233 },
235 set focusedElement(val) {
236 this._focusCounter++;
237 this._focusedElement = val;
238 },
240 setFocusedElement: function fa_setFocusedElement(element) {
241 let self = this;
243 if (element === this.focusedElement)
244 return;
246 if (this.focusedElement) {
247 this.focusedElement.removeEventListener('mousedown', this);
248 this.focusedElement.removeEventListener('mouseup', this);
249 this.focusedElement.removeEventListener('compositionend', this);
250 if (this._observer) {
251 this._observer.disconnect();
252 this._observer = null;
253 }
254 if (!element) {
255 this.focusedElement.blur();
256 }
257 }
259 this._documentEncoder = null;
260 if (this._editor) {
261 // When the nsIFrame of the input element is reconstructed by
262 // CSS restyling, the editor observers are removed. Catch
263 // [nsIEditor.removeEditorObserver] failure exception if that
264 // happens.
265 try {
266 this._editor.removeEditorObserver(this);
267 } catch (e) {}
268 this._editor = null;
269 }
271 if (element) {
272 element.addEventListener('mousedown', this);
273 element.addEventListener('mouseup', this);
274 element.addEventListener('compositionend', this);
275 if (isContentEditable(element)) {
276 this._documentEncoder = getDocumentEncoder(element);
277 }
278 this._editor = getPlaintextEditor(element);
279 if (this._editor) {
280 // Add a nsIEditorObserver to monitor the text content of the focused
281 // element.
282 this._editor.addEditorObserver(this);
283 }
285 // If our focusedElement is removed from DOM we want to handle it properly
286 let MutationObserver = element.ownerDocument.defaultView.MutationObserver;
287 this._observer = new MutationObserver(function(mutations) {
288 var del = [].some.call(mutations, function(m) {
289 return [].some.call(m.removedNodes, function(n) {
290 return n.contains(element);
291 });
292 });
293 if (del && element === self.focusedElement) {
294 // item was deleted, fake a blur so all state gets set correctly
295 self.handleEvent({ target: element, type: "blur" });
296 }
297 });
299 this._observer.observe(element.ownerDocument.body, {
300 childList: true,
301 subtree: true
302 });
303 }
305 this.focusedElement = element;
306 },
308 get documentEncoder() {
309 return this._documentEncoder;
310 },
312 // Get the nsIPlaintextEditor object of current input field.
313 get editor() {
314 return this._editor;
315 },
317 // Implements nsIEditorObserver get notification when the text content of
318 // current input field has changed.
319 EditAction: function fa_editAction() {
320 if (this._editing) {
321 return;
322 }
323 this.sendKeyboardState(this.focusedElement);
324 },
326 handleEvent: function fa_handleEvent(evt) {
327 let target = evt.target;
329 let range = null;
330 switch (evt.type) {
331 case "focus":
332 if (!target) {
333 break;
334 }
336 // Focusing on Window, Document or iFrame should focus body
337 if (target instanceof HTMLHtmlElement) {
338 target = target.document.body;
339 } else if (target instanceof HTMLDocument) {
340 target = target.body;
341 } else if (target instanceof HTMLIFrameElement) {
342 target = target.contentDocument ? target.contentDocument.body
343 : null;
344 }
346 if (!target) {
347 break;
348 }
350 if (isContentEditable(target)) {
351 this.showKeyboard(this.getTopLevelEditable(target));
352 this.updateSelection();
353 break;
354 }
356 if (this.isFocusableElement(target)) {
357 this.showKeyboard(target);
358 this.updateSelection();
359 }
360 break;
362 case "pagehide":
363 case "beforeunload":
364 // We are only interested to the pagehide and beforeunload events from
365 // the root document.
366 if (target && target != content.document) {
367 break;
368 }
369 // fall through
370 case "blur":
371 case "submit":
372 if (this.focusedElement) {
373 this.hideKeyboard();
374 this.selectionStart = -1;
375 this.selectionEnd = -1;
376 }
377 break;
379 case 'mousedown':
380 if (!this.focusedElement) {
381 break;
382 }
384 // We only listen for this event on the currently focused element.
385 // When the mouse goes down, note the cursor/selection position
386 this.updateSelection();
387 break;
389 case 'mouseup':
390 if (!this.focusedElement) {
391 break;
392 }
394 // We only listen for this event on the currently focused element.
395 // When the mouse goes up, see if the cursor has moved (or the
396 // selection changed) since the mouse went down. If it has, we
397 // need to tell the keyboard about it
398 range = getSelectionRange(this.focusedElement);
399 if (range[0] !== this.selectionStart ||
400 range[1] !== this.selectionEnd) {
401 this.updateSelection();
402 }
403 break;
405 case "resize":
406 if (!this.isKeyboardOpened)
407 return;
409 if (this.scrollIntoViewTimeout) {
410 content.clearTimeout(this.scrollIntoViewTimeout);
411 this.scrollIntoViewTimeout = null;
412 }
414 // We may receive multiple resize events in quick succession, so wait
415 // a bit before scrolling the input element into view.
416 if (this.focusedElement) {
417 this.scrollIntoViewTimeout = content.setTimeout(function () {
418 this.scrollIntoViewTimeout = null;
419 if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) {
420 scrollSelectionOrElementIntoView(this.focusedElement);
421 }
422 }.bind(this), RESIZE_SCROLL_DELAY);
423 }
424 break;
426 case "input":
427 if (this.focusedElement) {
428 // When the text content changes, notify the keyboard
429 this.updateSelection();
430 }
431 break;
433 case "keydown":
434 if (!this.focusedElement) {
435 break;
436 }
438 CompositionManager.endComposition('');
440 // We use 'setTimeout' to wait until the input element accomplishes the
441 // change in selection range.
442 content.setTimeout(function() {
443 this.updateSelection();
444 }.bind(this), 0);
445 break;
447 case "keyup":
448 if (!this.focusedElement) {
449 break;
450 }
452 CompositionManager.endComposition('');
454 break;
456 case "compositionend":
457 if (!this.focusedElement) {
458 break;
459 }
461 CompositionManager.onCompositionEnd();
462 break;
463 }
464 },
466 receiveMessage: function fa_receiveMessage(msg) {
467 let target = this.focusedElement;
468 let json = msg.json;
470 // To not break mozKeyboard contextId is optional
471 if ('contextId' in json &&
472 json.contextId !== this._focusCounter &&
473 json.requestId) {
474 // Ignore messages that are meant for a previously focused element
475 sendAsyncMessage("Forms:SequenceError", {
476 requestId: json.requestId,
477 error: "Expected contextId " + this._focusCounter +
478 " but was " + json.contextId
479 });
480 return;
481 }
483 if (!target) {
484 switch (msg.name) {
485 case "Forms:GetText":
486 sendAsyncMessage("Forms:GetText:Result:Error", {
487 requestId: json.requestId,
488 error: "No focused element"
489 });
490 break;
491 }
492 return;
493 }
495 this._editing = true;
496 switch (msg.name) {
497 case "Forms:Input:Value": {
498 CompositionManager.endComposition('');
500 target.value = json.value;
502 let event = target.ownerDocument.createEvent('HTMLEvents');
503 event.initEvent('input', true, false);
504 target.dispatchEvent(event);
505 break;
506 }
508 case "Forms:Input:SendKey":
509 CompositionManager.endComposition('');
511 this._editing = true;
512 let doKeypress = domWindowUtils.sendKeyEvent('keydown', json.keyCode,
513 json.charCode, json.modifiers);
514 if (doKeypress) {
515 domWindowUtils.sendKeyEvent('keypress', json.keyCode,
516 json.charCode, json.modifiers);
517 }
519 if(!json.repeat) {
520 domWindowUtils.sendKeyEvent('keyup', json.keyCode,
521 json.charCode, json.modifiers);
522 }
524 this._editing = false;
526 if (json.requestId && doKeypress) {
527 sendAsyncMessage("Forms:SendKey:Result:OK", {
528 requestId: json.requestId
529 });
530 }
531 else if (json.requestId && !doKeypress) {
532 sendAsyncMessage("Forms:SendKey:Result:Error", {
533 requestId: json.requestId,
534 error: "Keydown event got canceled"
535 });
536 }
537 break;
539 case "Forms:Select:Choice":
540 let options = target.options;
541 let valueChanged = false;
542 if ("index" in json) {
543 if (options.selectedIndex != json.index) {
544 options.selectedIndex = json.index;
545 valueChanged = true;
546 }
547 } else if ("indexes" in json) {
548 for (let i = 0; i < options.length; i++) {
549 let newValue = (json.indexes.indexOf(i) != -1);
550 if (options.item(i).selected != newValue) {
551 options.item(i).selected = newValue;
552 valueChanged = true;
553 }
554 }
555 }
557 // only fire onchange event if any selected option is changed
558 if (valueChanged) {
559 let event = target.ownerDocument.createEvent('HTMLEvents');
560 event.initEvent('change', true, true);
561 target.dispatchEvent(event);
562 }
563 break;
565 case "Forms:Select:Blur": {
566 this.setFocusedElement(null);
567 break;
568 }
570 case "Forms:SetSelectionRange": {
571 CompositionManager.endComposition('');
573 let start = json.selectionStart;
574 let end = json.selectionEnd;
576 if (!setSelectionRange(target, start, end)) {
577 if (json.requestId) {
578 sendAsyncMessage("Forms:SetSelectionRange:Result:Error", {
579 requestId: json.requestId,
580 error: "failed"
581 });
582 }
583 break;
584 }
586 this.updateSelection();
588 if (json.requestId) {
589 sendAsyncMessage("Forms:SetSelectionRange:Result:OK", {
590 requestId: json.requestId,
591 selectioninfo: this.getSelectionInfo()
592 });
593 }
594 break;
595 }
597 case "Forms:ReplaceSurroundingText": {
598 CompositionManager.endComposition('');
600 let selectionRange = getSelectionRange(target);
601 if (!replaceSurroundingText(target,
602 json.text,
603 selectionRange[0],
604 selectionRange[1],
605 json.offset,
606 json.length)) {
607 if (json.requestId) {
608 sendAsyncMessage("Forms:ReplaceSurroundingText:Result:Error", {
609 requestId: json.requestId,
610 error: "failed"
611 });
612 }
613 break;
614 }
616 if (json.requestId) {
617 sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", {
618 requestId: json.requestId,
619 selectioninfo: this.getSelectionInfo()
620 });
621 }
622 break;
623 }
625 case "Forms:GetText": {
626 let value = isContentEditable(target) ? getContentEditableText(target)
627 : target.value;
629 if (json.offset && json.length) {
630 value = value.substr(json.offset, json.length);
631 }
632 else if (json.offset) {
633 value = value.substr(json.offset);
634 }
636 sendAsyncMessage("Forms:GetText:Result:OK", {
637 requestId: json.requestId,
638 text: value
639 });
640 break;
641 }
643 case "Forms:GetContext": {
644 let obj = getJSON(target, this._focusCounter);
645 sendAsyncMessage("Forms:GetContext:Result:OK", obj);
646 break;
647 }
649 case "Forms:SetComposition": {
650 CompositionManager.setComposition(target, json.text, json.cursor,
651 json.clauses);
652 sendAsyncMessage("Forms:SetComposition:Result:OK", {
653 requestId: json.requestId,
654 });
655 break;
656 }
658 case "Forms:EndComposition": {
659 CompositionManager.endComposition(json.text);
660 sendAsyncMessage("Forms:EndComposition:Result:OK", {
661 requestId: json.requestId,
662 });
663 break;
664 }
665 }
666 this._editing = false;
668 },
670 showKeyboard: function fa_showKeyboard(target) {
671 if (this.focusedElement === target)
672 return;
674 if (target instanceof HTMLOptionElement)
675 target = target.parentNode;
677 this.setFocusedElement(target);
679 let kbOpened = this.sendKeyboardState(target);
680 if (this.isTextInputElement(target))
681 this.isKeyboardOpened = kbOpened;
682 },
684 hideKeyboard: function fa_hideKeyboard() {
685 sendAsyncMessage("Forms:Input", { "type": "blur" });
686 this.isKeyboardOpened = false;
687 this.setFocusedElement(null);
688 },
690 isFocusableElement: function fa_isFocusableElement(element) {
691 if (element instanceof HTMLSelectElement ||
692 element instanceof HTMLTextAreaElement)
693 return true;
695 if (element instanceof HTMLOptionElement &&
696 element.parentNode instanceof HTMLSelectElement)
697 return true;
699 return (element instanceof HTMLInputElement &&
700 !this.ignoredInputTypes.has(element.type));
701 },
703 isTextInputElement: function fa_isTextInputElement(element) {
704 return element instanceof HTMLInputElement ||
705 element instanceof HTMLTextAreaElement ||
706 isContentEditable(element);
707 },
709 getTopLevelEditable: function fa_getTopLevelEditable(element) {
710 function retrieveTopLevelEditable(element) {
711 while (element && !isContentEditable(element))
712 element = element.parentNode;
714 return element;
715 }
717 return retrieveTopLevelEditable(element) || element;
718 },
720 sendKeyboardState: function(element) {
721 // FIXME/bug 729623: work around apparent bug in the IME manager
722 // in gecko.
723 let readonly = element.getAttribute("readonly");
724 if (readonly) {
725 return false;
726 }
728 sendAsyncMessage("Forms:Input", getJSON(element, this._focusCounter));
729 return true;
730 },
732 getSelectionInfo: function fa_getSelectionInfo() {
733 let element = this.focusedElement;
734 let range = getSelectionRange(element);
736 let text = isContentEditable(element) ? getContentEditableText(element)
737 : element.value;
739 let textAround = getTextAroundCursor(text, range);
741 let changed = this.selectionStart !== range[0] ||
742 this.selectionEnd !== range[1] ||
743 this.textBeforeCursor !== textAround.before ||
744 this.textAfterCursor !== textAround.after;
746 this.selectionStart = range[0];
747 this.selectionEnd = range[1];
748 this.textBeforeCursor = textAround.before;
749 this.textAfterCursor = textAround.after;
751 return {
752 selectionStart: range[0],
753 selectionEnd: range[1],
754 textBeforeCursor: textAround.before,
755 textAfterCursor: textAround.after,
756 changed: changed
757 };
758 },
760 // Notify when the selection range changes
761 updateSelection: function fa_updateSelection() {
762 if (!this.focusedElement) {
763 return;
764 }
765 let selectionInfo = this.getSelectionInfo();
766 if (selectionInfo.changed) {
767 sendAsyncMessage("Forms:SelectionChange", this.getSelectionInfo());
768 }
769 }
770 };
772 FormAssistant.init();
774 function isContentEditable(element) {
775 if (!element) {
776 return false;
777 }
779 if (element.isContentEditable || element.designMode == "on")
780 return true;
782 return element.ownerDocument && element.ownerDocument.designMode == "on";
783 }
785 function isPlainTextField(element) {
786 if (!element) {
787 return false;
788 }
790 return element instanceof HTMLTextAreaElement ||
791 (element instanceof HTMLInputElement &&
792 element.mozIsTextField(false));
793 }
795 function getJSON(element, focusCounter) {
796 // <input type=number> has a nested anonymous <input type=text> element that
797 // takes focus on behalf of the number control when someone tries to focus
798 // the number control. If |element| is such an anonymous text control then we
799 // need it's number control here in order to get the correct 'type' etc.:
800 element = element.ownerNumberControl || element;
802 let type = element.type || "";
803 let value = element.value || "";
804 let max = element.max || "";
805 let min = element.min || "";
807 // Treat contenteditble element as a special text area field
808 if (isContentEditable(element)) {
809 type = "textarea";
810 value = getContentEditableText(element);
811 }
813 // Until the input type=date/datetime/range have been implemented
814 // let's return their real type even if the platform returns 'text'
815 let attributeType = element.getAttribute("type") || "";
817 if (attributeType) {
818 var typeLowerCase = attributeType.toLowerCase();
819 switch (typeLowerCase) {
820 case "datetime":
821 case "datetime-local":
822 case "range":
823 type = typeLowerCase;
824 break;
825 }
826 }
828 // Gecko has some support for @inputmode but behind a preference and
829 // it is disabled by default.
830 // Gaia is then using @x-inputmode has its proprietary way to set
831 // inputmode for fields. This shouldn't be used outside of pre-installed
832 // apps because the attribute is going to disappear as soon as a definitive
833 // solution will be find.
834 let inputmode = element.getAttribute('x-inputmode');
835 if (inputmode) {
836 inputmode = inputmode.toLowerCase();
837 } else {
838 inputmode = '';
839 }
841 let range = getSelectionRange(element);
842 let textAround = getTextAroundCursor(value, range);
844 return {
845 "contextId": focusCounter,
847 "type": type.toLowerCase(),
848 "choices": getListForElement(element),
849 "value": value,
850 "inputmode": inputmode,
851 "selectionStart": range[0],
852 "selectionEnd": range[1],
853 "max": max,
854 "min": min,
855 "lang": element.lang || "",
856 "textBeforeCursor": textAround.before,
857 "textAfterCursor": textAround.after
858 };
859 }
861 function getTextAroundCursor(value, range) {
862 let textBeforeCursor = range[0] < 100 ?
863 value.substr(0, range[0]) :
864 value.substr(range[0] - 100, 100);
866 let textAfterCursor = range[1] + 100 > value.length ?
867 value.substr(range[0], value.length) :
868 value.substr(range[0], range[1] - range[0] + 100);
870 return {
871 before: textBeforeCursor,
872 after: textAfterCursor
873 };
874 }
876 function getListForElement(element) {
877 if (!(element instanceof HTMLSelectElement))
878 return null;
880 let optionIndex = 0;
881 let result = {
882 "multiple": element.multiple,
883 "choices": []
884 };
886 // Build up a flat JSON array of the choices.
887 // In HTML, it's possible for select element choices to be under a
888 // group header (but not recursively). We distinguish between headers
889 // and entries using the boolean "list.group".
890 let children = element.children;
891 for (let i = 0; i < children.length; i++) {
892 let child = children[i];
894 if (child instanceof HTMLOptGroupElement) {
895 result.choices.push({
896 "group": true,
897 "text": child.label || child.firstChild.data,
898 "disabled": child.disabled
899 });
901 let subchildren = child.children;
902 for (let j = 0; j < subchildren.length; j++) {
903 let subchild = subchildren[j];
904 result.choices.push({
905 "group": false,
906 "inGroup": true,
907 "text": subchild.text,
908 "disabled": child.disabled || subchild.disabled,
909 "selected": subchild.selected,
910 "optionIndex": optionIndex++
911 });
912 }
913 } else if (child instanceof HTMLOptionElement) {
914 result.choices.push({
915 "group": false,
916 "inGroup": false,
917 "text": child.text,
918 "disabled": child.disabled,
919 "selected": child.selected,
920 "optionIndex": optionIndex++
921 });
922 }
923 }
925 return result;
926 };
928 // Create a plain text document encode from the focused element.
929 function getDocumentEncoder(element) {
930 let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"]
931 .createInstance(Ci.nsIDocumentEncoder);
932 let flags = Ci.nsIDocumentEncoder.SkipInvisibleContent |
933 Ci.nsIDocumentEncoder.OutputRaw |
934 Ci.nsIDocumentEncoder.OutputDropInvisibleBreak |
935 // Bug 902847. Don't trim trailing spaces of a line.
936 Ci.nsIDocumentEncoder.OutputDontRemoveLineEndingSpaces |
937 Ci.nsIDocumentEncoder.OutputLFLineBreak |
938 Ci.nsIDocumentEncoder.OutputNonTextContentAsPlaceholder;
939 encoder.init(element.ownerDocument, "text/plain", flags);
940 return encoder;
941 }
943 // Get the visible content text of a content editable element
944 function getContentEditableText(element) {
945 if (!element || !isContentEditable(element)) {
946 return null;
947 }
949 let doc = element.ownerDocument;
950 let range = doc.createRange();
951 range.selectNodeContents(element);
952 let encoder = FormAssistant.documentEncoder;
953 encoder.setRange(range);
954 return encoder.encodeToString();
955 }
957 function getSelectionRange(element) {
958 let start = 0;
959 let end = 0;
960 if (isPlainTextField(element)) {
961 // Get the selection range of <input> and <textarea> elements
962 start = element.selectionStart;
963 end = element.selectionEnd;
964 } else if (isContentEditable(element)){
965 // Get the selection range of contenteditable elements
966 let win = element.ownerDocument.defaultView;
967 let sel = win.getSelection();
968 if (sel && sel.rangeCount > 0) {
969 start = getContentEditableSelectionStart(element, sel);
970 end = start + getContentEditableSelectionLength(element, sel);
971 } else {
972 dump("Failed to get window.getSelection()\n");
973 }
974 }
975 return [start, end];
976 }
978 function getContentEditableSelectionStart(element, selection) {
979 let doc = element.ownerDocument;
980 let range = doc.createRange();
981 range.setStart(element, 0);
982 range.setEnd(selection.anchorNode, selection.anchorOffset);
983 let encoder = FormAssistant.documentEncoder;
984 encoder.setRange(range);
985 return encoder.encodeToString().length;
986 }
988 function getContentEditableSelectionLength(element, selection) {
989 let encoder = FormAssistant.documentEncoder;
990 encoder.setRange(selection.getRangeAt(0));
991 return encoder.encodeToString().length;
992 }
994 function setSelectionRange(element, start, end) {
995 let isTextField = isPlainTextField(element);
997 // Check the parameters
999 if (!isTextField && !isContentEditable(element)) {
1000 // Skip HTMLOptionElement and HTMLSelectElement elements, as they don't
1001 // support the operation of setSelectionRange
1002 return false;
1003 }
1005 let text = isTextField ? element.value : getContentEditableText(element);
1006 let length = text.length;
1007 if (start < 0) {
1008 start = 0;
1009 }
1010 if (end > length) {
1011 end = length;
1012 }
1013 if (start > end) {
1014 start = end;
1015 }
1017 if (isTextField) {
1018 // Set the selection range of <input> and <textarea> elements
1019 element.setSelectionRange(start, end, "forward");
1020 return true;
1021 } else {
1022 // set the selection range of contenteditable elements
1023 let win = element.ownerDocument.defaultView;
1024 let sel = win.getSelection();
1026 // Move the caret to the start position
1027 sel.collapse(element, 0);
1028 for (let i = 0; i < start; i++) {
1029 sel.modify("move", "forward", "character");
1030 }
1032 // Avoid entering infinite loop in case we cannot change the selection
1033 // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918
1034 let oldStart = getContentEditableSelectionStart(element, sel);
1035 let counter = 0;
1036 while (oldStart < start) {
1037 sel.modify("move", "forward", "character");
1038 let newStart = getContentEditableSelectionStart(element, sel);
1039 if (oldStart == newStart) {
1040 counter++;
1041 if (counter > MAX_BLOCKED_COUNT) {
1042 return false;
1043 }
1044 } else {
1045 counter = 0;
1046 oldStart = newStart;
1047 }
1048 }
1050 // Extend the selection to the end position
1051 for (let i = start; i < end; i++) {
1052 sel.modify("extend", "forward", "character");
1053 }
1055 // Avoid entering infinite loop in case we cannot change the selection
1056 // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918
1057 counter = 0;
1058 let selectionLength = end - start;
1059 let oldSelectionLength = getContentEditableSelectionLength(element, sel);
1060 while (oldSelectionLength < selectionLength) {
1061 sel.modify("extend", "forward", "character");
1062 let newSelectionLength = getContentEditableSelectionLength(element, sel);
1063 if (oldSelectionLength == newSelectionLength ) {
1064 counter++;
1065 if (counter > MAX_BLOCKED_COUNT) {
1066 return false;
1067 }
1068 } else {
1069 counter = 0;
1070 oldSelectionLength = newSelectionLength;
1071 }
1072 }
1073 return true;
1074 }
1075 }
1077 /**
1078 * Scroll the given element into view.
1079 *
1080 * Calls scrollSelectionIntoView for contentEditable elements.
1081 */
1082 function scrollSelectionOrElementIntoView(element) {
1083 let editor = getPlaintextEditor(element);
1084 if (editor) {
1085 editor.selectionController.scrollSelectionIntoView(
1086 Ci.nsISelectionController.SELECTION_NORMAL,
1087 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
1088 Ci.nsISelectionController.SCROLL_SYNCHRONOUS);
1089 } else {
1090 element.scrollIntoView(false);
1091 }
1092 }
1094 // Get nsIPlaintextEditor object from an input field
1095 function getPlaintextEditor(element) {
1096 let editor = null;
1097 // Get nsIEditor
1098 if (isPlainTextField(element)) {
1099 // Get from the <input> and <textarea> elements
1100 editor = element.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
1101 } else if (isContentEditable(element)) {
1102 // Get from content editable element
1103 let win = element.ownerDocument.defaultView;
1104 let editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor)
1105 .getInterface(Ci.nsIWebNavigation)
1106 .QueryInterface(Ci.nsIInterfaceRequestor)
1107 .getInterface(Ci.nsIEditingSession);
1108 if (editingSession) {
1109 editor = editingSession.getEditorForWindow(win);
1110 }
1111 }
1112 if (editor) {
1113 editor.QueryInterface(Ci.nsIPlaintextEditor);
1114 }
1115 return editor;
1116 }
1118 function replaceSurroundingText(element, text, selectionStart, selectionEnd,
1119 offset, length) {
1120 let editor = FormAssistant.editor;
1121 if (!editor) {
1122 return false;
1123 }
1125 // Check the parameters.
1126 let start = selectionStart + offset;
1127 if (start < 0) {
1128 start = 0;
1129 }
1130 if (length < 0) {
1131 length = 0;
1132 }
1133 let end = start + length;
1135 if (selectionStart != start || selectionEnd != end) {
1136 // Change selection range before replacing.
1137 if (!setSelectionRange(element, start, end)) {
1138 return false;
1139 }
1140 }
1142 if (start != end) {
1143 // Delete the selected text.
1144 editor.deleteSelection(Ci.nsIEditor.ePrevious, Ci.nsIEditor.eStrip);
1145 }
1147 if (text) {
1148 // We don't use CR but LF
1149 // see https://bugzilla.mozilla.org/show_bug.cgi?id=902847
1150 text = text.replace(/\r/g, '\n');
1151 // Insert the text to be replaced with.
1152 editor.insertText(text);
1153 }
1154 return true;
1155 }
1157 let CompositionManager = {
1158 _isStarted: false,
1159 _text: '',
1160 _clauseAttrMap: {
1161 'raw-input':
1162 Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT,
1163 'selected-raw-text':
1164 Ci.nsICompositionStringSynthesizer.ATTR_SELECTEDRAWTEXT,
1165 'converted-text':
1166 Ci.nsICompositionStringSynthesizer.ATTR_CONVERTEDTEXT,
1167 'selected-converted-text':
1168 Ci.nsICompositionStringSynthesizer.ATTR_SELECTEDCONVERTEDTEXT
1169 },
1171 setComposition: function cm_setComposition(element, text, cursor, clauses) {
1172 // Check parameters.
1173 if (!element) {
1174 return;
1175 }
1176 let len = text.length;
1177 if (cursor > len) {
1178 cursor = len;
1179 }
1180 let clauseLens = [];
1181 let clauseAttrs = [];
1182 if (clauses) {
1183 let remainingLength = len;
1184 for (let i = 0; i < clauses.length; i++) {
1185 if (clauses[i]) {
1186 let clauseLength = clauses[i].length || 0;
1187 // Make sure the total clauses length is not bigger than that of the
1188 // composition string.
1189 if (clauseLength > remainingLength) {
1190 clauseLength = remainingLength;
1191 }
1192 remainingLength -= clauseLength;
1193 clauseLens.push(clauseLength);
1194 clauseAttrs.push(this._clauseAttrMap[clauses[i].selectionType] ||
1195 Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT);
1196 }
1197 }
1198 // If the total clauses length is less than that of the composition
1199 // string, extend the last clause to the end of the composition string.
1200 if (remainingLength > 0) {
1201 clauseLens[clauseLens.length - 1] += remainingLength;
1202 }
1203 } else {
1204 clauseLens.push(len);
1205 clauseAttrs.push(Ci.nsICompositionStringSynthesizer.ATTR_RAWINPUT);
1206 }
1208 // Start composition if need to.
1209 if (!this._isStarted) {
1210 this._isStarted = true;
1211 domWindowUtils.sendCompositionEvent('compositionstart', '', '');
1212 this._text = '';
1213 }
1215 // Update the composing text.
1216 if (this._text !== text) {
1217 this._text = text;
1218 domWindowUtils.sendCompositionEvent('compositionupdate', text, '');
1219 }
1220 let compositionString = domWindowUtils.createCompositionStringSynthesizer();
1221 compositionString.setString(text);
1222 for (var i = 0; i < clauseLens.length; i++) {
1223 compositionString.appendClause(clauseLens[i], clauseAttrs[i]);
1224 }
1225 if (cursor >= 0) {
1226 compositionString.setCaret(cursor, 0);
1227 }
1228 compositionString.dispatchEvent();
1229 },
1231 endComposition: function cm_endComposition(text) {
1232 if (!this._isStarted) {
1233 return;
1234 }
1235 // Update the composing text.
1236 if (this._text !== text) {
1237 domWindowUtils.sendCompositionEvent('compositionupdate', text, '');
1238 }
1239 let compositionString = domWindowUtils.createCompositionStringSynthesizer();
1240 compositionString.setString(text);
1241 // Set the cursor position to |text.length| so that the text will be
1242 // committed before the cursor position.
1243 compositionString.setCaret(text.length, 0);
1244 compositionString.dispatchEvent();
1245 domWindowUtils.sendCompositionEvent('compositionend', text, '');
1246 this._text = '';
1247 this._isStarted = false;
1248 },
1250 // Composition ends due to external actions.
1251 onCompositionEnd: function cm_onCompositionEnd() {
1252 if (!this._isStarted) {
1253 return;
1254 }
1256 this._text = '';
1257 this._isStarted = false;
1258 }
1259 };