/* 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/. */ this.EXPORTED_SYMBOLS = [ "DocumentUtils" ]; const Cu = Components.utils; const Ci = Components.interfaces; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource:///modules/sessionstore/XPathGenerator.jsm"); this.DocumentUtils = { /** * Obtain form data for a DOMDocument instance. * * The returned object has 2 keys, "id" and "xpath". Each key holds an object * which further defines form data. * * The "id" object maps element IDs to values. The "xpath" object maps the * XPath of an element to its value. * * @param aDocument * DOMDocument instance to obtain form data for. * @return object * Form data encoded in an object. */ getFormData: function DocumentUtils_getFormData(aDocument) { let formNodes = aDocument.evaluate( XPathGenerator.restorableFormNodes, aDocument, XPathGenerator.resolveNS, Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null ); let node; let ret = {id: {}, xpath: {}}; // Limit the number of XPath expressions for performance reasons. See // bug 477564. const MAX_TRAVERSED_XPATHS = 100; let generatedCount = 0; while (node = formNodes.iterateNext()) { let nId = node.id; let hasDefaultValue = true; let value; // Only generate a limited number of XPath expressions for perf reasons // (cf. bug 477564) if (!nId && generatedCount > MAX_TRAVERSED_XPATHS) { continue; } if (node instanceof Ci.nsIDOMHTMLInputElement || node instanceof Ci.nsIDOMHTMLTextAreaElement) { switch (node.type) { case "checkbox": case "radio": value = node.checked; hasDefaultValue = value == node.defaultChecked; break; case "file": value = { type: "file", fileList: node.mozGetFileNameArray() }; hasDefaultValue = !value.fileList.length; break; default: // text, textarea value = node.value; hasDefaultValue = value == node.defaultValue; break; } } else if (!node.multiple) { // <select>s without the multiple attribute are hard to determine the // default value, so assume we don't have the default. hasDefaultValue = false; value = { selectedIndex: node.selectedIndex, value: node.value }; } else { // <select>s with the multiple attribute are easier to determine the // default value since each <option> has a defaultSelected let options = Array.map(node.options, function(aOpt, aIx) { let oSelected = aOpt.selected; hasDefaultValue = hasDefaultValue && (oSelected == aOpt.defaultSelected); return oSelected ? aOpt.value : -1; }); value = options.filter(function(aIx) aIx !== -1); } // In order to reduce XPath generation (which is slow), we only save data // for form fields that have been changed. (cf. bug 537289) if (!hasDefaultValue) { if (nId) { ret.id[nId] = value; } else { generatedCount++; ret.xpath[XPathGenerator.generate(node)] = value; } } } return ret; }, /** * Merges form data on a document from previously obtained data. * * This is the inverse of getFormData(). The data argument is the same object * type which is returned by getFormData(): an object containing the keys * "id" and "xpath" which are each objects mapping element identifiers to * form values. * * Where the document has existing form data for an element, the value * will be replaced. Where the document has a form element but no matching * data in the passed object, the element is untouched. * * @param aDocument * DOMDocument instance to which to restore form data. * @param aData * Object defining form data. */ mergeFormData: function DocumentUtils_mergeFormData(aDocument, aData) { if ("xpath" in aData) { for each (let [xpath, value] in Iterator(aData.xpath)) { let node = XPathGenerator.resolve(aDocument, xpath); if (node) { this.restoreFormValue(node, value, aDocument); } } } if ("id" in aData) { for each (let [id, value] in Iterator(aData.id)) { let node = aDocument.getElementById(id); if (node) { this.restoreFormValue(node, value, aDocument); } } } }, /** * Low-level function to restore a form value to a DOMNode. * * If you want a higher-level interface, see mergeFormData(). * * When the value is changed, the function will fire the appropriate DOM * events. * * @param aNode * DOMNode to set form value on. * @param aValue * Value to set form element to. * @param aDocument [optional] * DOMDocument node belongs to. If not defined, node.ownerDocument * is used. */ restoreFormValue: function DocumentUtils_restoreFormValue(aNode, aValue, aDocument) { aDocument = aDocument || aNode.ownerDocument; let eventType; if (typeof aValue == "string" && aNode.type != "file") { // Don't dispatch an input event if there is no change. if (aNode.value == aValue) { return; } aNode.value = aValue; eventType = "input"; } else if (typeof aValue == "boolean") { // Don't dispatch a change event for no change. if (aNode.checked == aValue) { return; } aNode.checked = aValue; eventType = "change"; } else if (typeof aValue == "number") { // handle select backwards compatibility, example { "#id" : index } // We saved the value blindly since selects take more work to determine // default values. So now we should check to avoid unnecessary events. if (aNode.selectedIndex == aValue) { return; } if (aValue < aNode.options.length) { aNode.selectedIndex = aValue; eventType = "change"; } } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) { // handle select new format // Don't dispatch a change event for no change if (aNode.options[aNode.selectedIndex].value == aValue.value) { return; } // find first option with matching aValue if possible for (let i = 0; i < aNode.options.length; i++) { if (aNode.options[i].value == aValue.value) { aNode.selectedIndex = i; break; } } eventType = "change"; } else if (aValue && aValue.fileList && aValue.type == "file" && aNode.type == "file") { aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length); eventType = "input"; } else if (aValue && typeof aValue.indexOf == "function" && aNode.options) { Array.forEach(aNode.options, function(opt, index) { // don't worry about malformed options with same values opt.selected = aValue.indexOf(opt.value) > -1; // Only fire the event here if this wasn't selected by default if (!opt.defaultSelected) { eventType = "change"; } }); } // Fire events for this node if applicable if (eventType) { let event = aDocument.createEvent("UIEvents"); event.initUIEvent(eventType, true, true, aDocument.defaultView, 0); aNode.dispatchEvent(event); } } };