diff options
Diffstat (limited to 'components/sessionstore/DocumentUtils.jsm')
-rw-r--r-- | components/sessionstore/DocumentUtils.jsm | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/components/sessionstore/DocumentUtils.jsm b/components/sessionstore/DocumentUtils.jsm new file mode 100644 index 0000000..6b3f729 --- /dev/null +++ b/components/sessionstore/DocumentUtils.jsm @@ -0,0 +1,230 @@ +/* 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); + } + } +}; |