]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/dom.jsm
Import r6976 from upstream hg supporting Firefox up to 25.*
[dactyl.git] / common / modules / dom.jsm
1 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2008-2013 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) 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(elem => elem.querySelectorAll(val));
162     },
163
164     findAnon: function findAnon(attr, val) {
165         return this.map(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) && !(tmp instanceof Ci.nsIDOMNode) && "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(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(elem => elem.parentNode),
249
250     get children() this.map(elem => Array.filter(elem.childNodes,
251                                                  e => e instanceof Ci.nsIDOMElement),
252                             this),
253
254     get contents() this.map(elem => elem.childNodes, this),
255
256     get siblings() this.map(elem => Array.filter(elem.parentNode.childNodes,
257                                                  e => e != elem && e instanceof Ci.nsIDOMElement),
258                             this),
259
260     get siblingsBefore() this.all(elem => elem.previousElementSibling),
261     get siblingsAfter() this.all(elem => elem.nextElementSibling),
262
263     get allSiblingsBefore() this.all(elem => elem.previousSibling),
264     get allSiblingsAfter() this.all(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(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                                 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, m0 => "-" + m0.toLowerCase()),
664
665         property: function (name) name.replace(/-(.)/g, (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(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(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 = () => 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                            elem => elem.innerHTML,
797                            util.wrapCallback((elem, val) => { elem.innerHTML = val; }));
798     },
799
800     text: function text(txt, self) {
801         return this.getSet(arguments,
802                            elem => elem.textContent,
803                            (elem, val) => { elem.textContent = val; });
804     },
805
806     val: function val(txt) {
807         return this.getSet(arguments,
808                            elem => elem.value,
809                            (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(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, (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 true
1094          * @returns {string} Canonical form.
1095          */
1096         canonicalKeys: function canonicalKeys(keys, unknownOk=true) {
1097             return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
1098         },
1099
1100         iterKeys: function iterKeys(keys) iter(function () {
1101             let match, re = /<.*?>?>|[^<]/g;
1102             while (match = re.exec(keys))
1103                 yield match[0];
1104         }()),
1105
1106         /**
1107          * Converts an event string into an array of pseudo-event objects.
1108          *
1109          * These objects can be used as arguments to {@link #stringify} or
1110          * {@link DOM.Event}, though they are unlikely to be much use for other
1111          * purposes. They have many of the properties you'd expect to find on a
1112          * real event, but none of the methods.
1113          *
1114          * Also may contain two "special" parameters, .dactylString and
1115          * .dactylShift these are set for characters that can never by
1116          * typed, but may appear in mappings, for example <Nop> is passed as
1117          * dactylString, and dactylShift is set when a user specifies
1118          * <S-@> where @ is a non-case-changeable, non-space character.
1119          *
1120          * @param {string} keys The string to parse.
1121          * @param {boolean} unknownOk Whether unknown keys are passed
1122          *     through rather than being converted to <lt>keyname>.
1123          *     @default true
1124          * @returns {Array[Object]}
1125          */
1126         parse: function parse(input, unknownOk=true) {
1127             if (isArray(input))
1128                 return array.flatten(input.map(k => this.parse(k, unknownOk)));
1129
1130             let out = [];
1131             for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1132                 let evt_str = match[0];
1133
1134                 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1135                                 keyCode: 0, charCode: 0, type: "keypress" };
1136
1137                 if (evt_str.length == 1) {
1138                     evt_obj.charCode = evt_str.charCodeAt(0);
1139                     evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
1140                     evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
1141                 }
1142                 else {
1143                     let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
1144                     modifier = Set(modifier.toUpperCase());
1145                     keyname = keyname.toLowerCase();
1146                     evt_obj.dactylKeyname = keyname;
1147                     if (/^u[0-9a-f]+$/.test(keyname))
1148                         keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
1149
1150                     if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
1151                                     this.key_code[keyname] || Set.has(this.pseudoKeys, keyname))) {
1152                         evt_obj.globKey  ="*" in modifier;
1153                         evt_obj.ctrlKey  ="C" in modifier;
1154                         evt_obj.altKey   ="A" in modifier;
1155                         evt_obj.shiftKey ="S" in modifier;
1156                         evt_obj.metaKey  ="M" in modifier || "⌘" in modifier;
1157                         evt_obj.dactylShift = evt_obj.shiftKey;
1158
1159                         if (keyname.length == 1) { // normal characters
1160                             if (evt_obj.shiftKey)
1161                                 keyname = keyname.toUpperCase();
1162
1163                             evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
1164                             evt_obj.charCode = keyname.charCodeAt(0);
1165                             evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
1166                         }
1167                         else if (Set.has(this.pseudoKeys, keyname)) {
1168                             evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
1169                         }
1170                         else if (/mouse$/.test(keyname)) { // mouse events
1171                             evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
1172                             evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
1173                             delete evt_obj.keyCode;
1174                             delete evt_obj.charCode;
1175                         }
1176                         else { // spaces, control characters, and <
1177                             evt_obj.keyCode = this.key_code[keyname];
1178                             evt_obj.charCode = 0;
1179                         }
1180                     }
1181                     else { // an invalid sequence starting with <, treat as a literal
1182                         out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
1183                         continue;
1184                     }
1185                 }
1186
1187                 // TODO: make a list of characters that need keyCode and charCode somewhere
1188                 if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
1189                     evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
1190                 if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
1191                     evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
1192
1193                 evt_obj.modifiers = (evt_obj.ctrlKey  && Ci.nsIDOMNSEvent.CONTROL_MASK)
1194                                   | (evt_obj.altKey   && Ci.nsIDOMNSEvent.ALT_MASK)
1195                                   | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
1196                                   | (evt_obj.metaKey  && Ci.nsIDOMNSEvent.META_MASK);
1197
1198                 out.push(evt_obj);
1199             }
1200             return out;
1201         },
1202
1203         /**
1204          * Converts the specified event to a string in dactyl key-code
1205          * notation. Returns null for an unknown event.
1206          *
1207          * @param {Event} event
1208          * @returns {string}
1209          */
1210         stringify: function stringify(event) {
1211             if (isArray(event))
1212                 return event.map(e => this.stringify(e)).join("");
1213
1214             if (event.dactylString)
1215                 return event.dactylString;
1216
1217             let key = null;
1218             let modifier = "";
1219
1220             if (event.globKey)
1221                 modifier += "*-";
1222             if (event.ctrlKey)
1223                 modifier += "C-";
1224             if (event.altKey)
1225                 modifier += "A-";
1226             if (event.metaKey)
1227                 modifier += "M-";
1228
1229             if (/^key/.test(event.type)) {
1230                 let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
1231                 if (charCode == 0) {
1232                     if (event.keyCode in this.code_key) {
1233                         key = this.code_key[event.keyCode];
1234
1235                         if (event.shiftKey && (key.length > 1 || key.toUpperCase() == key.toLowerCase()
1236                                                || event.ctrlKey || event.altKey || event.metaKey)
1237                                 || event.dactylShift)
1238                             modifier += "S-";
1239                         else if (!modifier && key.length === 1)
1240                             if (event.shiftKey)
1241                                 key = key.toUpperCase();
1242                             else
1243                                 key = key.toLowerCase();
1244
1245                         if (!modifier && key.length == 1)
1246                             return key;
1247                     }
1248                 }
1249                 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1250                 //            (i.e., cntrl codes 27--31)
1251                 // ---
1252                 // For more information, see:
1253                 //     [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
1254                 //     [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
1255                 //         https://bugzilla.mozilla.org/show_bug.cgi?id=416227
1256                 //     [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
1257                 //         https://bugzilla.mozilla.org/show_bug.cgi?id=432951
1258                 // ---
1259                 //
1260                 // The following fixes are only activated if config.OS.isMacOSX.
1261                 // Technically, they prevent mappings from <C-Esc> (and
1262                 // <C-C-]> if your fancy keyboard permits such things<?>), but
1263                 // these <C-control> mappings are probably pathological (<C-Esc>
1264                 // certainly is on Windows), and so it is probably
1265                 // harmless to remove the config.OS.isMacOSX if desired.
1266                 //
1267                 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1268                     if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1269                         key = "Esc";
1270                         modifier = modifier.replace("C-", "");
1271                     }
1272                     else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1273                         key = String.fromCharCode(charCode + 64);
1274                 }
1275                 // a normal key like a, b, c, 0, etc.
1276                 else if (charCode) {
1277                     key = String.fromCharCode(charCode);
1278
1279                     if (!/^[^<\s]$/i.test(key) && key in this.key_code) {
1280                         // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
1281                         if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
1282                             modifier += "S-";
1283
1284                         key = this.code_key[this.key_code[key]];
1285                     }
1286                     else {
1287                         // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
1288                         // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
1289                         if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1290                             modifier += "S-";
1291                         if (/^\s$/.test(key))
1292                             key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
1293                         else if (modifier.length == 0)
1294                             return key;
1295                     }
1296                 }
1297                 if (key == null) {
1298                     if (event.shiftKey)
1299                         modifier += "S-";
1300                     key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1301                 }
1302                 if (key == null)
1303                     return null;
1304             }
1305             else if (event.type == "click" || event.type == "dblclick") {
1306                 if (event.shiftKey)
1307                     modifier += "S-";
1308                 if (event.type == "dblclick")
1309                     modifier += "2-";
1310                 // TODO: triple and quadruple click
1311
1312                 switch (event.button) {
1313                 case 0:
1314                     key = "LeftMouse";
1315                     break;
1316                 case 1:
1317                     key = "MiddleMouse";
1318                     break;
1319                 case 2:
1320                     key = "RightMouse";
1321                     break;
1322                 }
1323             }
1324
1325             if (key == null)
1326                 return null;
1327
1328             return "<" + modifier + key + ">";
1329         },
1330
1331         defaults: {
1332             load:   { bubbles: false },
1333             submit: { cancelable: true }
1334         },
1335
1336         types: Class.Memoize(() => iter(
1337             {
1338                 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1339                        "hover " +
1340                        "popupshowing popupshown popuphiding popuphidden " +
1341                        "contextmenu",
1342                 Key:   "keydown keypress keyup",
1343                 "":    "change command dactyl-input input submit " +
1344                        "load unload pageshow pagehide DOMContentLoaded " +
1345                        "resize scroll"
1346             }
1347         ).map(([k, v]) => v.split(" ").map(v => [v, k]))
1348          .flatten()
1349          .toObject()),
1350
1351         /**
1352          * Dispatches an event to an element as if it were a native event.
1353          *
1354          * @param {Node} target The DOM node to which to dispatch the event.
1355          * @param {Event} event The event to dispatch.
1356          */
1357         dispatch: Class.Memoize(function ()
1358             config.haveGecko("2b")
1359                 ? function dispatch(target, event, extra) {
1360                     try {
1361                         this.feedingEvent = extra;
1362
1363                         if (target instanceof Ci.nsIDOMElement)
1364                             // This causes a crash on Gecko<2.0, it seems.
1365                             return (target.ownerDocument || target.document || target).defaultView
1366                                    .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
1367                                    .dispatchDOMEventViaPresShell(target, event, true);
1368                         else {
1369                             target.dispatchEvent(event);
1370                             return !event.defaultPrevented;
1371                         }
1372                     }
1373                     catch (e) {
1374                         util.reportError(e);
1375                     }
1376                     finally {
1377                         this.feedingEvent = null;
1378                     }
1379                 }
1380                 : function dispatch(target, event, extra) {
1381                     try {
1382                         this.feedingEvent = extra;
1383                         target.dispatchEvent(update(event, extra));
1384                     }
1385                     finally {
1386                         this.feedingEvent = null;
1387                     }
1388                 })
1389     }),
1390
1391     createContents: Class.Memoize(() => services.has("dactyl") && services.dactyl.createContents
1392         || (elem => {})),
1393
1394     isScrollable: Class.Memoize(() => services.has("dactyl") && services.dactyl.getScrollable
1395         ? (elem, dir) => services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
1396         : (elem, dir) => true),
1397
1398     isJSONXML: function isJSONXML(val) isArray(val) && isinstance(val[0], ["String", "Array", "XML", DOM.DOMString])
1399                                     || isObject(val) && "toDOM" in val,
1400
1401     DOMString: function DOMString(val) ({
1402         __proto__: DOMString.prototype,
1403
1404         toDOM: function toDOM(doc) doc.createTextNode(val),
1405
1406         toString: function () val
1407     }),
1408
1409     /**
1410      * The set of input element type attribute values that mark the element as
1411      * an editable field.
1412      */
1413     editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
1414                          "month", "number", "password", "range", "search",
1415                          "tel", "text", "time", "url", "week"]),
1416
1417     /**
1418      * Converts a given DOM Node, Range, or Selection to a string. If
1419      * *html* is true, the output is HTML, otherwise it is presentation
1420      * text.
1421      *
1422      * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1423      *      stringify.
1424      * @param {boolean} html Whether the output should be HTML rather
1425      *      than presentation text.
1426      */
1427     stringify: function stringify(node, html) {
1428         if (node instanceof Ci.nsISelection && node.isCollapsed)
1429             return "";
1430
1431         if (node instanceof Ci.nsIDOMNode) {
1432             let range = node.ownerDocument.createRange();
1433             range.selectNode(node);
1434             node = range;
1435         }
1436         let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1437         doc = doc.ownerDocument || doc;
1438
1439         let encoder = services.HtmlEncoder();
1440         encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1441         if (node instanceof Ci.nsISelection)
1442             encoder.setSelection(node);
1443         else if (node instanceof Ci.nsIDOMRange)
1444             encoder.setRange(node);
1445
1446         let str = services.String(encoder.encodeToString());
1447         if (html)
1448             return str.data;
1449
1450         let [result, length] = [{}, {}];
1451         services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
1452         return result.value.QueryInterface(Ci.nsISupportsString).data;
1453     },
1454
1455     /**
1456      * Compiles a CSS spec and XPath pattern matcher based on the given
1457      * list. List elements prefixed with "xpath:" are parsed as XPath
1458      * patterns, while other elements are parsed as CSS specs. The
1459      * returned function will, given a node, return an iterator of all
1460      * descendants of that node which match the given specs.
1461      *
1462      * @param {[string]} list The list of patterns to match.
1463      * @returns {function(Node)}
1464      */
1465     compileMatcher: function compileMatcher(list) {
1466         let xpath = [], css = [];
1467         for (let elem in values(list))
1468             if (/^xpath:/.test(elem))
1469                 xpath.push(elem.substr(6));
1470             else
1471                 css.push(elem);
1472
1473         return update(
1474             function matcher(node) {
1475                 if (matcher.xpath)
1476                     for (let elem in DOM.XPath(matcher.xpath, node))
1477                         yield elem;
1478
1479                 if (matcher.css)
1480                     for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1481                         yield elem;
1482             }, {
1483                 css: css.join(", "),
1484                 xpath: xpath.join(" | ")
1485             });
1486     },
1487
1488     /**
1489      * Validates a list as input for {@link #compileMatcher}. Returns
1490      * true if and only if every element of the list is a valid XPath or
1491      * CSS selector.
1492      *
1493      * @param {[string]} list The list of patterns to test
1494      * @returns {boolean} True when the patterns are all valid.
1495      */
1496     validateMatcher: function validateMatcher(list) {
1497         return this.testValues(list, DOM.closure.testMatcher);
1498     },
1499
1500     testMatcher: function testMatcher(value) {
1501         let evaluator = services.XPathEvaluator();
1502         let node = services.XMLDocument();
1503         if (/^xpath:/.test(value))
1504             util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
1505         else
1506             util.withProperErrors("querySelector", node, value);
1507         return true;
1508     },
1509
1510     /**
1511      * Converts HTML special characters in *str* to the equivalent HTML
1512      * entities.
1513      *
1514      * @param {string} str
1515      * @param {boolean} simple If true, only escape for the simple case
1516      *     of text nodes.
1517      * @returns {string}
1518      */
1519     escapeHTML: function escapeHTML(str, simple) {
1520         let map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" };
1521         let regexp = simple ? /[<>]/g : /['"&<>]/g;
1522         return str.replace(regexp, m => map[m]);
1523     },
1524
1525     /**
1526      * Converts an E4X XML literal to a DOM node. Any attribute named
1527      * highlight is present, it is transformed into dactyl:highlight,
1528      * and the named highlight groups are guaranteed to be loaded.
1529      *
1530      * @param {Node} node
1531      * @param {Document} doc
1532      * @param {Object} nodes If present, nodes with the "key" attribute are
1533      *     stored here, keyed to the value thereof.
1534      * @returns {Node}
1535      */
1536     fromXML: deprecated("DOM.fromJSON", { get: function fromXML()
1537                prefs.get("javascript.options.xml.chrome") !== false
1538             && require("dom-e4x").fromXML }),
1539
1540     fromJSON: update(function fromJSON(xml, doc, nodes, namespaces) {
1541         if (!doc)
1542             doc = document;
1543
1544         function tag(args, namespaces) {
1545             let _namespaces = namespaces;
1546
1547             // Deal with common error case
1548             if (args == null) {
1549                 util.reportError(Error("Unexpected null when processing XML."));
1550                 args = ["html:i", {}, "[NULL]"];
1551             }
1552
1553             if (isinstance(args, ["String", "Number", "Boolean", _]))
1554                 return doc.createTextNode(args);
1555             if (isXML(args))
1556                 return DOM.fromXML(args, doc, nodes);
1557             if (isObject(args) && "toDOM" in args)
1558                 return args.toDOM(doc, namespaces, nodes);
1559             if (args instanceof Ci.nsIDOMNode)
1560                 return args;
1561             if (args instanceof DOM)
1562                 return args.fragment();
1563             if ("toJSONXML" in args)
1564                 args = args.toJSONXML();
1565
1566             let [name, attr] = args;
1567
1568             if (!isString(name) || args.length == 0 || name === "") {
1569                 var frag = doc.createDocumentFragment();
1570                 Array.forEach(args, function (arg) {
1571                     if (!isArray(arg[0]))
1572                         arg = [arg];
1573                     arg.forEach(function (arg) {
1574                         frag.appendChild(tag(arg, namespaces));
1575                     });
1576                 });
1577                 return frag;
1578             }
1579
1580             attr = attr || {};
1581
1582             function parseNamespace(name) DOM.parseNamespace(name, namespaces);
1583
1584             // FIXME: Surely we can do better.
1585             for (var key in attr) {
1586                 if (/^xmlns(?:$|:)/.test(key)) {
1587                     if (_namespaces === namespaces)
1588                         namespaces = Object.create(namespaces);
1589
1590                     namespaces[key.substr(6)] = namespaces[attr[key]] || attr[key];
1591                 }}
1592
1593             var args = Array.slice(args, 2);
1594             var vals = parseNamespace(name);
1595             var elem = doc.createElementNS(vals[0] || namespaces[""],
1596                                            name);
1597
1598             for (var key in attr)
1599                 if (!/^xmlns(?:$|:)/.test(key)) {
1600                     var val = attr[key];
1601                     if (nodes && key == "key")
1602                         nodes[val] = elem;
1603
1604                     vals = parseNamespace(key);
1605                     if (key == "highlight")
1606                         ;
1607                     else if (typeof val == "function")
1608                         elem.addEventListener(key.replace(/^on/, ""), val, false);
1609                     else
1610                         elem.setAttributeNS(vals[0] || "", key, val);
1611                 }
1612             args.forEach(function (e) {
1613                 elem.appendChild(tag(e, namespaces));
1614             });
1615
1616             if ("highlight" in attr)
1617                 highlight.highlightNode(elem, attr.highlight, nodes || true);
1618             return elem;
1619         }
1620
1621         if (namespaces)
1622             namespaces = update({}, fromJSON.namespaces, namespaces);
1623         else
1624             namespaces = fromJSON.namespaces;
1625
1626         return tag(xml, namespaces);
1627     }, {
1628         namespaces: {
1629             "": "http://www.w3.org/1999/xhtml",
1630             dactyl: String(NS),
1631             html: "http://www.w3.org/1999/xhtml",
1632             xmlns: "http://www.w3.org/2000/xmlns/",
1633             xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
1634         }
1635     }),
1636
1637     toXML: function toXML(xml) {
1638         // Meh. For now.
1639         let doc = services.XMLDocument();
1640         let node = this.fromJSON(xml, doc);
1641         return services.XMLSerializer()
1642                        .serializeToString(node);
1643     },
1644
1645     toPrettyXML: function toPrettyXML(xml, asXML, indent, namespaces) {
1646         const INDENT = indent || "    ";
1647
1648         const EMPTY = Set("area base basefont br col frame hr img input isindex link meta param"
1649                             .split(" "));
1650
1651         function namespaced(namespaces, namespace, localName) {
1652             for (let [k, v] in Iterator(namespaces))
1653                 if (v == namespace)
1654                     return (k ? k + ":" + localName : localName);
1655
1656             throw Error("No such namespace");
1657         }
1658
1659         function isFragment(args) !isString(args[0]) || args.length == 0 || args[0] === "";
1660
1661         function hasString(args) {
1662             return args.some(a => (isString(a) || isFragment(a) && hasString(a)));
1663         }
1664
1665         function isStrings(args) {
1666             if (!isArray(args))
1667                 return util.dump("ARGS: " + {}.toString.call(args) + " " + args), false;
1668             return args.every(a => (isinstance(a, ["String", DOM.DOMString]) || isFragment(a) && isStrings(a)));
1669         }
1670
1671         function tag(args, namespaces, indent) {
1672             let _namespaces = namespaces;
1673
1674             if (args == "")
1675                 return "";
1676
1677             if (isinstance(args, ["String", "Number", "Boolean", _, DOM.DOMString]))
1678                 return indent +
1679                        DOM.escapeHTML(String(args), true);
1680
1681             if (isXML(args))
1682                 return indent +
1683                        args.toXMLString()
1684                            .replace(/^/m, indent);
1685
1686             if (isObject(args) && "toDOM" in args)
1687                 return indent +
1688                        services.XMLSerializer()
1689                                .serializeToString(args.toDOM(services.XMLDocument()))
1690                                .replace(/^/m, indent);
1691
1692             if (args instanceof Ci.nsIDOMNode)
1693                 return indent +
1694                        services.XMLSerializer()
1695                                .serializeToString(args)
1696                                .replace(/^/m, indent);
1697
1698             if ("toJSONXML" in args)
1699                 args = args.toJSONXML();
1700
1701             // Deal with common error case
1702             if (args == null) {
1703                 util.reportError(Error("Unexpected null when processing XML."));
1704                 return "[NULL]";
1705             }
1706
1707             let [name, attr] = args;
1708
1709             if (isFragment(args)) {
1710                 let res = [];
1711                 let join = isArray(args) && isStrings(args) ? "" : "\n";
1712                 Array.forEach(args, function (arg) {
1713                     if (!isArray(arg[0]))
1714                         arg = [arg];
1715
1716                     let contents = [];
1717                     arg.forEach(function (arg) {
1718                         let string = tag(arg, namespaces, indent);
1719                         if (string)
1720                             contents.push(string);
1721                     });
1722                     if (contents.length)
1723                         res.push(contents.join("\n"), join);
1724                 });
1725                 if (res[res.length - 1] == join)
1726                     res.pop();
1727                 return res.join("");
1728             }
1729
1730             attr = attr || {};
1731
1732             function parseNamespace(name) {
1733                 var m = /^(?:(.*):)?(.*)$/.exec(name);
1734                 return [namespaces[m[1]], m[2]];
1735             }
1736
1737             // FIXME: Surely we can do better.
1738             let skipAttr = {};
1739             for (var key in attr) {
1740                 if (/^xmlns(?:$|:)/.test(key)) {
1741                     if (_namespaces === namespaces)
1742                         namespaces = update({}, namespaces);
1743
1744                     let ns = namespaces[attr[key]] || attr[key];
1745                     if (ns == namespaces[key.substr(6)])
1746                         skipAttr[key] = true;
1747
1748                     attr[key] = namespaces[key.substr(6)] = ns;
1749                 }}
1750
1751             var args = Array.slice(args, 2);
1752             var vals = parseNamespace(name);
1753
1754             let res = [indent, "<", name];
1755
1756             for (let [key, val] in Iterator(attr)) {
1757                 if (Set.has(skipAttr, key))
1758                     continue;
1759
1760                 let vals = parseNamespace(key);
1761                 if (typeof val == "function") {
1762                     key = key.replace(/^(?:on)?/, "on");
1763                     val = val.toSource() + "(event)";
1764                 }
1765
1766                 if (key != "highlight" || vals[0] == String(NS))
1767                     res.push(" ", key, '="', DOM.escapeHTML(val), '"');
1768                 else
1769                     res.push(" ", namespaced(namespaces, String(NS), "highlight"),
1770                              '="', DOM.escapeHTML(val), '"');
1771             }
1772
1773             if ((vals[0] || namespaces[""]) == String(XHTML) && Set.has(EMPTY, vals[1])
1774                     || asXML && !args.length)
1775                 res.push("/>");
1776             else {
1777                 res.push(">");
1778
1779                 if (isStrings(args))
1780                     res.push(args.map(e => tag(e, namespaces, "")).join(""),
1781                              "</", name, ">");
1782                 else {
1783                     let contents = [];
1784                     args.forEach(function (e) {
1785                         let string = tag(e, namespaces, indent + INDENT);
1786                         if (string)
1787                             contents.push(string);
1788                     });
1789
1790                     res.push("\n", contents.join("\n"), "\n", indent, "</", name, ">");
1791                 }
1792             }
1793
1794             return res.join("");
1795         }
1796
1797         if (namespaces)
1798             namespaces = update({}, DOM.fromJSON.namespaces, namespaces);
1799         else
1800             namespaces = DOM.fromJSON.namespaces;
1801
1802         return tag(xml, namespaces, "");
1803     },
1804
1805     parseNamespace: function parseNamespace(name, namespaces) {
1806         if (name == "xmlns")
1807             return [DOM.fromJSON.namespaces.xmlns, "xmlns"];
1808
1809         var m = /^(?:(.*):)?(.*)$/.exec(name);
1810         return [(namespaces || DOM.fromJSON.namespaces)[m[1]],
1811                 m[2]];
1812     },
1813
1814     /**
1815      * Evaluates an XPath expression in the current or provided
1816      * document. It provides the xhtml, xhtml2 and dactyl XML
1817      * namespaces. The result may be used as an iterator.
1818      *
1819      * @param {string} expression The XPath expression to evaluate.
1820      * @param {Node} elem The context element.
1821      * @param {boolean} asIterator Whether to return the results as an
1822      *     XPath iterator.
1823      * @param {object} namespaces Additional namespaces to recognize.
1824      *     @optional
1825      * @returns {Object} Iterable result of the evaluation.
1826      */
1827     XPath: update(
1828         function XPath(expression, elem, asIterator, namespaces) {
1829             try {
1830                 let doc = elem.ownerDocument || elem;
1831
1832                 if (isArray(expression))
1833                     expression = DOM.makeXPath(expression);
1834
1835                 let resolver = XPath.resolver;
1836                 if (namespaces) {
1837                     namespaces = update({}, DOM.namespaces, namespaces);
1838                     resolver = prefix => namespaces[prefix] || null;
1839                 }
1840
1841                 let result = doc.evaluate(expression, elem,
1842                     resolver,
1843                     asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1844                     null
1845                 );
1846
1847                 let res = {
1848                     iterateNext: function () result.iterateNext(),
1849                     get resultType() result.resultType,
1850                     get snapshotLength() result.snapshotLength,
1851                     snapshotItem: function (i) result.snapshotItem(i),
1852                     __iterator__:
1853                         asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
1854                                    : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
1855                 };
1856                 return res;
1857             }
1858             catch (e) {
1859                 throw e.stack ? e : Error(e);
1860             }
1861         },
1862         {
1863             resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
1864         }),
1865
1866     /**
1867      * Returns an XPath union expression constructed from the specified node
1868      * tests. An expression is built with node tests for both the null and
1869      * XHTML namespaces. See {@link DOM.XPath}.
1870      *
1871      * @param nodes {Array(string)}
1872      * @returns {string}
1873      */
1874     makeXPath: function makeXPath(nodes) {
1875         return array(nodes).map(util.debrace).flatten()
1876                            .map(node => /^[a-z]+:/.test(node) ? node
1877                                                               : [node, "xhtml:" + node])
1878                            .flatten()
1879                            .map(node => "//" + node).join(" | ");
1880     },
1881
1882     namespaces: {
1883         xul: XUL,
1884         xhtml: XHTML,
1885         html: XHTML,
1886         xhtml2: "http://www.w3.org/2002/06/xhtml2",
1887         dactyl: NS
1888     },
1889
1890     namespaceNames: Class.Memoize(function ()
1891         iter(this.namespaces).map(([k, v]) => ([v, k])).toObject()),
1892 });
1893
1894 Object.keys(DOM.Event.types).forEach(function (event) {
1895     let name = event.replace(/-(.)/g, (m, m1) => m1.toUpperCase());
1896     if (!Set.has(DOM.prototype, name))
1897         DOM.prototype[name] =
1898             function _event(arg, extra) {
1899                 return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
1900             };
1901 });
1902
1903 var $ = DOM;
1904
1905 endModule();
1906
1907 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1908
1909 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: