browser/metro/base/content/contenthandlers/FormHelper.js

changeset 0
6474c204b198
     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;

mercurial