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