diff options
Diffstat (limited to 'modules/FormSubmitObserver.jsm')
-rw-r--r-- | modules/FormSubmitObserver.jsm | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/modules/FormSubmitObserver.jsm b/modules/FormSubmitObserver.jsm new file mode 100644 index 0000000..6b2ea3c --- /dev/null +++ b/modules/FormSubmitObserver.jsm @@ -0,0 +1,235 @@ +/* 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/. */ + +/* + * Handles the validation callback from nsIFormFillController and + * the display of the help panel on invalid elements. + */ + +"use strict"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +var HTMLInputElement = Ci.nsIDOMHTMLInputElement; +var HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement; +var HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; +var HTMLButtonElement = Ci.nsIDOMHTMLButtonElement; + +this.EXPORTED_SYMBOLS = [ "FormSubmitObserver" ]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/BrowserUtils.jsm"); + +function FormSubmitObserver(aWindow, aTabChildGlobal) { + this.init(aWindow, aTabChildGlobal); +} + +FormSubmitObserver.prototype = +{ + _validationMessage: "", + _content: null, + _element: null, + + /* + * Public apis + */ + + init: function(aWindow, aTabChildGlobal) + { + this._content = aWindow; + this._tab = aTabChildGlobal; + this._mm = + this._content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .sameTypeRootTreeItem + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + + // nsIFormSubmitObserver callback about invalid forms. See HTMLFormElement + // for details. + Services.obs.addObserver(this, "invalidformsubmit", false); + this._tab.addEventListener("pageshow", this, false); + this._tab.addEventListener("unload", this, false); + }, + + uninit: function() + { + Services.obs.removeObserver(this, "invalidformsubmit"); + this._content.removeEventListener("pageshow", this, false); + this._content.removeEventListener("unload", this, false); + this._mm = null; + this._element = null; + this._content = null; + this._tab = null; + }, + + /* + * Events + */ + + handleEvent: function (aEvent) { + switch (aEvent.type) { + case "pageshow": + if (this._isRootDocumentEvent(aEvent)) { + this._hidePopup(); + } + break; + case "unload": + this.uninit(); + break; + case "input": + this._onInput(aEvent); + break; + case "blur": + this._onBlur(aEvent); + break; + } + }, + + /* + * nsIFormSubmitObserver + */ + + notifyInvalidSubmit : function (aFormElement, aInvalidElements) + { + // We are going to handle invalid form submission attempt by focusing the + // first invalid element and show the corresponding validation message in a + // panel attached to the element. + if (!aInvalidElements.length) { + return; + } + + // Ensure that this is the FormSubmitObserver associated with the + // element / window this notification is about. + let element = aInvalidElements.queryElementAt(0, Ci.nsISupports); + if (this._content != element.ownerGlobal.top.document.defaultView) { + return; + } + + if (!(element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement || + element instanceof HTMLSelectElement || + element instanceof HTMLButtonElement)) { + return; + } + + // 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); + return; + } + this._element = element; + + element.focus(); + + // Watch for input changes which may change the validation message. + element.addEventListener("input", this, false); + + // Watch for focus changes so we can disconnect our listeners and + // hide the popup. + element.addEventListener("blur", this, false); + + this._showPopup(element); + }, + + /* + * 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: function (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: function (aEvent) { + aEvent.originalTarget.removeEventListener("input", this, false); + aEvent.originalTarget.removeEventListener("blur", this, false); + 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: function (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, null); + if (style.direction == 'rtl') { + offset = parseInt(style.paddingRight) + parseInt(style.borderRightWidth); + } else { + offset = parseInt(style.paddingLeft) + parseInt(style.borderLeftWidth); + } + let zoomFactor = this._getWindowUtils().fullZoom; + panelData.offset = Math.round(offset * zoomFactor); + panelData.position = "after_start"; + } + this._mm.sendAsyncMessage("FormValidation:ShowPopup", panelData); + }, + + _hidePopup: function () { + this._mm.sendAsyncMessage("FormValidation:HidePopup", {}); + }, + + _getWindowUtils: function () { + return this._content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + }, + + _isRootDocumentEvent: function (aEvent) { + if (this._content == null) { + return true; + } + let target = aEvent.originalTarget; + return (target == this._content.document || + (target.ownerDocument && target.ownerDocument == this._content.document)); + }, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]) +}; |