1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/base/content/contenthandlers/FormHelper.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,751 @@ 1.4 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +let Ci = Components.interfaces; 1.10 +let Cc = Components.classes; 1.11 + 1.12 +dump("### FormHelper.js loaded\n"); 1.13 + 1.14 +let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement; 1.15 +let HTMLInputElement = Ci.nsIDOMHTMLInputElement; 1.16 +let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; 1.17 +let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; 1.18 +let HTMLDocument = Ci.nsIDOMHTMLDocument; 1.19 +let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement; 1.20 +let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement; 1.21 +let HTMLLabelElement = Ci.nsIDOMHTMLLabelElement; 1.22 +let HTMLButtonElement = Ci.nsIDOMHTMLButtonElement; 1.23 +let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement; 1.24 +let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement; 1.25 +let XULMenuListElement = Ci.nsIDOMXULMenuListElement; 1.26 + 1.27 +/** 1.28 + * Responsible of navigation between forms fields and of the opening of the assistant 1.29 + */ 1.30 +function FormAssistant() { 1.31 + addMessageListener("FormAssist:Closed", this); 1.32 + addMessageListener("FormAssist:ChoiceSelect", this); 1.33 + addMessageListener("FormAssist:ChoiceChange", this); 1.34 + addMessageListener("FormAssist:AutoComplete", this); 1.35 + addMessageListener("FormAssist:Update", this); 1.36 + 1.37 + /* Listen text events in order to update the autocomplete suggestions as soon 1.38 + * a key is entered on device 1.39 + */ 1.40 + addEventListener("text", this, false); 1.41 + addEventListener("focus", this, true); 1.42 + addEventListener("blur", this, true); 1.43 + addEventListener("pageshow", this, false); 1.44 + addEventListener("pagehide", this, false); 1.45 + addEventListener("submit", this, false); 1.46 +} 1.47 + 1.48 +FormAssistant.prototype = { 1.49 + _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService), 1.50 + _open: false, 1.51 + _focusSync: false, 1.52 + _debugEvents: false, 1.53 + _selectWrapper: null, 1.54 + _currentElement: null, 1.55 + invalidSubmit: false, 1.56 + 1.57 + get focusSync() { 1.58 + return this._focusSync; 1.59 + }, 1.60 + 1.61 + set focusSync(aVal) { 1.62 + this._focusSync = aVal; 1.63 + }, 1.64 + 1.65 + get currentElement() { 1.66 + return this._currentElement; 1.67 + }, 1.68 + 1.69 + set currentElement(aElement) { 1.70 + if (!aElement || !this._isVisibleElement(aElement)) { 1.71 + return null; 1.72 + } 1.73 + 1.74 + this._currentElement = aElement; 1.75 + gFocusManager.setFocus(this._currentElement, Ci.nsIFocusManager.FLAG_NOSCROLL); 1.76 + 1.77 + // To ensure we get the current caret positionning of the focused 1.78 + // element we need to delayed a bit the event 1.79 + this._executeDelayed(function(self) { 1.80 + // Bug 640870 1.81 + // Sometimes the element inner frame get destroyed while the element 1.82 + // receive the focus because the display is turned to 'none' for 1.83 + // example, in this "fun" case just do nothing if the element is hidden 1.84 + if (self._isVisibleElement(gFocusManager.focusedElement)) { 1.85 + self._sendJsonMsgWrapper("FormAssist:Show"); 1.86 + } 1.87 + }); 1.88 + return this._currentElement; 1.89 + }, 1.90 + 1.91 + open: function formHelperOpen(aElement, aEvent) { 1.92 + // If the click is on an option element we want to check if the parent 1.93 + // is a valid target. 1.94 + if (aElement instanceof HTMLOptionElement && 1.95 + aElement.parentNode instanceof HTMLSelectElement && 1.96 + !aElement.disabled) { 1.97 + aElement = aElement.parentNode; 1.98 + } 1.99 + 1.100 + // Don't show the formhelper popup for multi-select boxes, except for touch. 1.101 + if (aElement instanceof HTMLSelectElement && aEvent) { 1.102 + if ((aElement.multiple || aElement.size > 1) && 1.103 + aEvent.mozInputSource != Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) { 1.104 + return false; 1.105 + } 1.106 + // Don't fire mouse events on selects; see bug 685197. 1.107 + aEvent.preventDefault(); 1.108 + aEvent.stopPropagation(); 1.109 + } 1.110 + 1.111 + // The form assistant will close if a click happen: 1.112 + // * outside of the scope of the form helper 1.113 + // * hover a button of type=[image|submit] 1.114 + // * hover a disabled element 1.115 + if (!this._isValidElement(aElement)) { 1.116 + let passiveButtons = { button: true, checkbox: true, file: true, radio: true, reset: true }; 1.117 + if ((aElement instanceof HTMLInputElement || aElement instanceof HTMLButtonElement) && 1.118 + passiveButtons[aElement.type] && !aElement.disabled) 1.119 + return false; 1.120 + return this.close(); 1.121 + } 1.122 + 1.123 + // Look for a top editable element 1.124 + if (this._isEditable(aElement)) { 1.125 + aElement = this._getTopLevelEditable(aElement); 1.126 + } 1.127 + 1.128 + // We only work with choice lists or elements with autocomplete suggestions 1.129 + if (!this._isSelectElement(aElement) && 1.130 + !this._isAutocomplete(aElement)) { 1.131 + return this.close(); 1.132 + } 1.133 + 1.134 + // Don't re-open when navigating to avoid repopulating list when changing selection. 1.135 + if (this._isAutocomplete(aElement) && this._open && Util.isNavigationKey(aEvent.keyCode)) { 1.136 + return false; 1.137 + } 1.138 + 1.139 + // Enable the assistant 1.140 + this.currentElement = aElement; 1.141 + return this._open = true; 1.142 + }, 1.143 + 1.144 + close: function close() { 1.145 + if (this._open) { 1.146 + this._currentElement = null; 1.147 + sendAsyncMessage("FormAssist:Hide", { }); 1.148 + this._open = false; 1.149 + } 1.150 + 1.151 + return this._open; 1.152 + }, 1.153 + 1.154 + receiveMessage: function receiveMessage(aMessage) { 1.155 + if (this._debugEvents) Util.dumpLn(aMessage.name); 1.156 + 1.157 + let currentElement = this.currentElement; 1.158 + if ((!this._isAutocomplete(currentElement) && 1.159 + !getWrapperForElement(currentElement)) || 1.160 + !currentElement) { 1.161 + return; 1.162 + } 1.163 + 1.164 + let json = aMessage.json; 1.165 + 1.166 + switch (aMessage.name) { 1.167 + case "FormAssist:ChoiceSelect": { 1.168 + this._selectWrapper = getWrapperForElement(currentElement); 1.169 + this._selectWrapper.select(json.index, json.selected); 1.170 + break; 1.171 + } 1.172 + 1.173 + case "FormAssist:ChoiceChange": { 1.174 + // ChoiceChange could happened once we have move to another element or 1.175 + // to nothing, so we should keep the used wrapper in mind. 1.176 + this._selectWrapper = getWrapperForElement(currentElement); 1.177 + this._selectWrapper.fireOnChange(); 1.178 + 1.179 + // New elements can be shown when a select is updated so we need to 1.180 + // reconstruct the inner elements array and to take care of possible 1.181 + // focus change, this is why we use "self.currentElement" instead of 1.182 + // using directly "currentElement". 1.183 + this._executeDelayed(function(self) { 1.184 + let currentElement = self.currentElement; 1.185 + if (!currentElement) 1.186 + return; 1.187 + self._currentElement = currentElement; 1.188 + }); 1.189 + break; 1.190 + } 1.191 + 1.192 + case "FormAssist:AutoComplete": { 1.193 + try { 1.194 + currentElement = currentElement.QueryInterface(Ci.nsIDOMNSEditableElement); 1.195 + let imeEditor = currentElement.editor.QueryInterface(Ci.nsIEditorIMESupport); 1.196 + if (imeEditor.composing) 1.197 + imeEditor.forceCompositionEnd(); 1.198 + } 1.199 + catch(e) {} 1.200 + 1.201 + currentElement.value = json.value; 1.202 + 1.203 + let event = currentElement.ownerDocument.createEvent("Events"); 1.204 + event.initEvent("DOMAutoComplete", true, true); 1.205 + currentElement.dispatchEvent(event); 1.206 + break; 1.207 + } 1.208 + 1.209 + case "FormAssist:Closed": 1.210 + currentElement.blur(); 1.211 + this._open = false; 1.212 + break; 1.213 + 1.214 + case "FormAssist:Update": 1.215 + this._sendJsonMsgWrapper("FormAssist:Show"); 1.216 + break; 1.217 + } 1.218 + }, 1.219 + 1.220 + handleEvent: function formHelperHandleEvent(aEvent) { 1.221 + if (this._debugEvents) Util.dumpLn(aEvent.type, this.currentElement); 1.222 + // focus changes should be taken into account only if the user has done a 1.223 + // manual operation like manually clicking 1.224 + let shouldIgnoreFocus = (aEvent.type == "focus" && !this._open && !this.focusSync); 1.225 + if ((!this._open && aEvent.type != "focus") || shouldIgnoreFocus) { 1.226 + return; 1.227 + } 1.228 + 1.229 + let currentElement = this.currentElement; 1.230 + switch (aEvent.type) { 1.231 + case "submit": 1.232 + // submit is a final action and the form assistant should be closed 1.233 + this.close(); 1.234 + break; 1.235 + 1.236 + case "pagehide": 1.237 + case "pageshow": 1.238 + // When reacting to a page show/hide, if the focus is different this 1.239 + // could mean the web page has dramatically changed because of 1.240 + // an Ajax change based on fragment identifier 1.241 + if (gFocusManager.focusedElement != currentElement) 1.242 + this.close(); 1.243 + break; 1.244 + 1.245 + case "focus": 1.246 + let focusedElement = 1.247 + gFocusManager.getFocusedElementForWindow(content, true, {}) || 1.248 + aEvent.target; 1.249 + 1.250 + // If a body element is editable and the body is the child of an 1.251 + // iframe we can assume this is an advanced HTML editor, so let's 1.252 + // redirect the form helper selection to the iframe element 1.253 + if (focusedElement && this._isEditable(focusedElement)) { 1.254 + let editableElement = this._getTopLevelEditable(focusedElement); 1.255 + if (this._isValidElement(editableElement)) { 1.256 + this._executeDelayed(function(self) { 1.257 + self.open(editableElement); 1.258 + }); 1.259 + } 1.260 + return; 1.261 + } 1.262 + 1.263 + // if an element is focused while we're closed but the element can be handle 1.264 + // by the assistant, try to activate it (only during mouseup) 1.265 + if (!currentElement) { 1.266 + if (focusedElement && this._isValidElement(focusedElement)) { 1.267 + this._executeDelayed(function(self) { 1.268 + self.open(focusedElement); 1.269 + }); 1.270 + } 1.271 + return; 1.272 + } 1.273 + 1.274 + if (this._currentElement != focusedElement) 1.275 + this.currentElement = focusedElement; 1.276 + break; 1.277 + 1.278 + case "blur": 1.279 + content.setTimeout(function(self) { 1.280 + if (!self._open) 1.281 + return; 1.282 + 1.283 + // If the blurring causes focus be in no other element, 1.284 + // we should close the form assistant. 1.285 + let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {}); 1.286 + if (!focusedElement) 1.287 + self.close(); 1.288 + }, 0, this); 1.289 + break; 1.290 + 1.291 + case "text": 1.292 + if (this._isAutocomplete(aEvent.target)) { 1.293 + this._sendJsonMsgWrapper("FormAssist:AutoComplete"); 1.294 + } 1.295 + break; 1.296 + } 1.297 + }, 1.298 + 1.299 + _executeDelayed: function formHelperExecuteSoon(aCallback) { 1.300 + let self = this; 1.301 + let timer = new Util.Timeout(function() { 1.302 + aCallback(self); 1.303 + }); 1.304 + timer.once(0); 1.305 + }, 1.306 + 1.307 + _isEditable: function formHelperIsEditable(aElement) { 1.308 + if (!aElement) 1.309 + return false; 1.310 + let canEdit = false; 1.311 + 1.312 + if (aElement.isContentEditable || aElement.designMode == "on") { 1.313 + canEdit = true; 1.314 + } else if (aElement instanceof HTMLIFrameElement && 1.315 + (aElement.contentDocument.body.isContentEditable || 1.316 + aElement.contentDocument.designMode == "on")) { 1.317 + canEdit = true; 1.318 + } else { 1.319 + canEdit = aElement.ownerDocument && aElement.ownerDocument.designMode == "on"; 1.320 + } 1.321 + 1.322 + return canEdit; 1.323 + }, 1.324 + 1.325 + _getTopLevelEditable: function formHelperGetTopLevelEditable(aElement) { 1.326 + if (!(aElement instanceof HTMLIFrameElement)) { 1.327 + let element = aElement; 1.328 + 1.329 + // Retrieve the top element that is editable 1.330 + if (element instanceof HTMLHtmlElement) 1.331 + element = element.ownerDocument.body; 1.332 + else if (element instanceof HTMLDocument) 1.333 + element = element.body; 1.334 + 1.335 + while (element && !this._isEditable(element)) 1.336 + element = element.parentNode; 1.337 + 1.338 + // Return the container frame if we are into a nested editable frame 1.339 + if (element && element instanceof HTMLBodyElement && element.ownerDocument.defaultView != content.document.defaultView) 1.340 + return element.ownerDocument.defaultView.frameElement; 1.341 + } 1.342 + 1.343 + return aElement; 1.344 + }, 1.345 + 1.346 + _isAutocomplete: function formHelperIsAutocomplete(aElement) { 1.347 + if (aElement instanceof HTMLInputElement) { 1.348 + if (aElement.getAttribute("type") == "password") 1.349 + return false; 1.350 + 1.351 + let autocomplete = aElement.getAttribute("autocomplete"); 1.352 + let allowedValues = ["off", "false", "disabled"]; 1.353 + if (allowedValues.indexOf(autocomplete) == -1) 1.354 + return true; 1.355 + } 1.356 + 1.357 + return false; 1.358 + }, 1.359 + 1.360 + /* 1.361 + * This function is similar to getListSuggestions from 1.362 + * components/satchel/src/nsInputListAutoComplete.js but sadly this one is 1.363 + * used by the autocomplete.xml binding which is not in used in fennec 1.364 + */ 1.365 + _getListSuggestions: function formHelperGetListSuggestions(aElement) { 1.366 + if (!(aElement instanceof HTMLInputElement) || !aElement.list) 1.367 + return []; 1.368 + 1.369 + let suggestions = []; 1.370 + let filter = !aElement.hasAttribute("mozNoFilter"); 1.371 + let lowerFieldValue = aElement.value.toLowerCase(); 1.372 + 1.373 + let options = aElement.list.options; 1.374 + let length = options.length; 1.375 + for (let i = 0; i < length; i++) { 1.376 + let item = options.item(i); 1.377 + 1.378 + let label = item.value; 1.379 + if (item.label) 1.380 + label = item.label; 1.381 + else if (item.text) 1.382 + label = item.text; 1.383 + 1.384 + if (filter && label.toLowerCase().indexOf(lowerFieldValue) == -1) 1.385 + continue; 1.386 + suggestions.push({ label: label, value: item.value }); 1.387 + } 1.388 + 1.389 + return suggestions; 1.390 + }, 1.391 + 1.392 + _isValidElement: function formHelperIsValidElement(aElement) { 1.393 + if (!aElement.getAttribute) 1.394 + return false; 1.395 + 1.396 + let formExceptions = { button: true, checkbox: true, file: true, image: true, radio: true, reset: true, submit: true }; 1.397 + if (aElement instanceof HTMLInputElement && formExceptions[aElement.type]) 1.398 + return false; 1.399 + 1.400 + if (aElement instanceof HTMLButtonElement || 1.401 + (aElement.getAttribute("role") == "button" && aElement.hasAttribute("tabindex"))) 1.402 + return false; 1.403 + 1.404 + return this._isNavigableElement(aElement) && this._isVisibleElement(aElement); 1.405 + }, 1.406 + 1.407 + _isNavigableElement: function formHelperIsNavigableElement(aElement) { 1.408 + if (aElement.disabled || aElement.getAttribute("tabindex") == "-1") 1.409 + return false; 1.410 + 1.411 + if (aElement.getAttribute("role") == "button" && aElement.hasAttribute("tabindex")) 1.412 + return true; 1.413 + 1.414 + if (this._isSelectElement(aElement) || aElement instanceof HTMLTextAreaElement) 1.415 + return true; 1.416 + 1.417 + if (aElement instanceof HTMLInputElement || aElement instanceof HTMLButtonElement) 1.418 + return !(aElement.type == "hidden"); 1.419 + 1.420 + return this._isEditable(aElement); 1.421 + }, 1.422 + 1.423 + _isVisibleElement: function formHelperIsVisibleElement(aElement) { 1.424 + if (!aElement || !aElement.ownerDocument) { 1.425 + return false; 1.426 + } 1.427 + let style = aElement.ownerDocument.defaultView.getComputedStyle(aElement, null); 1.428 + if (!style) 1.429 + return false; 1.430 + 1.431 + let isVisible = (style.getPropertyValue("visibility") != "hidden"); 1.432 + let isOpaque = (style.getPropertyValue("opacity") != 0); 1.433 + 1.434 + let rect = aElement.getBoundingClientRect(); 1.435 + 1.436 + // Since the only way to show a drop-down menu for a select when the form 1.437 + // assistant is enabled is to return true here, a select is allowed to have 1.438 + // an opacity to 0 in order to let web developpers add a custom design on 1.439 + // top of it. This is less important to use the form assistant for the 1.440 + // other types of fields because even if the form assistant won't fired, 1.441 + // the focus will be in and a VKB will popup if needed 1.442 + return isVisible && (isOpaque || this._isSelectElement(aElement)) && (rect.height != 0 || rect.width != 0); 1.443 + }, 1.444 + 1.445 + _isSelectElement: function formHelperIsSelectElement(aElement) { 1.446 + return (aElement instanceof HTMLSelectElement || aElement instanceof XULMenuListElement); 1.447 + }, 1.448 + 1.449 + /** Caret is used to input text for this element. */ 1.450 + _getCaretRect: function _formHelperGetCaretRect() { 1.451 + let element = this.currentElement; 1.452 + let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {}); 1.453 + if (element && (element.mozIsTextField && element.mozIsTextField(false) || 1.454 + element instanceof HTMLTextAreaElement) && focusedElement == element && this._isVisibleElement(element)) { 1.455 + let utils = Util.getWindowUtils(element.ownerDocument.defaultView); 1.456 + let rect = utils.sendQueryContentEvent(utils.QUERY_CARET_RECT, element.selectionEnd, 0, 0, 0, 1.457 + utils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK); 1.458 + if (rect) { 1.459 + let scroll = ContentScroll.getScrollOffset(element.ownerDocument.defaultView); 1.460 + return new Rect(scroll.x + rect.left, scroll.y + rect.top, rect.width, rect.height); 1.461 + } 1.462 + } 1.463 + 1.464 + return new Rect(0, 0, 0, 0); 1.465 + }, 1.466 + 1.467 + /** Gets a rect bounding important parts of the element that must be seen when assisting. */ 1.468 + _getRect: function _formHelperGetRect(aOptions={}) { 1.469 + const kDistanceMax = 100; 1.470 + let element = this.currentElement; 1.471 + let elRect = getBoundingContentRect(element); 1.472 + 1.473 + if (aOptions.alignToLabel) { 1.474 + let labels = this._getLabels(); 1.475 + for (let i=0; i<labels.length; i++) { 1.476 + let labelRect = labels[i].rect; 1.477 + if (labelRect.left < elRect.left) { 1.478 + let isClose = Math.abs(labelRect.left - elRect.left) - labelRect.width < kDistanceMax && 1.479 + Math.abs(labelRect.top - elRect.top) - labelRect.height < kDistanceMax; 1.480 + if (isClose) { 1.481 + let width = labelRect.width + elRect.width + (elRect.left - labelRect.left - labelRect.width); 1.482 + return new Rect(labelRect.left, labelRect.top, width, elRect.height).expandToIntegers(); 1.483 + } 1.484 + } 1.485 + } 1.486 + } 1.487 + return elRect; 1.488 + }, 1.489 + 1.490 + _getLabels: function formHelperGetLabels() { 1.491 + let associatedLabels = []; 1.492 + if (!this.currentElement) 1.493 + return associatedLabels; 1.494 + let element = this.currentElement; 1.495 + let labels = element.ownerDocument.getElementsByTagName("label"); 1.496 + for (let i=0; i<labels.length; i++) { 1.497 + let label = labels[i]; 1.498 + if ((label.control == element || label.getAttribute("for") == element.id) && this._isVisibleElement(label)) { 1.499 + associatedLabels.push({ 1.500 + rect: getBoundingContentRect(label), 1.501 + title: label.textContent 1.502 + }); 1.503 + } 1.504 + } 1.505 + 1.506 + return associatedLabels; 1.507 + }, 1.508 + 1.509 + _sendJsonMsgWrapper: function (aMsg) { 1.510 + let json = this._getJSON(); 1.511 + if (json) { 1.512 + sendAsyncMessage(aMsg, json); 1.513 + } 1.514 + }, 1.515 + 1.516 + _getJSON: function() { 1.517 + let element = this.currentElement; 1.518 + if (!element) { 1.519 + return null; 1.520 + } 1.521 + let choices = getListForElement(element); 1.522 + let editable = (element instanceof HTMLInputElement && element.mozIsTextField(false)) || this._isEditable(element); 1.523 + 1.524 + let labels = this._getLabels(); 1.525 + return { 1.526 + current: { 1.527 + id: element.id, 1.528 + name: element.name, 1.529 + title: labels.length ? labels[0].title : "", 1.530 + value: element.value, 1.531 + maxLength: element.maxLength, 1.532 + type: (element.getAttribute("type") || "").toLowerCase(), 1.533 + choices: choices, 1.534 + isAutocomplete: this._isAutocomplete(element), 1.535 + list: this._getListSuggestions(element), 1.536 + rect: this._getRect(), 1.537 + caretRect: this._getCaretRect(), 1.538 + editable: editable 1.539 + }, 1.540 + }; 1.541 + }, 1.542 + 1.543 + /** 1.544 + * For each radio button group, remove all but the checked button 1.545 + * if there is one, or the first button otherwise. 1.546 + */ 1.547 + _filterRadioButtons: function(aNodes) { 1.548 + // First pass: Find the checked or first element in each group. 1.549 + let chosenRadios = {}; 1.550 + for (let i=0; i < aNodes.length; i++) { 1.551 + let node = aNodes[i]; 1.552 + if (node.type == "radio" && (!chosenRadios.hasOwnProperty(node.name) || node.checked)) 1.553 + chosenRadios[node.name] = node; 1.554 + } 1.555 + 1.556 + // Second pass: Exclude all other radio buttons from the list. 1.557 + let result = []; 1.558 + for (let i=0; i < aNodes.length; i++) { 1.559 + let node = aNodes[i]; 1.560 + if (node.type == "radio" && chosenRadios[node.name] != node) 1.561 + continue; 1.562 + result.push(node); 1.563 + } 1.564 + return result; 1.565 + } 1.566 +}; 1.567 +this.FormAssistant = FormAssistant; 1.568 + 1.569 + 1.570 +/****************************************************************************** 1.571 + * The next classes wraps some forms elements such as different type of list to 1.572 + * abstract the difference between html and xul element while manipulating them 1.573 + * - SelectWrapper : <html:select> 1.574 + * - MenulistWrapper : <xul:menulist> 1.575 + *****************************************************************************/ 1.576 + 1.577 +function getWrapperForElement(aElement) { 1.578 + let wrapper = null; 1.579 + if (aElement instanceof HTMLSelectElement) { 1.580 + wrapper = new SelectWrapper(aElement); 1.581 + } 1.582 + else if (aElement instanceof XULMenuListElement) { 1.583 + wrapper = new MenulistWrapper(aElement); 1.584 + } 1.585 + 1.586 + return wrapper; 1.587 +} 1.588 + 1.589 +function getListForElement(aElement) { 1.590 + let wrapper = getWrapperForElement(aElement); 1.591 + if (!wrapper) 1.592 + return null; 1.593 + 1.594 + let optionIndex = 0; 1.595 + let result = { 1.596 + multiple: wrapper.getMultiple(), 1.597 + choices: [] 1.598 + }; 1.599 + 1.600 + // Build up a flat JSON array of the choices. In HTML, it's possible for select element choices 1.601 + // to be under a group header (but not recursively). We distinguish between headers and entries 1.602 + // using the boolean "list.group". 1.603 + // XXX If possible, this would be a great candidate for tracing. 1.604 + let children = wrapper.getChildren(); 1.605 + for (let i = 0; i < children.length; i++) { 1.606 + let child = children[i]; 1.607 + if (wrapper.isGroup(child)) { 1.608 + // This is the group element. Add an entry in the choices that says that the following 1.609 + // elements are a member of this group. 1.610 + result.choices.push({ group: true, 1.611 + text: child.label || child.firstChild.data, 1.612 + disabled: child.disabled 1.613 + }); 1.614 + let subchildren = child.children; 1.615 + for (let j = 0; j < subchildren.length; j++) { 1.616 + let subchild = subchildren[j]; 1.617 + result.choices.push({ 1.618 + group: false, 1.619 + inGroup: true, 1.620 + text: wrapper.getText(subchild), 1.621 + disabled: child.disabled || subchild.disabled, 1.622 + selected: subchild.selected, 1.623 + optionIndex: optionIndex++ 1.624 + }); 1.625 + } 1.626 + } 1.627 + else if (wrapper.isOption(child)) { 1.628 + // This is a regular choice under no group. 1.629 + result.choices.push({ 1.630 + group: false, 1.631 + inGroup: false, 1.632 + text: wrapper.getText(child), 1.633 + disabled: child.disabled, 1.634 + selected: child.selected, 1.635 + optionIndex: optionIndex++ 1.636 + }); 1.637 + } 1.638 + } 1.639 + 1.640 + return result; 1.641 +} 1.642 + 1.643 + 1.644 +function SelectWrapper(aControl) { 1.645 + this._control = aControl; 1.646 +} 1.647 + 1.648 +SelectWrapper.prototype = { 1.649 + getSelectedIndex: function() { 1.650 + return this._control.selectedIndex; 1.651 + }, 1.652 + 1.653 + getMultiple: function() { 1.654 + return this._control.multiple; 1.655 + }, 1.656 + 1.657 + getOptions: function() { 1.658 + return this._control.options; 1.659 + }, 1.660 + 1.661 + getChildren: function() { 1.662 + return this._control.children; 1.663 + }, 1.664 + 1.665 + getText: function(aChild) { 1.666 + return aChild.text; 1.667 + }, 1.668 + 1.669 + isOption: function(aChild) { 1.670 + return aChild instanceof HTMLOptionElement; 1.671 + }, 1.672 + 1.673 + isGroup: function(aChild) { 1.674 + return aChild instanceof HTMLOptGroupElement; 1.675 + }, 1.676 + 1.677 + select: function(aIndex, aSelected) { 1.678 + let options = this._control.options; 1.679 + if (this.getMultiple()) 1.680 + options[aIndex].selected = aSelected; 1.681 + else 1.682 + options.selectedIndex = aIndex; 1.683 + }, 1.684 + 1.685 + fireOnChange: function() { 1.686 + let control = this._control; 1.687 + let evt = this._control.ownerDocument.createEvent("Events"); 1.688 + evt.initEvent("change", true, true, this._control.ownerDocument.defaultView, 0, 1.689 + false, false, 1.690 + false, false, null); 1.691 + content.setTimeout(function() { 1.692 + control.dispatchEvent(evt); 1.693 + }, 0); 1.694 + } 1.695 +}; 1.696 +this.SelectWrapper = SelectWrapper; 1.697 + 1.698 + 1.699 +// bug 559792 1.700 +// Use wrappedJSObject when control is in content for extra protection 1.701 +function MenulistWrapper(aControl) { 1.702 + this._control = aControl; 1.703 +} 1.704 + 1.705 +MenulistWrapper.prototype = { 1.706 + getSelectedIndex: function() { 1.707 + let control = this._control.wrappedJSObject || this._control; 1.708 + let result = control.selectedIndex; 1.709 + return ((typeof result == "number" && !isNaN(result)) ? result : -1); 1.710 + }, 1.711 + 1.712 + getMultiple: function() { 1.713 + return false; 1.714 + }, 1.715 + 1.716 + getOptions: function() { 1.717 + let control = this._control.wrappedJSObject || this._control; 1.718 + return control.menupopup.children; 1.719 + }, 1.720 + 1.721 + getChildren: function() { 1.722 + let control = this._control.wrappedJSObject || this._control; 1.723 + return control.menupopup.children; 1.724 + }, 1.725 + 1.726 + getText: function(aChild) { 1.727 + return aChild.label; 1.728 + }, 1.729 + 1.730 + isOption: function(aChild) { 1.731 + return aChild instanceof Ci.nsIDOMXULSelectControlItemElement; 1.732 + }, 1.733 + 1.734 + isGroup: function(aChild) { 1.735 + return false; 1.736 + }, 1.737 + 1.738 + select: function(aIndex, aSelected) { 1.739 + let control = this._control.wrappedJSObject || this._control; 1.740 + control.selectedIndex = aIndex; 1.741 + }, 1.742 + 1.743 + fireOnChange: function() { 1.744 + let control = this._control; 1.745 + let evt = document.createEvent("XULCommandEvent"); 1.746 + evt.initCommandEvent("command", true, true, window, 0, 1.747 + false, false, 1.748 + false, false, null); 1.749 + content.setTimeout(function() { 1.750 + control.dispatchEvent(evt); 1.751 + }, 0); 1.752 + } 1.753 +}; 1.754 +this.MenulistWrapper = MenulistWrapper;