Raw File
FormValidationChild.jsm
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

/**
 * Handles the validation callback from nsIFormFillController and
 * the display of the help panel on invalid elements.
 */

var EXPORTED_SYMBOLS = ["FormValidationChild"];

const { ActorChild } = ChromeUtils.import(
  "resource://gre/modules/ActorChild.jsm"
);
const { BrowserUtils } = ChromeUtils.import(
  "resource://gre/modules/BrowserUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");

class FormValidationChild extends ActorChild {
  constructor(dispatcher) {
    super(dispatcher);

    this._validationMessage = "";
    this._element = null;

    this.mm.addEventListener("pageshow", this);
  }

  /*
   * Events
   */

  handleEvent(aEvent) {
    switch (aEvent.type) {
      case "MozInvalidForm":
        aEvent.preventDefault();
        this.notifyInvalidSubmit(aEvent.target, aEvent.detail);
        break;
      case "pageshow":
        if (this._isRootDocumentEvent(aEvent)) {
          this._hidePopup();
        }
        break;
      case "input":
        this._onInput(aEvent);
        break;
      case "blur":
        this._onBlur(aEvent);
        break;
    }
  }

  /*
   * nsIFormSubmitObserver
   */

  notifyInvalidSubmit(aFormElement, aInvalidElements) {
    // Show a validation message on the first focusable element.
    for (let element of aInvalidElements) {
      // Insure that this is the FormSubmitObserver associated with the
      // element / window this notification is about.
      if (this.content != element.ownerGlobal.top.document.defaultView) {
        return;
      }

      if (
        !(
          ChromeUtils.getClassName(element) === "HTMLInputElement" ||
          ChromeUtils.getClassName(element) === "HTMLTextAreaElement" ||
          ChromeUtils.getClassName(element) === "HTMLSelectElement" ||
          ChromeUtils.getClassName(element) === "HTMLButtonElement"
        )
      ) {
        continue;
      }

      if (!Services.focus.elementIsFocusable(element, 0)) {
        continue;
      }

      // Update validation message before showing notification
      this._validationMessage = element.validationMessage;

      // Don't connect up to the same element more than once.
      if (this._element == element) {
        this._showPopup(element);
        break;
      }
      this._element = element;

      element.focus();

      // Watch for input changes which may change the validation message.
      element.addEventListener("input", this);

      // Watch for focus changes so we can disconnect our listeners and
      // hide the popup.
      element.addEventListener("blur", this);

      this._showPopup(element);
      break;
    }
  }

  /*
   * Internal
   */

  /*
   * Handles input changes on the form element we've associated a popup
   * with. Updates the validation message or closes the popup if form data
   * becomes valid.
   */
  _onInput(aEvent) {
    let element = aEvent.originalTarget;

    // If the form input is now valid, hide the popup.
    if (element.validity.valid) {
      this._hidePopup();
      return;
    }

    // If the element is still invalid for a new reason, we should update
    // the popup error message.
    if (this._validationMessage != element.validationMessage) {
      this._validationMessage = element.validationMessage;
      this._showPopup(element);
    }
  }

  /*
   * Blur event handler in which we disconnect from the form element and
   * hide the popup.
   */
  _onBlur(aEvent) {
    aEvent.originalTarget.removeEventListener("input", this);
    aEvent.originalTarget.removeEventListener("blur", this);
    this._element = null;
    this._hidePopup();
  }

  /*
   * Send the show popup message to chrome with appropriate position
   * information. Can be called repetitively to update the currently
   * displayed popup position and text.
   */
  _showPopup(aElement) {
    // Collect positional information and show the popup
    let panelData = {};

    panelData.message = this._validationMessage;

    // Note, this is relative to the browser and needs to be translated
    // in chrome.
    panelData.contentRect = BrowserUtils.getElementBoundingRect(aElement);

    // We want to show the popup at the middle of checkbox and radio buttons
    // and where the content begin for the other elements.
    let offset = 0;

    if (
      aElement.tagName == "INPUT" &&
      (aElement.type == "radio" || aElement.type == "checkbox")
    ) {
      panelData.position = "bottomcenter topleft";
    } else {
      let win = aElement.ownerGlobal;
      let style = win.getComputedStyle(aElement);
      if (style.direction == "rtl") {
        offset =
          parseInt(style.paddingRight) + parseInt(style.borderRightWidth);
      } else {
        offset = parseInt(style.paddingLeft) + parseInt(style.borderLeftWidth);
      }
      let zoomFactor = this.content.windowUtils.fullZoom;
      panelData.offset = Math.round(offset * zoomFactor);
      panelData.position = "after_start";
    }
    this.mm.sendAsyncMessage("FormValidation:ShowPopup", panelData);
  }

  _hidePopup() {
    this.mm.sendAsyncMessage("FormValidation:HidePopup", {});
  }

  _isRootDocumentEvent(aEvent) {
    if (this.content == null) {
      return true;
    }
    let target = aEvent.originalTarget;
    return (
      target == this.content.document ||
      (target.ownerDocument && target.ownerDocument == this.content.document)
    );
  }
}
back to top