summaryrefslogtreecommitdiffstats
path: root/webbrowser/components/sessionstore/DocumentUtils.jsm
blob: 6b3f729b54cb654b20b88938a0e502d2aed40144 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
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);
    }
  }
};