You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
230 lines
7.4 KiB
230 lines
7.4 KiB
/* 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(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(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(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); |
|
} |
|
} |
|
};
|
|
|