|
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 |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 let Ci = Components.interfaces; |
|
7 let Cc = Components.classes; |
|
8 |
|
9 dump("### FormHelper.js loaded\n"); |
|
10 |
|
11 let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement; |
|
12 let HTMLInputElement = Ci.nsIDOMHTMLInputElement; |
|
13 let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; |
|
14 let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; |
|
15 let HTMLDocument = Ci.nsIDOMHTMLDocument; |
|
16 let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement; |
|
17 let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement; |
|
18 let HTMLLabelElement = Ci.nsIDOMHTMLLabelElement; |
|
19 let HTMLButtonElement = Ci.nsIDOMHTMLButtonElement; |
|
20 let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement; |
|
21 let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement; |
|
22 let XULMenuListElement = Ci.nsIDOMXULMenuListElement; |
|
23 |
|
24 /** |
|
25 * Responsible of navigation between forms fields and of the opening of the assistant |
|
26 */ |
|
27 function FormAssistant() { |
|
28 addMessageListener("FormAssist:Closed", this); |
|
29 addMessageListener("FormAssist:ChoiceSelect", this); |
|
30 addMessageListener("FormAssist:ChoiceChange", this); |
|
31 addMessageListener("FormAssist:AutoComplete", this); |
|
32 addMessageListener("FormAssist:Update", this); |
|
33 |
|
34 /* Listen text events in order to update the autocomplete suggestions as soon |
|
35 * a key is entered on device |
|
36 */ |
|
37 addEventListener("text", this, false); |
|
38 addEventListener("focus", this, true); |
|
39 addEventListener("blur", this, true); |
|
40 addEventListener("pageshow", this, false); |
|
41 addEventListener("pagehide", this, false); |
|
42 addEventListener("submit", this, false); |
|
43 } |
|
44 |
|
45 FormAssistant.prototype = { |
|
46 _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService), |
|
47 _open: false, |
|
48 _focusSync: false, |
|
49 _debugEvents: false, |
|
50 _selectWrapper: null, |
|
51 _currentElement: null, |
|
52 invalidSubmit: false, |
|
53 |
|
54 get focusSync() { |
|
55 return this._focusSync; |
|
56 }, |
|
57 |
|
58 set focusSync(aVal) { |
|
59 this._focusSync = aVal; |
|
60 }, |
|
61 |
|
62 get currentElement() { |
|
63 return this._currentElement; |
|
64 }, |
|
65 |
|
66 set currentElement(aElement) { |
|
67 if (!aElement || !this._isVisibleElement(aElement)) { |
|
68 return null; |
|
69 } |
|
70 |
|
71 this._currentElement = aElement; |
|
72 gFocusManager.setFocus(this._currentElement, Ci.nsIFocusManager.FLAG_NOSCROLL); |
|
73 |
|
74 // To ensure we get the current caret positionning of the focused |
|
75 // element we need to delayed a bit the event |
|
76 this._executeDelayed(function(self) { |
|
77 // Bug 640870 |
|
78 // Sometimes the element inner frame get destroyed while the element |
|
79 // receive the focus because the display is turned to 'none' for |
|
80 // example, in this "fun" case just do nothing if the element is hidden |
|
81 if (self._isVisibleElement(gFocusManager.focusedElement)) { |
|
82 self._sendJsonMsgWrapper("FormAssist:Show"); |
|
83 } |
|
84 }); |
|
85 return this._currentElement; |
|
86 }, |
|
87 |
|
88 open: function formHelperOpen(aElement, aEvent) { |
|
89 // If the click is on an option element we want to check if the parent |
|
90 // is a valid target. |
|
91 if (aElement instanceof HTMLOptionElement && |
|
92 aElement.parentNode instanceof HTMLSelectElement && |
|
93 !aElement.disabled) { |
|
94 aElement = aElement.parentNode; |
|
95 } |
|
96 |
|
97 // Don't show the formhelper popup for multi-select boxes, except for touch. |
|
98 if (aElement instanceof HTMLSelectElement && aEvent) { |
|
99 if ((aElement.multiple || aElement.size > 1) && |
|
100 aEvent.mozInputSource != Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) { |
|
101 return false; |
|
102 } |
|
103 // Don't fire mouse events on selects; see bug 685197. |
|
104 aEvent.preventDefault(); |
|
105 aEvent.stopPropagation(); |
|
106 } |
|
107 |
|
108 // The form assistant will close if a click happen: |
|
109 // * outside of the scope of the form helper |
|
110 // * hover a button of type=[image|submit] |
|
111 // * hover a disabled element |
|
112 if (!this._isValidElement(aElement)) { |
|
113 let passiveButtons = { button: true, checkbox: true, file: true, radio: true, reset: true }; |
|
114 if ((aElement instanceof HTMLInputElement || aElement instanceof HTMLButtonElement) && |
|
115 passiveButtons[aElement.type] && !aElement.disabled) |
|
116 return false; |
|
117 return this.close(); |
|
118 } |
|
119 |
|
120 // Look for a top editable element |
|
121 if (this._isEditable(aElement)) { |
|
122 aElement = this._getTopLevelEditable(aElement); |
|
123 } |
|
124 |
|
125 // We only work with choice lists or elements with autocomplete suggestions |
|
126 if (!this._isSelectElement(aElement) && |
|
127 !this._isAutocomplete(aElement)) { |
|
128 return this.close(); |
|
129 } |
|
130 |
|
131 // Don't re-open when navigating to avoid repopulating list when changing selection. |
|
132 if (this._isAutocomplete(aElement) && this._open && Util.isNavigationKey(aEvent.keyCode)) { |
|
133 return false; |
|
134 } |
|
135 |
|
136 // Enable the assistant |
|
137 this.currentElement = aElement; |
|
138 return this._open = true; |
|
139 }, |
|
140 |
|
141 close: function close() { |
|
142 if (this._open) { |
|
143 this._currentElement = null; |
|
144 sendAsyncMessage("FormAssist:Hide", { }); |
|
145 this._open = false; |
|
146 } |
|
147 |
|
148 return this._open; |
|
149 }, |
|
150 |
|
151 receiveMessage: function receiveMessage(aMessage) { |
|
152 if (this._debugEvents) Util.dumpLn(aMessage.name); |
|
153 |
|
154 let currentElement = this.currentElement; |
|
155 if ((!this._isAutocomplete(currentElement) && |
|
156 !getWrapperForElement(currentElement)) || |
|
157 !currentElement) { |
|
158 return; |
|
159 } |
|
160 |
|
161 let json = aMessage.json; |
|
162 |
|
163 switch (aMessage.name) { |
|
164 case "FormAssist:ChoiceSelect": { |
|
165 this._selectWrapper = getWrapperForElement(currentElement); |
|
166 this._selectWrapper.select(json.index, json.selected); |
|
167 break; |
|
168 } |
|
169 |
|
170 case "FormAssist:ChoiceChange": { |
|
171 // ChoiceChange could happened once we have move to another element or |
|
172 // to nothing, so we should keep the used wrapper in mind. |
|
173 this._selectWrapper = getWrapperForElement(currentElement); |
|
174 this._selectWrapper.fireOnChange(); |
|
175 |
|
176 // New elements can be shown when a select is updated so we need to |
|
177 // reconstruct the inner elements array and to take care of possible |
|
178 // focus change, this is why we use "self.currentElement" instead of |
|
179 // using directly "currentElement". |
|
180 this._executeDelayed(function(self) { |
|
181 let currentElement = self.currentElement; |
|
182 if (!currentElement) |
|
183 return; |
|
184 self._currentElement = currentElement; |
|
185 }); |
|
186 break; |
|
187 } |
|
188 |
|
189 case "FormAssist:AutoComplete": { |
|
190 try { |
|
191 currentElement = currentElement.QueryInterface(Ci.nsIDOMNSEditableElement); |
|
192 let imeEditor = currentElement.editor.QueryInterface(Ci.nsIEditorIMESupport); |
|
193 if (imeEditor.composing) |
|
194 imeEditor.forceCompositionEnd(); |
|
195 } |
|
196 catch(e) {} |
|
197 |
|
198 currentElement.value = json.value; |
|
199 |
|
200 let event = currentElement.ownerDocument.createEvent("Events"); |
|
201 event.initEvent("DOMAutoComplete", true, true); |
|
202 currentElement.dispatchEvent(event); |
|
203 break; |
|
204 } |
|
205 |
|
206 case "FormAssist:Closed": |
|
207 currentElement.blur(); |
|
208 this._open = false; |
|
209 break; |
|
210 |
|
211 case "FormAssist:Update": |
|
212 this._sendJsonMsgWrapper("FormAssist:Show"); |
|
213 break; |
|
214 } |
|
215 }, |
|
216 |
|
217 handleEvent: function formHelperHandleEvent(aEvent) { |
|
218 if (this._debugEvents) Util.dumpLn(aEvent.type, this.currentElement); |
|
219 // focus changes should be taken into account only if the user has done a |
|
220 // manual operation like manually clicking |
|
221 let shouldIgnoreFocus = (aEvent.type == "focus" && !this._open && !this.focusSync); |
|
222 if ((!this._open && aEvent.type != "focus") || shouldIgnoreFocus) { |
|
223 return; |
|
224 } |
|
225 |
|
226 let currentElement = this.currentElement; |
|
227 switch (aEvent.type) { |
|
228 case "submit": |
|
229 // submit is a final action and the form assistant should be closed |
|
230 this.close(); |
|
231 break; |
|
232 |
|
233 case "pagehide": |
|
234 case "pageshow": |
|
235 // When reacting to a page show/hide, if the focus is different this |
|
236 // could mean the web page has dramatically changed because of |
|
237 // an Ajax change based on fragment identifier |
|
238 if (gFocusManager.focusedElement != currentElement) |
|
239 this.close(); |
|
240 break; |
|
241 |
|
242 case "focus": |
|
243 let focusedElement = |
|
244 gFocusManager.getFocusedElementForWindow(content, true, {}) || |
|
245 aEvent.target; |
|
246 |
|
247 // If a body element is editable and the body is the child of an |
|
248 // iframe we can assume this is an advanced HTML editor, so let's |
|
249 // redirect the form helper selection to the iframe element |
|
250 if (focusedElement && this._isEditable(focusedElement)) { |
|
251 let editableElement = this._getTopLevelEditable(focusedElement); |
|
252 if (this._isValidElement(editableElement)) { |
|
253 this._executeDelayed(function(self) { |
|
254 self.open(editableElement); |
|
255 }); |
|
256 } |
|
257 return; |
|
258 } |
|
259 |
|
260 // if an element is focused while we're closed but the element can be handle |
|
261 // by the assistant, try to activate it (only during mouseup) |
|
262 if (!currentElement) { |
|
263 if (focusedElement && this._isValidElement(focusedElement)) { |
|
264 this._executeDelayed(function(self) { |
|
265 self.open(focusedElement); |
|
266 }); |
|
267 } |
|
268 return; |
|
269 } |
|
270 |
|
271 if (this._currentElement != focusedElement) |
|
272 this.currentElement = focusedElement; |
|
273 break; |
|
274 |
|
275 case "blur": |
|
276 content.setTimeout(function(self) { |
|
277 if (!self._open) |
|
278 return; |
|
279 |
|
280 // If the blurring causes focus be in no other element, |
|
281 // we should close the form assistant. |
|
282 let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {}); |
|
283 if (!focusedElement) |
|
284 self.close(); |
|
285 }, 0, this); |
|
286 break; |
|
287 |
|
288 case "text": |
|
289 if (this._isAutocomplete(aEvent.target)) { |
|
290 this._sendJsonMsgWrapper("FormAssist:AutoComplete"); |
|
291 } |
|
292 break; |
|
293 } |
|
294 }, |
|
295 |
|
296 _executeDelayed: function formHelperExecuteSoon(aCallback) { |
|
297 let self = this; |
|
298 let timer = new Util.Timeout(function() { |
|
299 aCallback(self); |
|
300 }); |
|
301 timer.once(0); |
|
302 }, |
|
303 |
|
304 _isEditable: function formHelperIsEditable(aElement) { |
|
305 if (!aElement) |
|
306 return false; |
|
307 let canEdit = false; |
|
308 |
|
309 if (aElement.isContentEditable || aElement.designMode == "on") { |
|
310 canEdit = true; |
|
311 } else if (aElement instanceof HTMLIFrameElement && |
|
312 (aElement.contentDocument.body.isContentEditable || |
|
313 aElement.contentDocument.designMode == "on")) { |
|
314 canEdit = true; |
|
315 } else { |
|
316 canEdit = aElement.ownerDocument && aElement.ownerDocument.designMode == "on"; |
|
317 } |
|
318 |
|
319 return canEdit; |
|
320 }, |
|
321 |
|
322 _getTopLevelEditable: function formHelperGetTopLevelEditable(aElement) { |
|
323 if (!(aElement instanceof HTMLIFrameElement)) { |
|
324 let element = aElement; |
|
325 |
|
326 // Retrieve the top element that is editable |
|
327 if (element instanceof HTMLHtmlElement) |
|
328 element = element.ownerDocument.body; |
|
329 else if (element instanceof HTMLDocument) |
|
330 element = element.body; |
|
331 |
|
332 while (element && !this._isEditable(element)) |
|
333 element = element.parentNode; |
|
334 |
|
335 // Return the container frame if we are into a nested editable frame |
|
336 if (element && element instanceof HTMLBodyElement && element.ownerDocument.defaultView != content.document.defaultView) |
|
337 return element.ownerDocument.defaultView.frameElement; |
|
338 } |
|
339 |
|
340 return aElement; |
|
341 }, |
|
342 |
|
343 _isAutocomplete: function formHelperIsAutocomplete(aElement) { |
|
344 if (aElement instanceof HTMLInputElement) { |
|
345 if (aElement.getAttribute("type") == "password") |
|
346 return false; |
|
347 |
|
348 let autocomplete = aElement.getAttribute("autocomplete"); |
|
349 let allowedValues = ["off", "false", "disabled"]; |
|
350 if (allowedValues.indexOf(autocomplete) == -1) |
|
351 return true; |
|
352 } |
|
353 |
|
354 return false; |
|
355 }, |
|
356 |
|
357 /* |
|
358 * This function is similar to getListSuggestions from |
|
359 * components/satchel/src/nsInputListAutoComplete.js but sadly this one is |
|
360 * used by the autocomplete.xml binding which is not in used in fennec |
|
361 */ |
|
362 _getListSuggestions: function formHelperGetListSuggestions(aElement) { |
|
363 if (!(aElement instanceof HTMLInputElement) || !aElement.list) |
|
364 return []; |
|
365 |
|
366 let suggestions = []; |
|
367 let filter = !aElement.hasAttribute("mozNoFilter"); |
|
368 let lowerFieldValue = aElement.value.toLowerCase(); |
|
369 |
|
370 let options = aElement.list.options; |
|
371 let length = options.length; |
|
372 for (let i = 0; i < length; i++) { |
|
373 let item = options.item(i); |
|
374 |
|
375 let label = item.value; |
|
376 if (item.label) |
|
377 label = item.label; |
|
378 else if (item.text) |
|
379 label = item.text; |
|
380 |
|
381 if (filter && label.toLowerCase().indexOf(lowerFieldValue) == -1) |
|
382 continue; |
|
383 suggestions.push({ label: label, value: item.value }); |
|
384 } |
|
385 |
|
386 return suggestions; |
|
387 }, |
|
388 |
|
389 _isValidElement: function formHelperIsValidElement(aElement) { |
|
390 if (!aElement.getAttribute) |
|
391 return false; |
|
392 |
|
393 let formExceptions = { button: true, checkbox: true, file: true, image: true, radio: true, reset: true, submit: true }; |
|
394 if (aElement instanceof HTMLInputElement && formExceptions[aElement.type]) |
|
395 return false; |
|
396 |
|
397 if (aElement instanceof HTMLButtonElement || |
|
398 (aElement.getAttribute("role") == "button" && aElement.hasAttribute("tabindex"))) |
|
399 return false; |
|
400 |
|
401 return this._isNavigableElement(aElement) && this._isVisibleElement(aElement); |
|
402 }, |
|
403 |
|
404 _isNavigableElement: function formHelperIsNavigableElement(aElement) { |
|
405 if (aElement.disabled || aElement.getAttribute("tabindex") == "-1") |
|
406 return false; |
|
407 |
|
408 if (aElement.getAttribute("role") == "button" && aElement.hasAttribute("tabindex")) |
|
409 return true; |
|
410 |
|
411 if (this._isSelectElement(aElement) || aElement instanceof HTMLTextAreaElement) |
|
412 return true; |
|
413 |
|
414 if (aElement instanceof HTMLInputElement || aElement instanceof HTMLButtonElement) |
|
415 return !(aElement.type == "hidden"); |
|
416 |
|
417 return this._isEditable(aElement); |
|
418 }, |
|
419 |
|
420 _isVisibleElement: function formHelperIsVisibleElement(aElement) { |
|
421 if (!aElement || !aElement.ownerDocument) { |
|
422 return false; |
|
423 } |
|
424 let style = aElement.ownerDocument.defaultView.getComputedStyle(aElement, null); |
|
425 if (!style) |
|
426 return false; |
|
427 |
|
428 let isVisible = (style.getPropertyValue("visibility") != "hidden"); |
|
429 let isOpaque = (style.getPropertyValue("opacity") != 0); |
|
430 |
|
431 let rect = aElement.getBoundingClientRect(); |
|
432 |
|
433 // Since the only way to show a drop-down menu for a select when the form |
|
434 // assistant is enabled is to return true here, a select is allowed to have |
|
435 // an opacity to 0 in order to let web developpers add a custom design on |
|
436 // top of it. This is less important to use the form assistant for the |
|
437 // other types of fields because even if the form assistant won't fired, |
|
438 // the focus will be in and a VKB will popup if needed |
|
439 return isVisible && (isOpaque || this._isSelectElement(aElement)) && (rect.height != 0 || rect.width != 0); |
|
440 }, |
|
441 |
|
442 _isSelectElement: function formHelperIsSelectElement(aElement) { |
|
443 return (aElement instanceof HTMLSelectElement || aElement instanceof XULMenuListElement); |
|
444 }, |
|
445 |
|
446 /** Caret is used to input text for this element. */ |
|
447 _getCaretRect: function _formHelperGetCaretRect() { |
|
448 let element = this.currentElement; |
|
449 let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {}); |
|
450 if (element && (element.mozIsTextField && element.mozIsTextField(false) || |
|
451 element instanceof HTMLTextAreaElement) && focusedElement == element && this._isVisibleElement(element)) { |
|
452 let utils = Util.getWindowUtils(element.ownerDocument.defaultView); |
|
453 let rect = utils.sendQueryContentEvent(utils.QUERY_CARET_RECT, element.selectionEnd, 0, 0, 0, |
|
454 utils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK); |
|
455 if (rect) { |
|
456 let scroll = ContentScroll.getScrollOffset(element.ownerDocument.defaultView); |
|
457 return new Rect(scroll.x + rect.left, scroll.y + rect.top, rect.width, rect.height); |
|
458 } |
|
459 } |
|
460 |
|
461 return new Rect(0, 0, 0, 0); |
|
462 }, |
|
463 |
|
464 /** Gets a rect bounding important parts of the element that must be seen when assisting. */ |
|
465 _getRect: function _formHelperGetRect(aOptions={}) { |
|
466 const kDistanceMax = 100; |
|
467 let element = this.currentElement; |
|
468 let elRect = getBoundingContentRect(element); |
|
469 |
|
470 if (aOptions.alignToLabel) { |
|
471 let labels = this._getLabels(); |
|
472 for (let i=0; i<labels.length; i++) { |
|
473 let labelRect = labels[i].rect; |
|
474 if (labelRect.left < elRect.left) { |
|
475 let isClose = Math.abs(labelRect.left - elRect.left) - labelRect.width < kDistanceMax && |
|
476 Math.abs(labelRect.top - elRect.top) - labelRect.height < kDistanceMax; |
|
477 if (isClose) { |
|
478 let width = labelRect.width + elRect.width + (elRect.left - labelRect.left - labelRect.width); |
|
479 return new Rect(labelRect.left, labelRect.top, width, elRect.height).expandToIntegers(); |
|
480 } |
|
481 } |
|
482 } |
|
483 } |
|
484 return elRect; |
|
485 }, |
|
486 |
|
487 _getLabels: function formHelperGetLabels() { |
|
488 let associatedLabels = []; |
|
489 if (!this.currentElement) |
|
490 return associatedLabels; |
|
491 let element = this.currentElement; |
|
492 let labels = element.ownerDocument.getElementsByTagName("label"); |
|
493 for (let i=0; i<labels.length; i++) { |
|
494 let label = labels[i]; |
|
495 if ((label.control == element || label.getAttribute("for") == element.id) && this._isVisibleElement(label)) { |
|
496 associatedLabels.push({ |
|
497 rect: getBoundingContentRect(label), |
|
498 title: label.textContent |
|
499 }); |
|
500 } |
|
501 } |
|
502 |
|
503 return associatedLabels; |
|
504 }, |
|
505 |
|
506 _sendJsonMsgWrapper: function (aMsg) { |
|
507 let json = this._getJSON(); |
|
508 if (json) { |
|
509 sendAsyncMessage(aMsg, json); |
|
510 } |
|
511 }, |
|
512 |
|
513 _getJSON: function() { |
|
514 let element = this.currentElement; |
|
515 if (!element) { |
|
516 return null; |
|
517 } |
|
518 let choices = getListForElement(element); |
|
519 let editable = (element instanceof HTMLInputElement && element.mozIsTextField(false)) || this._isEditable(element); |
|
520 |
|
521 let labels = this._getLabels(); |
|
522 return { |
|
523 current: { |
|
524 id: element.id, |
|
525 name: element.name, |
|
526 title: labels.length ? labels[0].title : "", |
|
527 value: element.value, |
|
528 maxLength: element.maxLength, |
|
529 type: (element.getAttribute("type") || "").toLowerCase(), |
|
530 choices: choices, |
|
531 isAutocomplete: this._isAutocomplete(element), |
|
532 list: this._getListSuggestions(element), |
|
533 rect: this._getRect(), |
|
534 caretRect: this._getCaretRect(), |
|
535 editable: editable |
|
536 }, |
|
537 }; |
|
538 }, |
|
539 |
|
540 /** |
|
541 * For each radio button group, remove all but the checked button |
|
542 * if there is one, or the first button otherwise. |
|
543 */ |
|
544 _filterRadioButtons: function(aNodes) { |
|
545 // First pass: Find the checked or first element in each group. |
|
546 let chosenRadios = {}; |
|
547 for (let i=0; i < aNodes.length; i++) { |
|
548 let node = aNodes[i]; |
|
549 if (node.type == "radio" && (!chosenRadios.hasOwnProperty(node.name) || node.checked)) |
|
550 chosenRadios[node.name] = node; |
|
551 } |
|
552 |
|
553 // Second pass: Exclude all other radio buttons from the list. |
|
554 let result = []; |
|
555 for (let i=0; i < aNodes.length; i++) { |
|
556 let node = aNodes[i]; |
|
557 if (node.type == "radio" && chosenRadios[node.name] != node) |
|
558 continue; |
|
559 result.push(node); |
|
560 } |
|
561 return result; |
|
562 } |
|
563 }; |
|
564 this.FormAssistant = FormAssistant; |
|
565 |
|
566 |
|
567 /****************************************************************************** |
|
568 * The next classes wraps some forms elements such as different type of list to |
|
569 * abstract the difference between html and xul element while manipulating them |
|
570 * - SelectWrapper : <html:select> |
|
571 * - MenulistWrapper : <xul:menulist> |
|
572 *****************************************************************************/ |
|
573 |
|
574 function getWrapperForElement(aElement) { |
|
575 let wrapper = null; |
|
576 if (aElement instanceof HTMLSelectElement) { |
|
577 wrapper = new SelectWrapper(aElement); |
|
578 } |
|
579 else if (aElement instanceof XULMenuListElement) { |
|
580 wrapper = new MenulistWrapper(aElement); |
|
581 } |
|
582 |
|
583 return wrapper; |
|
584 } |
|
585 |
|
586 function getListForElement(aElement) { |
|
587 let wrapper = getWrapperForElement(aElement); |
|
588 if (!wrapper) |
|
589 return null; |
|
590 |
|
591 let optionIndex = 0; |
|
592 let result = { |
|
593 multiple: wrapper.getMultiple(), |
|
594 choices: [] |
|
595 }; |
|
596 |
|
597 // Build up a flat JSON array of the choices. In HTML, it's possible for select element choices |
|
598 // to be under a group header (but not recursively). We distinguish between headers and entries |
|
599 // using the boolean "list.group". |
|
600 // XXX If possible, this would be a great candidate for tracing. |
|
601 let children = wrapper.getChildren(); |
|
602 for (let i = 0; i < children.length; i++) { |
|
603 let child = children[i]; |
|
604 if (wrapper.isGroup(child)) { |
|
605 // This is the group element. Add an entry in the choices that says that the following |
|
606 // elements are a member of this group. |
|
607 result.choices.push({ group: true, |
|
608 text: child.label || child.firstChild.data, |
|
609 disabled: child.disabled |
|
610 }); |
|
611 let subchildren = child.children; |
|
612 for (let j = 0; j < subchildren.length; j++) { |
|
613 let subchild = subchildren[j]; |
|
614 result.choices.push({ |
|
615 group: false, |
|
616 inGroup: true, |
|
617 text: wrapper.getText(subchild), |
|
618 disabled: child.disabled || subchild.disabled, |
|
619 selected: subchild.selected, |
|
620 optionIndex: optionIndex++ |
|
621 }); |
|
622 } |
|
623 } |
|
624 else if (wrapper.isOption(child)) { |
|
625 // This is a regular choice under no group. |
|
626 result.choices.push({ |
|
627 group: false, |
|
628 inGroup: false, |
|
629 text: wrapper.getText(child), |
|
630 disabled: child.disabled, |
|
631 selected: child.selected, |
|
632 optionIndex: optionIndex++ |
|
633 }); |
|
634 } |
|
635 } |
|
636 |
|
637 return result; |
|
638 } |
|
639 |
|
640 |
|
641 function SelectWrapper(aControl) { |
|
642 this._control = aControl; |
|
643 } |
|
644 |
|
645 SelectWrapper.prototype = { |
|
646 getSelectedIndex: function() { |
|
647 return this._control.selectedIndex; |
|
648 }, |
|
649 |
|
650 getMultiple: function() { |
|
651 return this._control.multiple; |
|
652 }, |
|
653 |
|
654 getOptions: function() { |
|
655 return this._control.options; |
|
656 }, |
|
657 |
|
658 getChildren: function() { |
|
659 return this._control.children; |
|
660 }, |
|
661 |
|
662 getText: function(aChild) { |
|
663 return aChild.text; |
|
664 }, |
|
665 |
|
666 isOption: function(aChild) { |
|
667 return aChild instanceof HTMLOptionElement; |
|
668 }, |
|
669 |
|
670 isGroup: function(aChild) { |
|
671 return aChild instanceof HTMLOptGroupElement; |
|
672 }, |
|
673 |
|
674 select: function(aIndex, aSelected) { |
|
675 let options = this._control.options; |
|
676 if (this.getMultiple()) |
|
677 options[aIndex].selected = aSelected; |
|
678 else |
|
679 options.selectedIndex = aIndex; |
|
680 }, |
|
681 |
|
682 fireOnChange: function() { |
|
683 let control = this._control; |
|
684 let evt = this._control.ownerDocument.createEvent("Events"); |
|
685 evt.initEvent("change", true, true, this._control.ownerDocument.defaultView, 0, |
|
686 false, false, |
|
687 false, false, null); |
|
688 content.setTimeout(function() { |
|
689 control.dispatchEvent(evt); |
|
690 }, 0); |
|
691 } |
|
692 }; |
|
693 this.SelectWrapper = SelectWrapper; |
|
694 |
|
695 |
|
696 // bug 559792 |
|
697 // Use wrappedJSObject when control is in content for extra protection |
|
698 function MenulistWrapper(aControl) { |
|
699 this._control = aControl; |
|
700 } |
|
701 |
|
702 MenulistWrapper.prototype = { |
|
703 getSelectedIndex: function() { |
|
704 let control = this._control.wrappedJSObject || this._control; |
|
705 let result = control.selectedIndex; |
|
706 return ((typeof result == "number" && !isNaN(result)) ? result : -1); |
|
707 }, |
|
708 |
|
709 getMultiple: function() { |
|
710 return false; |
|
711 }, |
|
712 |
|
713 getOptions: function() { |
|
714 let control = this._control.wrappedJSObject || this._control; |
|
715 return control.menupopup.children; |
|
716 }, |
|
717 |
|
718 getChildren: function() { |
|
719 let control = this._control.wrappedJSObject || this._control; |
|
720 return control.menupopup.children; |
|
721 }, |
|
722 |
|
723 getText: function(aChild) { |
|
724 return aChild.label; |
|
725 }, |
|
726 |
|
727 isOption: function(aChild) { |
|
728 return aChild instanceof Ci.nsIDOMXULSelectControlItemElement; |
|
729 }, |
|
730 |
|
731 isGroup: function(aChild) { |
|
732 return false; |
|
733 }, |
|
734 |
|
735 select: function(aIndex, aSelected) { |
|
736 let control = this._control.wrappedJSObject || this._control; |
|
737 control.selectedIndex = aIndex; |
|
738 }, |
|
739 |
|
740 fireOnChange: function() { |
|
741 let control = this._control; |
|
742 let evt = document.createEvent("XULCommandEvent"); |
|
743 evt.initCommandEvent("command", true, true, window, 0, |
|
744 false, false, |
|
745 false, false, null); |
|
746 content.setTimeout(function() { |
|
747 control.dispatchEvent(evt); |
|
748 }, 0); |
|
749 } |
|
750 }; |
|
751 this.MenulistWrapper = MenulistWrapper; |