]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/dom.jsm
Import r6948 from upstream hg supporting Firefox up to 24.*
[dactyl.git] / common / modules / dom.jsm
1 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2008-2012 Kris Maglione <maglione.k@gmail.com>
3 //
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
6 "use strict";
7
8 defineModule("dom", {
9     exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
10 });
11
12 lazyRequire("highlight", ["highlight"]);
13 lazyRequire("messages", ["_"]);
14 lazyRequire("overlay", ["overlay"]);
15 lazyRequire("prefs", ["prefs"]);
16 lazyRequire("template", ["template"]);
17
18 var XBL = "http://www.mozilla.org/xbl";
19 var XHTML = "http://www.w3.org/1999/xhtml";
20 var XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
21 var NS = "http://vimperator.org/namespaces/liberator";
22
23 function BooleanAttribute(attr) ({
24     get: function (elem) elem.getAttribute(attr) == "true",
25     set: function (elem, val) {
26         if (val === "false" || !val)
27             elem.removeAttribute(attr);
28         else
29             elem.setAttribute(attr, true);
30     }
31 });
32
33 /**
34  * @class
35  *
36  * A jQuery-inspired DOM utility framework.
37  *
38  * Please note that while this currently implements an Array-like
39  * interface, this is *not a defined interface* and is very likely to
40  * change in the near future.
41  */
42 var DOM = Class("DOM", {
43     init: function init(val, context, nodes) {
44         let self;
45         let length = 0;
46
47         if (nodes)
48             this.nodes = nodes;
49
50         if (context instanceof Ci.nsIDOMDocument)
51             this.document = context;
52
53         if (typeof val == "string")
54             val = context.querySelectorAll(val);
55
56         if (val == null)
57             ;
58         else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument)
59             this[length++] = DOM.fromXML(val, context, this.nodes);
60         else if (DOM.isJSONXML(val)) {
61             if (context instanceof Ci.nsIDOMDocument)
62                 this[length++] = DOM.fromJSON(val, context, this.nodes);
63             else
64                 this[length++] = val;
65         }
66         else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
67             this[length++] = val;
68         else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
69             for (let elem in val)
70                 this[length++] = elem;
71         else if ("length" in val)
72             for (let i = 0; i < val.length; i++)
73                 this[length++] = val[i];
74         else
75             this[length++] = val;
76
77         this.length = length;
78         return self || this;
79     },
80
81     __iterator__: function __iterator__() {
82         for (let i = 0; i < this.length; i++)
83             yield this[i];
84     },
85
86     Empty: function Empty() this.constructor(null, this.document),
87
88     nodes: Class.Memoize(function () ({})),
89
90     get items() {
91         for (let i = 0; i < this.length; i++)
92             yield this.eq(i);
93     },
94
95     get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
96     set document(val) this._document = val,
97
98     attrHooks: array.toObject([
99         ["", {
100             href: { get: function (elem) elem.href || elem.getAttribute("href") },
101             src:  { get: function (elem) elem.src || elem.getAttribute("src") },
102             checked: { get: function (elem) elem.hasAttribute("checked") ? elem.getAttribute("checked") == "true" : elem.checked,
103                        set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val; } },
104             collapsed: BooleanAttribute("collapsed"),
105             disabled: BooleanAttribute("disabled"),
106             hidden: BooleanAttribute("hidden"),
107             readonly: BooleanAttribute("readonly")
108         }]
109     ]),
110
111     matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
112
113     each: function each(fn, self) {
114         let obj = self || this.Empty();
115         for (let i = 0; i < this.length; i++)
116             fn.call(self || update(obj, [this[i]]), this[i], i);
117         return this;
118     },
119
120     eachDOM: function eachDOM(val, fn, self) {
121         let dom = this;
122         function munge(val, container, idx) {
123             if (val instanceof Ci.nsIDOMRange)
124                 return val.extractContents();
125             if (val instanceof Ci.nsIDOMNode)
126                 return val;
127
128             if (DOM.isJSONXML(val)) {
129                 val = dom.constructor(val, dom.document);
130                 if (container)
131                     container[idx] = val[0];
132             }
133
134             if (isObject(val) && "length" in val) {
135                 let frag = dom.document.createDocumentFragment();
136                 for (let i = 0; i < val.length; i++)
137                     frag.appendChild(munge(val[i], val, i));
138                 return frag;
139             }
140             return val;
141         }
142
143         if (DOM.isJSONXML(val))
144             val = (function () this).bind(val);
145
146         if (callable(val))
147             return this.each(function (elem, i) {
148                 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
149             }, self || this);
150
151         if (this.length)
152             util.withProperErrors(fn, self || this, munge(val), this[0], 0);
153         return this;
154     },
155
156     eq: function eq(idx) {
157         return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
158     },
159
160     find: function find(val) {
161         return this.map(function (elem) elem.querySelectorAll(val));
162     },
163
164     findAnon: function findAnon(attr, val) {
165         return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
166     },
167
168     filter: function filter(val, self) {
169         let res = this.Empty();
170
171         if (!callable(val))
172             val = this.matcher(val);
173
174         this.constructor(Array.filter(this, val, self || this));
175         let obj = self || this.Empty();
176         for (let i = 0; i < this.length; i++)
177             if (val.call(self || update(obj, [this[i]]), this[i], i))
178                 res[res.length++] = this[i];
179
180         return res;
181     },
182
183     is: function is(val) {
184         return this.some(this.matcher(val));
185     },
186
187     reverse: function reverse() {
188         Array.reverse(this);
189         return this;
190     },
191
192     all: function all(fn, self) {
193         let res = this.Empty();
194
195         this.each(function (elem) {
196             while (true) {
197                 elem = fn.call(this, elem);
198                 if (elem instanceof Ci.nsIDOMNode)
199                     res[res.length++] = elem;
200                 else if (elem && "length" in elem)
201                     for (let i = 0; i < elem.length; i++)
202                         res[res.length++] = elem[j];
203                 else
204                     break;
205             }
206         }, self || this);
207         return res;
208     },
209
210     map: function map(fn, self) {
211         let res = this.Empty();
212         let obj = self || this.Empty();
213
214         for (let i = 0; i < this.length; i++) {
215             let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
216             if (isObject(tmp) && "length" in tmp)
217                 for (let j = 0; j < tmp.length; j++)
218                     res[res.length++] = tmp[j];
219             else if (tmp != null)
220                 res[res.length++] = tmp;
221         }
222
223         return res;
224     },
225
226     slice: function eq(start, end) {
227         return this.constructor(Array.slice(this, start, end));
228     },
229
230     some: function some(fn, self) {
231         for (let i = 0; i < this.length; i++)
232             if (fn.call(self || this, this[i], i))
233                 return true;
234         return false;
235     },
236
237     get parent() this.map(function (elem) elem.parentNode, this),
238
239     get offsetParent() this.map(function (elem) {
240         do {
241             var parent = elem.offsetParent;
242             if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
243                 return parent;
244         }
245         while (parent);
246     }, this),
247
248     get ancestors() this.all(function (elem) elem.parentNode),
249
250     get children() this.map(function (elem) Array.filter(elem.childNodes,
251                                                          function (e) e instanceof Ci.nsIDOMElement),
252                             this),
253
254     get contents() this.map(function (elem) elem.childNodes, this),
255
256     get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
257                                                          function (e) e != elem && e instanceof Ci.nsIDOMElement),
258                             this),
259
260     get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
261     get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
262
263     get allSiblingsBefore() this.all(function (elem) elem.previousSibling),
264     get allSiblingsAfter() this.all(function (elem) elem.nextSibling),
265
266     get class() let (self = this) ({
267         toString: function () self[0].className,
268
269         get list() Array.slice(self[0].classList),
270         set list(val) self.attr("class", val.join(" ")),
271
272         each: function each(meth, arg) {
273             return self.each(function (elem) {
274                 elem.classList[meth](arg);
275             });
276         },
277
278         add: function add(cls) this.each("add", cls),
279         remove: function remove(cls) this.each("remove", cls),
280         toggle: function toggle(cls, val, thisObj) {
281             if (callable(val))
282                 return self.each(function (elem, i) {
283                     this.class.toggle(cls, val.call(thisObj || this, elem, i));
284                 });
285             return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
286         },
287
288         has: function has(cls) this[0].classList.has(cls)
289     }),
290
291     get highlight() let (self = this) ({
292         toString: function () self.attrNS(NS, "highlight") || "",
293
294         get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
295         set list(val) {
296             let str = array.uniq(val).join(" ").trim();
297             self.attrNS(NS, "highlight", str || null);
298         },
299
300         has: function has(hl) ~this.list.indexOf(hl),
301
302         add: function add(hl) self.each(function () {
303             highlight.loaded[hl] = true;
304             this.highlight.list = this.highlight.list.concat(hl);
305         }),
306
307         remove: function remove(hl) self.each(function () {
308             this.highlight.list = this.highlight.list.filter(function (h) h != hl);
309         }),
310
311         toggle: function toggle(hl, val, thisObj) self.each(function (elem, i) {
312             let { highlight } = this;
313             let v = callable(val) ? val.call(thisObj || this, elem, i) : val;
314
315             highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl);
316         }),
317     }),
318
319     get rect() this[0] instanceof Ci.nsIDOMWindow ? { width: this[0].scrollMaxX + this[0].innerWidth,
320                                                       height: this[0].scrollMaxY + this[0].innerHeight,
321                                                       get right() this.width + this.left,
322                                                       get bottom() this.height + this.top,
323                                                       top: -this[0].scrollY,
324                                                       left: -this[0].scrollX } :
325                this[0]                            ? this[0].getBoundingClientRect() : {},
326
327     get viewport() {
328         let node = this[0];
329         if (node instanceof Ci.nsIDOMDocument)
330             node = node.defaultView;
331
332         if (node instanceof Ci.nsIDOMWindow)
333             return {
334                 get width() this.right - this.left,
335                 get height() this.bottom - this.top,
336                 bottom: node.innerHeight,
337                 right: node.innerWidth,
338                 top: 0, left: 0
339             };
340
341         let r = this.rect;
342         return {
343             width: node.clientWidth,
344             height: node.clientHeight,
345             top: r.top + node.clientTop,
346             get bottom() this.top + this.height,
347             left: r.left + node.clientLeft,
348             get right() this.left + this.width
349         };
350     },
351
352     scrollPos: function scrollPos(left, top) {
353         if (arguments.length == 0) {
354             if (this[0] instanceof Ci.nsIDOMElement)
355                 return { top: this[0].scrollTop, left: this[0].scrollLeft,
356                          height: this[0].scrollHeight, width: this[0].scrollWidth,
357                          innerHeight: this[0].clientHeight, innerWidth: this[0].innerWidth };
358
359             if (this[0] instanceof Ci.nsIDOMWindow)
360                 return { top: this[0].scrollY, left: this[0].scrollX,
361                          height: this[0].scrollMaxY + this[0].innerHeight,
362                          width: this[0].scrollMaxX + this[0].innerWidth,
363                          innerHeight: this[0].innerHeight, innerWidth: this[0].innerWidth };
364
365             return null;
366         }
367         let func = callable(left) && left;
368
369         return this.each(function (elem, i) {
370             if (func)
371                 ({ left, top }) = func.call(this, elem, i);
372
373             if (elem instanceof Ci.nsIDOMWindow)
374                 elem.scrollTo(left == null ? elem.scrollX : left,
375                               top  == null ? elem.scrollY : top);
376             else {
377                 if (left != null)
378                     elem.scrollLeft = left;
379                 if (top != null)
380                     elem.scrollTop = top;
381             }
382         });
383     },
384
385     /**
386      * Returns true if the given DOM node is currently visible.
387      * @returns {boolean}
388      */
389     get isVisible() {
390         let style = this[0] && this.style;
391         return style && style.visibility == "visible" && style.display != "none";
392     },
393
394     get editor() {
395         if (!this.length)
396             return;
397
398         this[0] instanceof Ci.nsIDOMNSEditableElement;
399         try {
400             if (this[0].editor instanceof Ci.nsIEditor)
401                 var editor = this[0].editor;
402         }
403         catch (e) {
404             util.reportError(e);
405         }
406
407         try {
408             if (!editor)
409                 editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
410                                 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
411                                 .getEditorForWindow(this[0]);
412         }
413         catch (e) {}
414
415         editor instanceof Ci.nsIPlaintextEditor;
416         editor instanceof Ci.nsIHTMLEditor;
417         return editor;
418     },
419
420     get isEditable() !!this.editor || this[0] instanceof Ci.nsIDOMElement && this.style.MozUserModify == "read-write",
421
422     get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
423                                        Ci.nsIDOMHTMLTextAreaElement,
424                                        Ci.nsIDOMXULTextBoxElement])
425                     && this.isEditable,
426
427     /**
428      * Returns an object representing a Node's computed CSS style.
429      * @returns {Object}
430      */
431     get style() {
432         let node = this[0];
433         if (node instanceof Ci.nsIDOMWindow)
434             node = node.document;
435         if (node instanceof Ci.nsIDOMDocument)
436             node = node.documentElement;
437         while (node && !(node instanceof Ci.nsIDOMElement) && node.parentNode)
438             node = node.parentNode;
439
440         try {
441             var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
442         }
443         catch (e) {}
444
445         if (res == null) {
446             util.dumpStack(_("error.nullComputedStyle", node));
447             Cu.reportError(Error(_("error.nullComputedStyle", node)));
448             return {};
449         }
450         return res;
451     },
452
453     /**
454      * Parses the fields of a form and returns a URL/POST-data pair
455      * that is the equivalent of submitting the form.
456      *
457      * @returns {object} An object with the following elements:
458      *      url: The URL the form points to.
459      *      postData: A string containing URL-encoded post data, if this
460      *                form is to be POSTed
461      *      charset: The character set of the GET or POST data.
462      *      elements: The key=value pairs used to generate query information.
463      */
464     // Nuances gleaned from browser.jar/content/browser/browser.js
465     get formData() {
466         function encode(name, value, param) {
467             param = param ? "%s" : "";
468             if (post)
469                 return name + "=" + encodeComponent(value + param);
470             return encodeComponent(name) + "=" + encodeComponent(value) + param;
471         }
472
473         let field = this[0];
474         let form = field.form;
475         let doc = form.ownerDocument;
476
477         let charset = doc.characterSet;
478         let converter = services.CharsetConv(charset);
479         for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
480             let c = services.CharsetConv(cs);
481             if (c) {
482                 converter = services.CharsetConv(cs);
483                 charset = cs;
484             }
485         }
486
487         let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
488         let url = util.newURI(form.action, charset, uri).spec;
489
490         let post = form.method.toUpperCase() == "POST";
491
492         let encodeComponent = encodeURIComponent;
493         if (charset !== "UTF-8")
494             encodeComponent = function encodeComponent(str)
495                 escape(converter.ConvertFromUnicode(str) + converter.Finish());
496
497         let elems = [];
498         if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
499             elems.push(encode(field.name, field.value));
500
501         for (let [, elem] in iter(form.elements))
502             if (elem.name && !elem.disabled) {
503                 if (DOM(elem).isInput
504                         || /^(?:hidden|textarea)$/.test(elem.type)
505                         || elem.type == "submit" && elem == field
506                         || elem.checked && /^(?:checkbox|radio)$/.test(elem.type)) {
507
508                     if (elem !== field)
509                         elems.push(encode(elem.name, elem.value));
510                     else if (overlay.getData(elem, "had-focus"))
511                         elems.push(encode(elem.name, elem.value, true));
512                     else
513                         elems.push(encode(elem.name, "", true));
514                 }
515                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
516                     for (let [, opt] in Iterator(elem.options))
517                         if (opt.selected)
518                             elems.push(encode(elem.name, opt.value));
519                 }
520             }
521
522         if (post)
523             return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
524         return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
525     },
526
527     /**
528      * Generates an XPath expression for the given element.
529      *
530      * @returns {string}
531      */
532     get xpath() {
533         function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
534         if (!(this[0] instanceof Ci.nsIDOMElement))
535             return null;
536
537         let res = [];
538         let doc = this.document;
539         for (let elem = this[0];; elem = elem.parentNode) {
540             if (!(elem instanceof Ci.nsIDOMElement))
541                 res.push("");
542             else if (elem.id)
543                 res.push("id(" + quote(elem.id) + ")");
544             else {
545                 let name = elem.localName;
546                 if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
547                     if (elem.namespaceURI in DOM.namespaceNames)
548                         name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
549                     else
550                         name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
551
552                 res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
553                 continue;
554             }
555             break;
556         }
557
558         return res.reverse().join("/");
559     },
560
561     /**
562      * Returns a string or XML representation of this node.
563      *
564      * @param {boolean} color If true, return a colored, XML
565      *  representation of this node.
566      */
567     repr: function repr(color) {
568         function namespaced(node) {
569             var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[1];
570             if (!ns)
571                 return node.localName;
572             if (color)
573                 return [["span", { highlight: "HelpXMLNamespace" }, ns],
574                         node.localName];
575             return ns + ":" + node.localName;
576         }
577
578         let res = [];
579         this.each(function (elem) {
580             try {
581                 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling);
582                 if (color)
583                     res.push(["span", { highlight: "HelpXML" },
584                         ["span", { highlight: "HelpXMLTagStart" },
585                             "<", namespaced(elem), " ",
586                             template.map(array.iterValues(elem.attributes),
587                                 function (attr) [
588                                     ["span", { highlight: "HelpXMLAttribute" }, namespaced(attr)],
589                                     ["span", { highlight: "HelpXMLString" }, attr.value]
590                                 ],
591                                 " "),
592                             !hasChildren ? "/>" : ">",
593                         ],
594                         !hasChildren ? "" :
595                             ["", "...",
596                              ["span", { highlight: "HtmlTagEnd" }, "<", namespaced(elem), ">"]]
597                     ]);
598                 else {
599                     let tag = "<" + [namespaced(elem)].concat(
600                         [namespaced(a) + '="' + String.replace(a.value, /["<]/, DOM.escapeHTML) + '"'
601                          for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
602
603                     res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
604                 }
605             }
606             catch (e) {
607                 res.push({}.toString.call(elem));
608             }
609         }, this);
610         res = template.map(res, util.identity, ",");
611         return color ? res : res.join("");
612     },
613
614     attr: function attr(key, val) {
615         return this.attrNS("", key, val);
616     },
617
618     attrNS: function attrNS(ns, key, val) {
619         if (val !== undefined)
620             key = array.toObject([[key, val]]);
621
622         let hooks = this.attrHooks[ns] || {};
623
624         if (isObject(key))
625             return this.each(function (elem, i) {
626                 for (let [k, v] in Iterator(key)) {
627                     if (callable(v))
628                         v = v.call(this, elem, i);
629
630                     if (Set.has(hooks, k) && hooks[k].set)
631                         hooks[k].set.call(this, elem, v, k);
632                     else if (v == null)
633                         elem.removeAttributeNS(ns, k);
634                     else
635                         elem.setAttributeNS(ns, k, v);
636                 }
637             });
638
639         if (!this.length)
640             return null;
641
642         if (Set.has(hooks, key) && hooks[key].get)
643             return hooks[key].get.call(this, this[0], key);
644
645         if (!this[0].hasAttributeNS(ns, key))
646             return null;
647
648         return this[0].getAttributeNS(ns, key);
649     },
650
651     css: update(function css(key, val) {
652         if (val !== undefined)
653             key = array.toObject([[key, val]]);
654
655         if (isObject(key))
656             return this.each(function (elem) {
657                 for (let [k, v] in Iterator(key))
658                     elem.style[css.property(k)] = v;
659             });
660
661         return this[0].style[css.property(key)];
662     }, {
663         name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
664
665         property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
666     }),
667
668     append: function append(val) {
669         return this.eachDOM(val, function (elem, target) {
670             target.appendChild(elem);
671         });
672     },
673
674     prepend: function prepend(val) {
675         return this.eachDOM(val, function (elem, target) {
676             target.insertBefore(elem, target.firstChild);
677         });
678     },
679
680     before: function before(val) {
681         return this.eachDOM(val, function (elem, target) {
682             target.parentNode.insertBefore(elem, target);
683         });
684     },
685
686     after: function after(val) {
687         return this.eachDOM(val, function (elem, target) {
688             target.parentNode.insertBefore(elem, target.nextSibling);
689         });
690     },
691
692     appendTo: function appendTo(elem) {
693         if (!(elem instanceof this.constructor))
694             elem = this.constructor(elem, this.document);
695         elem.append(this);
696         return this;
697     },
698
699     prependTo: function prependTo(elem) {
700         if (!(elem instanceof this.constructor))
701             elem = this.constructor(elem, this.document);
702         elem.prepend(this);
703         return this;
704     },
705
706     insertBefore: function insertBefore(elem) {
707         if (!(elem instanceof this.constructor))
708             elem = this.constructor(elem, this.document);
709         elem.before(this);
710         return this;
711     },
712
713     insertAfter: function insertAfter(elem) {
714         if (!(elem instanceof this.constructor))
715             elem = this.constructor(elem, this.document);
716         elem.after(this);
717         return this;
718     },
719
720     remove: function remove() {
721         return this.each(function (elem) {
722             if (elem.parentNode)
723                 elem.parentNode.removeChild(elem);
724         }, this);
725     },
726
727     empty: function empty() {
728         return this.each(function (elem) {
729             while (elem.firstChild)
730                 elem.removeChild(elem.firstChild);
731         }, this);
732     },
733
734     fragment: function fragment() {
735         let frag = this.document.createDocumentFragment();
736         this.appendTo(frag);
737         return this;
738     },
739
740     clone: function clone(deep)
741         this.map(function (elem) elem.cloneNode(deep)),
742
743     toggle: function toggle(val, self) {
744         if (callable(val))
745             return this.each(function (elem, i) {
746                 this[val.call(self || this, elem, i) ? "show" : "hide"]();
747             });
748
749         if (arguments.length)
750             return this[val ? "show" : "hide"]();
751
752         let hidden = this.map(function (elem) elem.style.display == "none");
753         return this.each(function (elem, i) {
754             this[hidden[i] ? "show" : "hide"]();
755         });
756     },
757     hide: function hide() {
758         return this.each(function (elem) { elem.style.display = "none"; }, this);
759     },
760     show: function show() {
761         for (let i = 0; i < this.length; i++)
762             if (!this[i].dactylDefaultDisplay && this[i].style.display)
763                 this[i].style.display = "";
764
765         this.each(function (elem) {
766             if (!elem.dactylDefaultDisplay)
767                 elem.dactylDefaultDisplay = this.style.display;
768         });
769
770         return this.each(function (elem) {
771             elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
772         }, this);
773     },
774
775     createContents: function createContents()
776         this.each(DOM.createContents, this),
777
778     isScrollable: function isScrollable(direction)
779         this.length && DOM.isScrollable(this[0], direction),
780
781     getSet: function getSet(args, get, set) {
782         if (!args.length)
783             return this[0] && get.call(this, this[0]);
784
785         let [fn, self] = args;
786         if (!callable(fn))
787             fn = function () args[0];
788
789         return this.each(function (elem, i) {
790             set.call(this, elem, fn.call(self || this, elem, i));
791         }, this);
792     },
793
794     html: function html(txt, self) {
795         return this.getSet(arguments,
796                            function (elem) elem.innerHTML,
797                            util.wrapCallback(function (elem, val) { elem.innerHTML = val; }));
798     },
799
800     text: function text(txt, self) {
801         return this.getSet(arguments,
802                            function (elem) elem.textContent,
803                            function (elem, val) { elem.textContent = val; });
804     },
805
806     val: function val(txt) {
807         return this.getSet(arguments,
808                            function (elem) elem.value,
809                            function (elem, val) { elem.value = val == null ? "" : val; });
810     },
811
812     listen: function listen(event, listener, capture) {
813         if (isObject(event))
814             capture = listener;
815         else
816             event = array.toObject([[event, listener]]);
817
818         for (let [evt, callback] in Iterator(event))
819             event[evt] = util.wrapCallback(callback, true);
820
821         return this.each(function (elem) {
822             for (let [evt, callback] in Iterator(event))
823                 elem.addEventListener(evt, callback, capture);
824         });
825     },
826     unlisten: function unlisten(event, listener, capture) {
827         if (isObject(event))
828             capture = listener;
829         else
830             event = array.toObject([[event, listener]]);
831
832         return this.each(function (elem) {
833             for (let [k, v] in Iterator(event))
834                 elem.removeEventListener(k, v.wrapper || v, capture);
835         });
836     },
837     once: function once(event, listener, capture) {
838         if (isObject(event))
839             capture = listener;
840         else
841             event = array.toObject([[event, listener]]);
842
843         for (let pair in Iterator(event)) {
844             let [evt, callback] = pair;
845             event[evt] = util.wrapCallback(function wrapper(event) {
846                 this.removeEventListener(evt, wrapper.wrapper, capture);
847                 return callback.apply(this, arguments);
848             }, true);
849         }
850
851         return this.each(function (elem) {
852             for (let [k, v] in Iterator(event))
853                 elem.addEventListener(k, v, capture);
854         });
855     },
856
857     dispatch: function dispatch(event, params, extraProps) {
858         this.canceled = false;
859         return this.each(function (elem) {
860             let evt = DOM.Event(this.document, event, params, elem);
861             if (!DOM.Event.dispatch(elem, evt, extraProps))
862                 this.canceled = true;
863         }, this);
864     },
865
866     focus: function focus(arg, extra) {
867         if (callable(arg))
868             return this.listen("focus", arg, extra);
869
870         let elem = this[0];
871         let flags = arg || services.focus.FLAG_BYMOUSE;
872         try {
873             if (elem instanceof Ci.nsIDOMDocument)
874                 elem = elem.defaultView;
875             if (elem instanceof Ci.nsIDOMElement)
876                 services.focus.setFocus(elem, flags);
877             else if (elem instanceof Ci.nsIDOMWindow) {
878                 services.focus.focusedWindow = elem;
879                 if (services.focus.focusedWindow != elem)
880                     services.focus.clearFocus(elem);
881             }
882         }
883         catch (e) {
884             util.dump(elem);
885             util.reportError(e);
886         }
887         return this;
888     },
889     blur: function blur(arg, extra) {
890         if (callable(arg))
891             return this.listen("blur", arg, extra);
892         return this.each(function (elem) { elem.blur(); }, this);
893     },
894
895     /**
896      * Scrolls an element into view if and only if it's not already
897      * fully visible.
898      */
899     scrollIntoView: function scrollIntoView(alignWithTop) {
900         return this.each(function (elem) {
901             function getAlignment(viewport) {
902                 if (alignWithTop !== undefined)
903                     return alignWithTop;
904                 if (rect.bottom < viewport.top)
905                     return true;
906                 if (rect.top > viewport.bottom)
907                     return false;
908                 return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom);
909             }
910
911             let rect;
912             function fix(parent) {
913                 if (!(parent[0] instanceof Ci.nsIDOMWindow)
914                         && parent.style.overflow == "visible")
915                     return;
916
917                 ({ rect }) = DOM(elem);
918                 let { viewport } = parent;
919                 let isect = util.intersection(rect, viewport);
920
921                 if (isect.height < Math.min(viewport.height, rect.height)) {
922                     let { top } = parent.scrollPos();
923                     if (getAlignment(viewport))
924                         parent.scrollPos(null, top - (viewport.top - rect.top));
925                     else
926                         parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
927
928                 }
929             }
930
931             for (let parent in this.ancestors.items)
932                 fix(parent);
933
934             fix(DOM(this.document.defaultView));
935         });
936     },
937 }, {
938     /**
939      * Creates an actual event from a pseudo-event object.
940      *
941      * The pseudo-event object (such as may be retrieved from
942      * DOM.Event.parse) should have any properties you want the event to
943      * have.
944      *
945      * @param {Document} doc The DOM document to associate this event with
946      * @param {Type} type The type of event (keypress, click, etc.)
947      * @param {Object} opts The pseudo-event. @optional
948      */
949     Event: Class("Event", {
950         init: function Event(doc, type, opts, target) {
951             const DEFAULTS = {
952                 HTML: {
953                     type: type, bubbles: true, cancelable: false
954                 },
955                 Key: {
956                     type: type,
957                     bubbles: true, cancelable: true,
958                     view: doc.defaultView,
959                     ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
960                     keyCode: 0, charCode: 0
961                 },
962                 Mouse: {
963                     type: type,
964                     bubbles: true, cancelable: true,
965                     view: doc.defaultView,
966                     detail: 1,
967                     get screenX() this.view.mozInnerScreenX
968                                 + Math.max(0, this.clientX + (DOM(target || opts.target).rect.left || 0)),
969                     get screenY() this.view.mozInnerScreenY
970                                 + Math.max(0, this.clientY + (DOM(target || opts.target).rect.top || 0)),
971                     clientX: 0,
972                     clientY: 0,
973                     ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
974                     button: 0,
975                     relatedTarget: null
976                 }
977             };
978
979             opts = opts || {};
980             var t = this.constructor.types[type] || "";
981             var evt = doc.createEvent(t + "Events");
982
983             let params = DEFAULTS[t || "HTML"];
984             let args = Object.keys(params);
985             update(params, this.constructor.defaults[type],
986                    iter.toObject([k, opts[k]] for (k in opts) if (k in params)));
987
988             evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
989             return evt;
990         }
991     }, {
992         init: function init() {
993             // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
994             //       matters, so use that string as the first item, that you
995             //       want to refer to within dactyl's source code for
996             //       comparisons like if (key == "<Esc>") { ... }
997             this.keyTable = {
998                 add: ["+", "Plus", "Add"],
999                 back_quote: ["`"],
1000                 back_slash: ["\\"],
1001                 back_space: ["BS"],
1002                 comma: [","],
1003                 count: ["count"],
1004                 close_bracket: ["]"],
1005                 delete: ["Del"],
1006                 equals: ["="],
1007                 escape: ["Esc", "Escape"],
1008                 insert: ["Insert", "Ins"],
1009                 leader: ["Leader"],
1010                 left_shift: ["LT", "<"],
1011                 nop: ["Nop"],
1012                 open_bracket: ["["],
1013                 pass: ["Pass"],
1014                 period: ["."],
1015                 quote: ["'"],
1016                 return: ["Return", "CR", "Enter"],
1017                 right_shift: [">"],
1018                 semicolon: [";"],
1019                 slash: ["/"],
1020                 space: ["Space", " "],
1021                 subtract: ["-", "Minus", "Subtract"]
1022             };
1023
1024             this.key_key = {};
1025             this.code_key = {};
1026             this.key_code = {};
1027             this.code_nativeKey = {};
1028
1029             for (let list in values(this.keyTable))
1030                 for (let v in values(list)) {
1031                     if (v.length == 1)
1032                         v = v.toLowerCase();
1033                     this.key_key[v.toLowerCase()] = v;
1034                 }
1035
1036             for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
1037                 if (!/^DOM_VK_/.test(k))
1038                     continue;
1039
1040                 this.code_nativeKey[v] = k.substr(4);
1041
1042                 k = k.substr(7).toLowerCase();
1043                 let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
1044                               .replace(/^NUMPAD/, "k")];
1045
1046                 if (names[0].length == 1)
1047                     names[0] = names[0].toLowerCase();
1048
1049                 if (k in this.keyTable)
1050                     names = this.keyTable[k];
1051
1052                 this.code_key[v] = names[0];
1053                 for (let [, name] in Iterator(names)) {
1054                     this.key_key[name.toLowerCase()] = name;
1055                     this.key_code[name.toLowerCase()] = v;
1056                 }
1057             }
1058
1059             // HACK: as Gecko does not include an event for <, we must add this in manually.
1060             if (!("<" in this.key_code)) {
1061                 this.key_code["<"] = 60;
1062                 this.key_code["lt"] = 60;
1063                 this.code_key[60] = "lt";
1064             }
1065
1066             return this;
1067         },
1068
1069         code_key:       Class.Memoize(function (prop) this.init()[prop]),
1070         code_nativeKey: Class.Memoize(function (prop) this.init()[prop]),
1071         keyTable:       Class.Memoize(function (prop) this.init()[prop]),
1072         key_code:       Class.Memoize(function (prop) this.init()[prop]),
1073         key_key:        Class.Memoize(function (prop) this.init()[prop]),
1074         pseudoKeys:     Set(["count", "leader", "nop", "pass"]),
1075
1076         /**
1077          * Converts a user-input string of keys into a canonical
1078          * representation.
1079          *
1080          * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
1081          * <C- > maps to <C-Space>, <S-a> maps to A
1082          * << maps to <lt><lt>
1083          *
1084          * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
1085          * in macros.
1086          *
1087          * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
1088          * of x.
1089          *
1090          * @param {string} keys Messy form.
1091          * @param {boolean} unknownOk Whether unknown keys are passed
1092          *     through rather than being converted to <lt>keyname>.
1093          *     @default false
1094          * @returns {string} Canonical form.
1095          */
1096         canonicalKeys: function canonicalKeys(keys, unknownOk) {
1097             if (arguments.length === 1)
1098                 unknownOk = true;
1099             return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
1100         },
1101
1102         iterKeys: function iterKeys(keys) iter(function () {
1103             let match, re = /<.*?>?>|[^<]/g;
1104             while (match = re.exec(keys))
1105                 yield match[0];
1106         }()),
1107
1108         /**
1109          * Converts an event string into an array of pseudo-event objects.
1110          *
1111          * These objects can be used as arguments to {@link #stringify} or
1112          * {@link DOM.Event}, though they are unlikely to be much use for other
1113          * purposes. They have many of the properties you'd expect to find on a
1114          * real event, but none of the methods.
1115          *
1116          * Also may contain two "special" parameters, .dactylString and
1117          * .dactylShift these are set for characters that can never by
1118          * typed, but may appear in mappings, for example <Nop> is passed as
1119          * dactylString, and dactylShift is set when a user specifies
1120          * <S-@> where @ is a non-case-changeable, non-space character.
1121          *
1122          * @param {string} keys The string to parse.
1123          * @param {boolean} unknownOk Whether unknown keys are passed
1124          *     through rather than being converted to <lt>keyname>.
1125          *     @default false
1126          * @returns {Array[Object]}
1127          */
1128         parse: function parse(input, unknownOk) {
1129             if (isArray(input))
1130                 return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
1131
1132             if (arguments.length === 1)
1133                 unknownOk = true;
1134
1135             let out = [];
1136             for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1137                 let evt_str = match[0];
1138
1139                 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1140                                 keyCode: 0, charCode: 0, type: "keypress" };
1141
1142                 if (evt_str.length == 1) {
1143                     evt_obj.charCode = evt_str.charCodeAt(0);
1144                     evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
1145                     evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
1146                 }
1147                 else {
1148                     let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
1149                     modifier = Set(modifier.toUpperCase());
1150                     keyname = keyname.toLowerCase();
1151                     evt_obj.dactylKeyname = keyname;
1152                     if (/^u[0-9a-f]+$/.test(keyname))
1153                         keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
1154
1155                     if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
1156                                     this.key_code[keyname] || Set.has(this.pseudoKeys, keyname))) {
1157                         evt_obj.globKey  ="*" in modifier;
1158                         evt_obj.ctrlKey  ="C" in modifier;
1159                         evt_obj.altKey   ="A" in modifier;
1160                         evt_obj.shiftKey ="S" in modifier;
1161                         evt_obj.metaKey  ="M" in modifier || "⌘" in modifier;
1162                         evt_obj.dactylShift = evt_obj.shiftKey;
1163
1164                         if (keyname.length == 1) { // normal characters
1165                             if (evt_obj.shiftKey)
1166                                 keyname = keyname.toUpperCase();
1167
1168                             evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
1169                             evt_obj.charCode = keyname.charCodeAt(0);
1170                             evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
1171                         }
1172                         else if (Set.has(this.pseudoKeys, keyname)) {
1173                             evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
1174                         }
1175                         else if (/mouse$/.test(keyname)) { // mouse events
1176                             evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
1177                             evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
1178                             delete evt_obj.keyCode;
1179                             delete evt_obj.charCode;
1180                         }
1181                         else { // spaces, control characters, and <
1182                             evt_obj.keyCode = this.key_code[keyname];
1183                             evt_obj.charCode = 0;
1184                         }
1185                     }
1186                     else { // an invalid sequence starting with <, treat as a literal
1187                         out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
1188                         continue;
1189                     }
1190                 }
1191
1192                 // TODO: make a list of characters that need keyCode and charCode somewhere
1193                 if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
1194                     evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
1195                 if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
1196                     evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
1197
1198                 evt_obj.modifiers = (evt_obj.ctrlKey  && Ci.nsIDOMNSEvent.CONTROL_MASK)
1199                                   | (evt_obj.altKey   && Ci.nsIDOMNSEvent.ALT_MASK)
1200                                   | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
1201                                   | (evt_obj.metaKey  && Ci.nsIDOMNSEvent.META_MASK);
1202
1203                 out.push(evt_obj);
1204             }
1205             return out;
1206         },
1207
1208         /**
1209          * Converts the specified event to a string in dactyl key-code
1210          * notation. Returns null for an unknown event.
1211          *
1212          * @param {Event} event
1213          * @returns {string}
1214          */
1215         stringify: function stringify(event) {
1216             if (isArray(event))
1217                 return event.map(function (e) this.stringify(e), this).join("");
1218
1219             if (event.dactylString)
1220                 return event.dactylString;
1221
1222             let key = null;
1223             let modifier = "";
1224
1225             if (event.globKey)
1226                 modifier += "*-";
1227             if (event.ctrlKey)
1228                 modifier += "C-";
1229             if (event.altKey)
1230                 modifier += "A-";
1231             if (event.metaKey)
1232                 modifier += "M-";
1233
1234             if (/^key/.test(event.type)) {
1235                 let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
1236                 if (charCode == 0) {
1237                     if (event.keyCode in this.code_key) {
1238                         key = this.code_key[event.keyCode];
1239
1240                         if (event.shiftKey && (key.length > 1 || key.toUpperCase() == key.toLowerCase()
1241                                                || event.ctrlKey || event.altKey || event.metaKey)
1242                                 || event.dactylShift)
1243                             modifier += "S-";
1244                         else if (!modifier && key.length === 1)
1245                             if (event.shiftKey)
1246                                 key = key.toUpperCase();
1247                             else
1248                                 key = key.toLowerCase();
1249
1250                         if (!modifier && key.length == 1)
1251                             return key;
1252                     }
1253                 }
1254                 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1255                 //            (i.e., cntrl codes 27--31)
1256                 // ---
1257                 // For more information, see:
1258                 //     [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
1259                 //     [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
1260                 //         https://bugzilla.mozilla.org/show_bug.cgi?id=416227
1261                 //     [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
1262                 //         https://bugzilla.mozilla.org/show_bug.cgi?id=432951
1263                 // ---
1264                 //
1265                 // The following fixes are only activated if config.OS.isMacOSX.
1266                 // Technically, they prevent mappings from <C-Esc> (and
1267                 // <C-C-]> if your fancy keyboard permits such things<?>), but
1268                 // these <C-control> mappings are probably pathological (<C-Esc>
1269                 // certainly is on Windows), and so it is probably
1270                 // harmless to remove the config.OS.isMacOSX if desired.
1271                 //
1272                 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1273                     if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1274                         key = "Esc";
1275                         modifier = modifier.replace("C-", "");
1276                     }
1277                     else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1278                         key = String.fromCharCode(charCode + 64);
1279                 }
1280                 // a normal key like a, b, c, 0, etc.
1281                 else if (charCode) {
1282                     key = String.fromCharCode(charCode);
1283
1284                     if (!/^[^<\s]$/i.test(key) && key in this.key_code) {
1285                         // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
1286                         if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
1287                             modifier += "S-";
1288
1289                         key = this.code_key[this.key_code[key]];
1290                     }
1291                     else {
1292                         // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
1293                         // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
1294                         if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1295                             modifier += "S-";
1296                         if (/^\s$/.test(key))
1297                             key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
1298                         else if (modifier.length == 0)
1299                             return key;
1300                     }
1301                 }
1302                 if (key == null) {
1303                     if (event.shiftKey)
1304                         modifier += "S-";
1305                     key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1306                 }
1307                 if (key == null)
1308                     return null;
1309             }
1310             else if (event.type == "click" || event.type == "dblclick") {
1311                 if (event.shiftKey)
1312                     modifier += "S-";
1313                 if (event.type == "dblclick")
1314                     modifier += "2-";
1315                 // TODO: triple and quadruple click
1316
1317                 switch (event.button) {
1318                 case 0:
1319                     key = "LeftMouse";
1320                     break;
1321                 case 1:
1322                     key = "MiddleMouse";
1323                     break;
1324                 case 2:
1325                     key = "RightMouse";
1326                     break;
1327                 }
1328             }
1329
1330             if (key == null)
1331                 return null;
1332
1333             return "<" + modifier + key + ">";
1334         },
1335
1336         defaults: {
1337             load:   { bubbles: false },
1338             submit: { cancelable: true }
1339         },
1340
1341         types: Class.Memoize(function () iter(
1342             {
1343                 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1344                        "hover " +
1345                        "popupshowing popupshown popuphiding popuphidden " +
1346                        "contextmenu",
1347                 Key:   "keydown keypress keyup",
1348                 "":    "change command dactyl-input input submit " +
1349                        "load unload pageshow pagehide DOMContentLoaded " +
1350                        "resize scroll"
1351             }
1352         ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
1353          .flatten()
1354          .toObject()),
1355
1356         /**
1357          * Dispatches an event to an element as if it were a native event.
1358          *
1359          * @param {Node} target The DOM node to which to dispatch the event.
1360          * @param {Event} event The event to dispatch.
1361          */
1362         dispatch: Class.Memoize(function ()
1363             config.haveGecko("2b")
1364                 ? function dispatch(target, event, extra) {
1365                     try {
1366                         this.feedingEvent = extra;
1367
1368                         if (target instanceof Ci.nsIDOMElement)
1369                             // This causes a crash on Gecko<2.0, it seems.
1370                             return (target.ownerDocument || target.document || target).defaultView
1371                                    .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
1372                                    .dispatchDOMEventViaPresShell(target, event, true);
1373                         else {
1374                             target.dispatchEvent(event);
1375                             return !event.getPreventDefault();
1376                         }
1377                     }
1378                     catch (e) {
1379                         util.reportError(e);
1380                     }
1381                     finally {
1382                         this.feedingEvent = null;
1383                     }
1384                 }
1385                 : function dispatch(target, event, extra) {
1386                     try {
1387                         this.feedingEvent = extra;
1388                         target.dispatchEvent(update(event, extra));
1389                     }
1390                     finally {
1391                         this.feedingEvent = null;
1392                     }
1393                 })
1394     }),
1395
1396     createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
1397         || function (elem) {}),
1398
1399     isScrollable: Class.Memoize(function () services.has("dactyl") && services.dactyl.getScrollable
1400         ? function (elem, dir) services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
1401         : function (elem, dir) true),
1402
1403     isJSONXML: function isJSONXML(val) isArray(val) && isinstance(val[0], ["String", "Array", "XML", DOM.DOMString])
1404                                     || isObject(val) && "toDOM" in val,
1405
1406     DOMString: function DOMString(val) ({
1407         __proto__: DOMString.prototype,
1408
1409         toDOM: function toDOM(doc) doc.createTextNode(val),
1410
1411         toString: function () val
1412     }),
1413
1414     /**
1415      * The set of input element type attribute values that mark the element as
1416      * an editable field.
1417      */
1418     editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
1419                          "month", "number", "password", "range", "search",
1420                          "tel", "text", "time", "url", "week"]),
1421
1422     /**
1423      * Converts a given DOM Node, Range, or Selection to a string. If
1424      * *html* is true, the output is HTML, otherwise it is presentation
1425      * text.
1426      *
1427      * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1428      *      stringify.
1429      * @param {boolean} html Whether the output should be HTML rather
1430      *      than presentation text.
1431      */
1432     stringify: function stringify(node, html) {
1433         if (node instanceof Ci.nsISelection && node.isCollapsed)
1434             return "";
1435
1436         if (node instanceof Ci.nsIDOMNode) {
1437             let range = node.ownerDocument.createRange();
1438             range.selectNode(node);
1439             node = range;
1440         }
1441         let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1442         doc = doc.ownerDocument || doc;
1443
1444         let encoder = services.HtmlEncoder();
1445         encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1446         if (node instanceof Ci.nsISelection)
1447             encoder.setSelection(node);
1448         else if (node instanceof Ci.nsIDOMRange)
1449             encoder.setRange(node);
1450
1451         let str = services.String(encoder.encodeToString());
1452         if (html)
1453             return str.data;
1454
1455         let [result, length] = [{}, {}];
1456         services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
1457         return result.value.QueryInterface(Ci.nsISupportsString).data;
1458     },
1459
1460     /**
1461      * Compiles a CSS spec and XPath pattern matcher based on the given
1462      * list. List elements prefixed with "xpath:" are parsed as XPath
1463      * patterns, while other elements are parsed as CSS specs. The
1464      * returned function will, given a node, return an iterator of all
1465      * descendants of that node which match the given specs.
1466      *
1467      * @param {[string]} list The list of patterns to match.
1468      * @returns {function(Node)}
1469      */
1470     compileMatcher: function compileMatcher(list) {
1471         let xpath = [], css = [];
1472         for (let elem in values(list))
1473             if (/^xpath:/.test(elem))
1474                 xpath.push(elem.substr(6));
1475             else
1476                 css.push(elem);
1477
1478         return update(
1479             function matcher(node) {
1480                 if (matcher.xpath)
1481                     for (let elem in DOM.XPath(matcher.xpath, node))
1482                         yield elem;
1483
1484                 if (matcher.css)
1485                     for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1486                         yield elem;
1487             }, {
1488                 css: css.join(", "),
1489                 xpath: xpath.join(" | ")
1490             });
1491     },
1492
1493     /**
1494      * Validates a list as input for {@link #compileMatcher}. Returns
1495      * true if and only if every element of the list is a valid XPath or
1496      * CSS selector.
1497      *
1498      * @param {[string]} list The list of patterns to test
1499      * @returns {boolean} True when the patterns are all valid.
1500      */
1501     validateMatcher: function validateMatcher(list) {
1502         return this.testValues(list, DOM.closure.testMatcher);
1503     },
1504
1505     testMatcher: function testMatcher(value) {
1506         let evaluator = services.XPathEvaluator();
1507         let node = services.XMLDocument();
1508         if (/^xpath:/.test(value))
1509             util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
1510         else
1511             util.withProperErrors("querySelector", node, value);
1512         return true;
1513     },
1514
1515     /**
1516      * Converts HTML special characters in *str* to the equivalent HTML
1517      * entities.
1518      *
1519      * @param {string} str
1520      * @param {boolean} simple If true, only escape for the simple case
1521      *     of text nodes.
1522      * @returns {string}
1523      */
1524     escapeHTML: function escapeHTML(str, simple) {
1525         let map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" };
1526         let regexp = simple ? /[<>]/g : /['"&<>]/g;
1527         return str.replace(regexp, function (m) map[m]);
1528     },
1529
1530     /**
1531      * Converts an E4X XML literal to a DOM node. Any attribute named
1532      * highlight is present, it is transformed into dactyl:highlight,
1533      * and the named highlight groups are guaranteed to be loaded.
1534      *
1535      * @param {Node} node
1536      * @param {Document} doc
1537      * @param {Object} nodes If present, nodes with the "key" attribute are
1538      *     stored here, keyed to the value thereof.
1539      * @returns {Node}
1540      */
1541     fromXML: deprecated("DOM.fromJSON", { get: function fromXML()
1542                prefs.get("javascript.options.xml.chrome") !== false
1543             && require("dom-e4x").fromXML }),
1544
1545     fromJSON: update(function fromJSON(xml, doc, nodes, namespaces) {
1546         if (!doc)
1547             doc = document;
1548
1549         function tag(args, namespaces) {
1550             let _namespaces = namespaces;
1551
1552             // Deal with common error case
1553             if (args == null) {
1554                 util.reportError(Error("Unexpected null when processing XML."));
1555                 args = ["html:i", {}, "[NULL]"];
1556             }
1557
1558             if (isinstance(args, ["String", "Number", "Boolean", _]))
1559                 return doc.createTextNode(args);
1560             if (isXML(args))
1561                 return DOM.fromXML(args, doc, nodes);
1562             if (isObject(args) && "toDOM" in args)
1563                 return args.toDOM(doc, namespaces, nodes);
1564             if (args instanceof Ci.nsIDOMNode)
1565                 return args;
1566             if (args instanceof DOM)
1567                 return args.fragment();
1568             if ("toJSONXML" in args)
1569                 args = args.toJSONXML();
1570
1571             let [name, attr] = args;
1572
1573             if (!isString(name) || args.length == 0 || name === "") {
1574                 var frag = doc.createDocumentFragment();
1575                 Array.forEach(args, function (arg) {
1576                     if (!isArray(arg[0]))
1577                         arg = [arg];
1578                     arg.forEach(function (arg) {
1579                         frag.appendChild(tag(arg, namespaces));
1580                     });
1581                 });
1582                 return frag;
1583             }
1584
1585             attr = attr || {};
1586
1587             function parseNamespace(name) DOM.parseNamespace(name, namespaces);
1588
1589             // FIXME: Surely we can do better.
1590             for (var key in attr) {
1591                 if (/^xmlns(?:$|:)/.test(key)) {
1592                     if (_namespaces === namespaces)
1593                         namespaces = Object.create(namespaces);
1594
1595                     namespaces[key.substr(6)] = namespaces[attr[key]] || attr[key];
1596                 }}
1597
1598             var args = Array.slice(args, 2);
1599             var vals = parseNamespace(name);
1600             var elem = doc.createElementNS(vals[0] || namespaces[""],
1601                                            name);
1602
1603             for (var key in attr)
1604                 if (!/^xmlns(?:$|:)/.test(key)) {
1605                     var val = attr[key];
1606                     if (nodes && key == "key")
1607                         nodes[val] = elem;
1608
1609                     vals = parseNamespace(key);
1610                     if (key == "highlight")
1611                         ;
1612                     else if (typeof val == "function")
1613                         elem.addEventListener(key.replace(/^on/, ""), val, false);
1614                     else
1615                         elem.setAttributeNS(vals[0] || "", key, val);
1616                 }
1617             args.forEach(function (e) {
1618                 elem.appendChild(tag(e, namespaces));
1619             });
1620
1621             if ("highlight" in attr)
1622                 highlight.highlightNode(elem, attr.highlight, nodes || true);
1623             return elem;
1624         }
1625
1626         if (namespaces)
1627             namespaces = update({}, fromJSON.namespaces, namespaces);
1628         else
1629             namespaces = fromJSON.namespaces;
1630
1631         return tag(xml, namespaces);
1632     }, {
1633         namespaces: {
1634             "": "http://www.w3.org/1999/xhtml",
1635             dactyl: String(NS),
1636             html: "http://www.w3.org/1999/xhtml",
1637             xmlns: "http://www.w3.org/2000/xmlns/",
1638             xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
1639         }
1640     }),
1641
1642     toXML: function toXML(xml) {
1643         // Meh. For now.
1644         let doc = services.XMLDocument();
1645         let node = this.fromJSON(xml, doc);
1646         return services.XMLSerializer()
1647                        .serializeToString(node);
1648     },
1649
1650     toPrettyXML: function toPrettyXML(xml, asXML, indent, namespaces) {
1651         const INDENT = indent || "    ";
1652
1653         const EMPTY = Set("area base basefont br col frame hr img input isindex link meta param"
1654                             .split(" "));
1655
1656         function namespaced(namespaces, namespace, localName) {
1657             for (let [k, v] in Iterator(namespaces))
1658                 if (v == namespace)
1659                     return (k ? k + ":" + localName : localName);
1660
1661             throw Error("No such namespace");
1662         }
1663
1664         function isFragment(args) !isString(args[0]) || args.length == 0 || args[0] === "";
1665
1666         function hasString(args) {
1667             return args.some(function (a) isString(a) || isFragment(a) && hasString(a));
1668         }
1669
1670         function isStrings(args) {
1671             if (!isArray(args))
1672                 return util.dump("ARGS: " + {}.toString.call(args) + " " + args), false;
1673             return args.every(function (a) isinstance(a, ["String", DOM.DOMString]) || isFragment(a) && isStrings(a));
1674         }
1675
1676         function tag(args, namespaces, indent) {
1677             let _namespaces = namespaces;
1678
1679             if (args == "")
1680                 return "";
1681
1682             if (isinstance(args, ["String", "Number", "Boolean", _, DOM.DOMString]))
1683                 return indent +
1684                        DOM.escapeHTML(String(args), true);
1685
1686             if (isXML(args))
1687                 return indent +
1688                        args.toXMLString()
1689                            .replace(/^/m, indent);
1690
1691             if (isObject(args) && "toDOM" in args)
1692                 return indent +
1693                        services.XMLSerializer()
1694                                .serializeToString(args.toDOM(services.XMLDocument()))
1695                                .replace(/^/m, indent);
1696
1697             if (args instanceof Ci.nsIDOMNode)
1698                 return indent +
1699                        services.XMLSerializer()
1700                                .serializeToString(args)
1701                                .replace(/^/m, indent);
1702
1703             if ("toJSONXML" in args)
1704                 args = args.toJSONXML();
1705
1706             // Deal with common error case
1707             if (args == null) {
1708                 util.reportError(Error("Unexpected null when processing XML."));
1709                 return "[NULL]";
1710             }
1711
1712             let [name, attr] = args;
1713
1714             if (isFragment(args)) {
1715                 let res = [];
1716                 let join = isArray(args) && isStrings(args) ? "" : "\n";
1717                 Array.forEach(args, function (arg) {
1718                     if (!isArray(arg[0]))
1719                         arg = [arg];
1720
1721                     let contents = [];
1722                     arg.forEach(function (arg) {
1723                         let string = tag(arg, namespaces, indent);
1724                         if (string)
1725                             contents.push(string);
1726                     });
1727                     if (contents.length)
1728                         res.push(contents.join("\n"), join);
1729                 });
1730                 if (res[res.length - 1] == join)
1731                     res.pop();
1732                 return res.join("");
1733             }
1734
1735             attr = attr || {};
1736
1737             function parseNamespace(name) {
1738                 var m = /^(?:(.*):)?(.*)$/.exec(name);
1739                 return [namespaces[m[1]], m[2]];
1740             }
1741
1742             // FIXME: Surely we can do better.
1743             let skipAttr = {};
1744             for (var key in attr) {
1745                 if (/^xmlns(?:$|:)/.test(key)) {
1746                     if (_namespaces === namespaces)
1747                         namespaces = update({}, namespaces);
1748
1749                     let ns = namespaces[attr[key]] || attr[key];
1750                     if (ns == namespaces[key.substr(6)])
1751                         skipAttr[key] = true;
1752
1753                     attr[key] = namespaces[key.substr(6)] = ns;
1754                 }}
1755
1756             var args = Array.slice(args, 2);
1757             var vals = parseNamespace(name);
1758
1759             let res = [indent, "<", name];
1760
1761             for (let [key, val] in Iterator(attr)) {
1762                 if (Set.has(skipAttr, key))
1763                     continue;
1764
1765                 let vals = parseNamespace(key);
1766                 if (typeof val == "function") {
1767                     key = key.replace(/^(?:on)?/, "on");
1768                     val = val.toSource() + "(event)";
1769                 }
1770
1771                 if (key != "highlight" || vals[0] == String(NS))
1772                     res.push(" ", key, '="', DOM.escapeHTML(val), '"');
1773                 else
1774                     res.push(" ", namespaced(namespaces, String(NS), "highlight"),
1775                              '="', DOM.escapeHTML(val), '"');
1776             }
1777
1778             if ((vals[0] || namespaces[""]) == String(XHTML) && Set.has(EMPTY, vals[1])
1779                     || asXML && !args.length)
1780                 res.push("/>");
1781             else {
1782                 res.push(">");
1783
1784                 if (isStrings(args))
1785                     res.push(args.map(function (e) tag(e, namespaces, "")).join(""),
1786                              "</", name, ">");
1787                 else {
1788                     let contents = [];
1789                     args.forEach(function (e) {
1790                         let string = tag(e, namespaces, indent + INDENT);
1791                         if (string)
1792                             contents.push(string);
1793                     });
1794
1795                     res.push("\n", contents.join("\n"), "\n", indent, "</", name, ">");
1796                 }
1797             }
1798
1799             return res.join("");
1800         }
1801
1802         if (namespaces)
1803             namespaces = update({}, DOM.fromJSON.namespaces, namespaces);
1804         else
1805             namespaces = DOM.fromJSON.namespaces;
1806
1807         return tag(xml, namespaces, "");
1808     },
1809
1810     parseNamespace: function parseNamespace(name, namespaces) {
1811         if (name == "xmlns")
1812             return [DOM.fromJSON.namespaces.xmlns, "xmlns"];
1813
1814         var m = /^(?:(.*):)?(.*)$/.exec(name);
1815         return [(namespaces || DOM.fromJSON.namespaces)[m[1]],
1816                 m[2]];
1817     },
1818
1819     /**
1820      * Evaluates an XPath expression in the current or provided
1821      * document. It provides the xhtml, xhtml2 and dactyl XML
1822      * namespaces. The result may be used as an iterator.
1823      *
1824      * @param {string} expression The XPath expression to evaluate.
1825      * @param {Node} elem The context element.
1826      * @param {boolean} asIterator Whether to return the results as an
1827      *     XPath iterator.
1828      * @param {object} namespaces Additional namespaces to recognize.
1829      *     @optional
1830      * @returns {Object} Iterable result of the evaluation.
1831      */
1832     XPath: update(
1833         function XPath(expression, elem, asIterator, namespaces) {
1834             try {
1835                 let doc = elem.ownerDocument || elem;
1836
1837                 if (isArray(expression))
1838                     expression = DOM.makeXPath(expression);
1839
1840                 let resolver = XPath.resolver;
1841                 if (namespaces) {
1842                     namespaces = update({}, DOM.namespaces, namespaces);
1843                     resolver = function (prefix) namespaces[prefix] || null;
1844                 }
1845
1846                 let result = doc.evaluate(expression, elem,
1847                     resolver,
1848                     asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1849                     null
1850                 );
1851
1852                 let res = {
1853                     iterateNext: function () result.iterateNext(),
1854                     get resultType() result.resultType,
1855                     get snapshotLength() result.snapshotLength,
1856                     snapshotItem: function (i) result.snapshotItem(i),
1857                     __iterator__:
1858                         asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
1859                                    : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
1860                 };
1861                 return res;
1862             }
1863             catch (e) {
1864                 throw e.stack ? e : Error(e);
1865             }
1866         },
1867         {
1868             resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
1869         }),
1870
1871     /**
1872      * Returns an XPath union expression constructed from the specified node
1873      * tests. An expression is built with node tests for both the null and
1874      * XHTML namespaces. See {@link DOM.XPath}.
1875      *
1876      * @param nodes {Array(string)}
1877      * @returns {string}
1878      */
1879     makeXPath: function makeXPath(nodes) {
1880         return array(nodes).map(util.debrace).flatten()
1881                            .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
1882                            .map(function (node) "//" + node).join(" | ");
1883     },
1884
1885     namespaces: {
1886         xul: XUL,
1887         xhtml: XHTML,
1888         html: XHTML,
1889         xhtml2: "http://www.w3.org/2002/06/xhtml2",
1890         dactyl: NS
1891     },
1892
1893     namespaceNames: Class.Memoize(function ()
1894         iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
1895 });
1896
1897 Object.keys(DOM.Event.types).forEach(function (event) {
1898     let name = event.replace(/-(.)/g, function (m, m1) m1.toUpperCase());
1899     if (!Set.has(DOM.prototype, name))
1900         DOM.prototype[name] =
1901             function _event(arg, extra) {
1902                 return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
1903             };
1904 });
1905
1906 var $ = DOM;
1907
1908 endModule();
1909
1910 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1911
1912 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: