1 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2008-2012 Kris Maglione <maglione.k@gmail.com>
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
9 exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
12 lazyRequire("highlight", ["highlight"]);
13 lazyRequire("messages", ["_"]);
14 lazyRequire("overlay", ["overlay"]);
15 lazyRequire("prefs", ["prefs"]);
16 lazyRequire("template", ["template"]);
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";
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);
29 elem.setAttribute(attr, true);
36 * A jQuery-inspired DOM utility framework.
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.
42 var DOM = Class("DOM", {
43 init: function init(val, context, nodes) {
50 if (context instanceof Ci.nsIDOMDocument)
51 this.document = context;
53 if (typeof val == "string")
54 val = context.querySelectorAll(val);
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);
66 else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
68 else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
70 this[length++] = elem;
71 else if ("length" in val)
72 for (let i = 0; i < val.length; i++)
73 this[length++] = val[i];
81 __iterator__: function __iterator__() {
82 for (let i = 0; i < this.length; i++)
86 Empty: function Empty() this.constructor(null, this.document),
88 nodes: Class.Memoize(function () ({})),
91 for (let i = 0; i < this.length; i++)
95 get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
96 set document(val) this._document = val,
98 attrHooks: array.toObject([
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")
111 matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
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);
120 eachDOM: function eachDOM(val, fn, self) {
122 function munge(val, container, idx) {
123 if (val instanceof Ci.nsIDOMRange)
124 return val.extractContents();
125 if (val instanceof Ci.nsIDOMNode)
128 if (DOM.isJSONXML(val)) {
129 val = dom.constructor(val, dom.document);
131 container[idx] = val[0];
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));
143 if (DOM.isJSONXML(val))
144 val = (function () this).bind(val);
147 return this.each(function (elem, i) {
148 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
152 util.withProperErrors(fn, self || this, munge(val), this[0], 0);
156 eq: function eq(idx) {
157 return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
160 find: function find(val) {
161 return this.map(function (elem) elem.querySelectorAll(val));
164 findAnon: function findAnon(attr, val) {
165 return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
168 filter: function filter(val, self) {
169 let res = this.Empty();
172 val = this.matcher(val);
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];
183 is: function is(val) {
184 return this.some(this.matcher(val));
187 reverse: function reverse() {
192 all: function all(fn, self) {
193 let res = this.Empty();
195 this.each(function (elem) {
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];
210 map: function map(fn, self) {
211 let res = this.Empty();
212 let obj = self || this.Empty();
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;
226 slice: function eq(start, end) {
227 return this.constructor(Array.slice(this, start, end));
230 some: function some(fn, self) {
231 for (let i = 0; i < this.length; i++)
232 if (fn.call(self || this, this[i], i))
237 get parent() this.map(function (elem) elem.parentNode, this),
239 get offsetParent() this.map(function (elem) {
241 var parent = elem.offsetParent;
242 if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
248 get ancestors() this.all(function (elem) elem.parentNode),
250 get children() this.map(function (elem) Array.filter(elem.childNodes,
251 function (e) e instanceof Ci.nsIDOMElement),
254 get contents() this.map(function (elem) elem.childNodes, this),
256 get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
257 function (e) e != elem && e instanceof Ci.nsIDOMElement),
260 get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
261 get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
263 get allSiblingsBefore() this.all(function (elem) elem.previousSibling),
264 get allSiblingsAfter() this.all(function (elem) elem.nextSibling),
266 get class() let (self = this) ({
267 toString: function () self[0].className,
269 get list() Array.slice(self[0].classList),
270 set list(val) self.attr("class", val.join(" ")),
272 each: function each(meth, arg) {
273 return self.each(function (elem) {
274 elem.classList[meth](arg);
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) {
282 return self.each(function (elem, i) {
283 this.class.toggle(cls, val.call(thisObj || this, elem, i));
285 return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
288 has: function has(cls) this[0].classList.has(cls)
291 get highlight() let (self = this) ({
292 toString: function () self.attrNS(NS, "highlight") || "",
294 get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
296 let str = array.uniq(val).join(" ").trim();
297 self.attrNS(NS, "highlight", str || null);
300 has: function has(hl) ~this.list.indexOf(hl),
302 add: function add(hl) self.each(function () {
303 highlight.loaded[hl] = true;
304 this.highlight.list = this.highlight.list.concat(hl);
307 remove: function remove(hl) self.each(function () {
308 this.highlight.list = this.highlight.list.filter(function (h) h != hl);
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;
315 highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl);
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() : {},
329 if (node instanceof Ci.nsIDOMDocument)
330 node = node.defaultView;
332 if (node instanceof Ci.nsIDOMWindow)
334 get width() this.right - this.left,
335 get height() this.bottom - this.top,
336 bottom: node.innerHeight,
337 right: node.innerWidth,
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
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 };
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 };
367 let func = callable(left) && left;
369 return this.each(function (elem, i) {
371 ({ left, top }) = func.call(this, elem, i);
373 if (elem instanceof Ci.nsIDOMWindow)
374 elem.scrollTo(left == null ? elem.scrollX : left,
375 top == null ? elem.scrollY : top);
378 elem.scrollLeft = left;
380 elem.scrollTop = top;
386 * Returns true if the given DOM node is currently visible.
390 let style = this[0] && this.style;
391 return style && style.visibility == "visible" && style.display != "none";
398 this[0] instanceof Ci.nsIDOMNSEditableElement;
400 if (this[0].editor instanceof Ci.nsIEditor)
401 var editor = this[0].editor;
409 editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
410 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
411 .getEditorForWindow(this[0]);
415 editor instanceof Ci.nsIPlaintextEditor;
416 editor instanceof Ci.nsIHTMLEditor;
420 get isEditable() !!this.editor || this[0] instanceof Ci.nsIDOMElement && this.style.MozUserModify == "read-write",
422 get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
423 Ci.nsIDOMHTMLTextAreaElement,
424 Ci.nsIDOMXULTextBoxElement])
428 * Returns an object representing a Node's computed CSS style.
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;
441 var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
446 util.dumpStack(_("error.nullComputedStyle", node));
447 Cu.reportError(Error(_("error.nullComputedStyle", node)));
454 * Parses the fields of a form and returns a URL/POST-data pair
455 * that is the equivalent of submitting the form.
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.
464 // Nuances gleaned from browser.jar/content/browser/browser.js
466 function encode(name, value, param) {
467 param = param ? "%s" : "";
469 return name + "=" + encodeComponent(value + param);
470 return encodeComponent(name) + "=" + encodeComponent(value) + param;
474 let form = field.form;
475 let doc = form.ownerDocument;
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);
482 converter = services.CharsetConv(cs);
487 let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
488 let url = util.newURI(form.action, charset, uri).spec;
490 let post = form.method.toUpperCase() == "POST";
492 let encodeComponent = encodeURIComponent;
493 if (charset !== "UTF-8")
494 encodeComponent = function encodeComponent(str)
495 escape(converter.ConvertFromUnicode(str) + converter.Finish());
498 if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
499 elems.push(encode(field.name, field.value));
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)) {
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));
513 elems.push(encode(elem.name, "", true));
515 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
516 for (let [, opt] in Iterator(elem.options))
518 elems.push(encode(elem.name, opt.value));
523 return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
524 return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
528 * Generates an XPath expression for the given element.
533 function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
534 if (!(this[0] instanceof Ci.nsIDOMElement))
538 let doc = this.document;
539 for (let elem = this[0];; elem = elem.parentNode) {
540 if (!(elem instanceof Ci.nsIDOMElement))
543 res.push("id(" + quote(elem.id) + ")");
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;
550 name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
552 res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
558 return res.reverse().join("/");
562 * Returns a string or XML representation of this node.
564 * @param {boolean} color If true, return a colored, XML
565 * representation of this node.
567 repr: function repr(color) {
568 function namespaced(node) {
569 var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[1];
571 return node.localName;
573 return [["span", { highlight: "HelpXMLNamespace" }, ns],
575 return ns + ":" + node.localName;
579 this.each(function (elem) {
581 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling);
583 res.push(["span", { highlight: "HelpXML" },
584 ["span", { highlight: "HelpXMLTagStart" },
585 "<", namespaced(elem), " ",
586 template.map(array.iterValues(elem.attributes),
588 ["span", { highlight: "HelpXMLAttribute" }, namespaced(attr)],
589 ["span", { highlight: "HelpXMLString" }, attr.value]
592 !hasChildren ? "/>" : ">",
596 ["span", { highlight: "HtmlTagEnd" }, "<", namespaced(elem), ">"]]
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(" ");
603 res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
607 res.push({}.toString.call(elem));
610 res = template.map(res, util.identity, ",");
611 return color ? res : res.join("");
614 attr: function attr(key, val) {
615 return this.attrNS("", key, val);
618 attrNS: function attrNS(ns, key, val) {
619 if (val !== undefined)
620 key = array.toObject([[key, val]]);
622 let hooks = this.attrHooks[ns] || {};
625 return this.each(function (elem, i) {
626 for (let [k, v] in Iterator(key)) {
628 v = v.call(this, elem, i);
630 if (Set.has(hooks, k) && hooks[k].set)
631 hooks[k].set.call(this, elem, v, k);
633 elem.removeAttributeNS(ns, k);
635 elem.setAttributeNS(ns, k, v);
642 if (Set.has(hooks, key) && hooks[key].get)
643 return hooks[key].get.call(this, this[0], key);
645 if (!this[0].hasAttributeNS(ns, key))
648 return this[0].getAttributeNS(ns, key);
651 css: update(function css(key, val) {
652 if (val !== undefined)
653 key = array.toObject([[key, val]]);
656 return this.each(function (elem) {
657 for (let [k, v] in Iterator(key))
658 elem.style[css.property(k)] = v;
661 return this[0].style[css.property(key)];
663 name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
665 property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
668 append: function append(val) {
669 return this.eachDOM(val, function (elem, target) {
670 target.appendChild(elem);
674 prepend: function prepend(val) {
675 return this.eachDOM(val, function (elem, target) {
676 target.insertBefore(elem, target.firstChild);
680 before: function before(val) {
681 return this.eachDOM(val, function (elem, target) {
682 target.parentNode.insertBefore(elem, target);
686 after: function after(val) {
687 return this.eachDOM(val, function (elem, target) {
688 target.parentNode.insertBefore(elem, target.nextSibling);
692 appendTo: function appendTo(elem) {
693 if (!(elem instanceof this.constructor))
694 elem = this.constructor(elem, this.document);
699 prependTo: function prependTo(elem) {
700 if (!(elem instanceof this.constructor))
701 elem = this.constructor(elem, this.document);
706 insertBefore: function insertBefore(elem) {
707 if (!(elem instanceof this.constructor))
708 elem = this.constructor(elem, this.document);
713 insertAfter: function insertAfter(elem) {
714 if (!(elem instanceof this.constructor))
715 elem = this.constructor(elem, this.document);
720 remove: function remove() {
721 return this.each(function (elem) {
723 elem.parentNode.removeChild(elem);
727 empty: function empty() {
728 return this.each(function (elem) {
729 while (elem.firstChild)
730 elem.removeChild(elem.firstChild);
734 fragment: function fragment() {
735 let frag = this.document.createDocumentFragment();
740 clone: function clone(deep)
741 this.map(function (elem) elem.cloneNode(deep)),
743 toggle: function toggle(val, self) {
745 return this.each(function (elem, i) {
746 this[val.call(self || this, elem, i) ? "show" : "hide"]();
749 if (arguments.length)
750 return this[val ? "show" : "hide"]();
752 let hidden = this.map(function (elem) elem.style.display == "none");
753 return this.each(function (elem, i) {
754 this[hidden[i] ? "show" : "hide"]();
757 hide: function hide() {
758 return this.each(function (elem) { elem.style.display = "none"; }, this);
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 = "";
765 this.each(function (elem) {
766 if (!elem.dactylDefaultDisplay)
767 elem.dactylDefaultDisplay = this.style.display;
770 return this.each(function (elem) {
771 elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
775 createContents: function createContents()
776 this.each(DOM.createContents, this),
778 isScrollable: function isScrollable(direction)
779 this.length && DOM.isScrollable(this[0], direction),
781 getSet: function getSet(args, get, set) {
783 return this[0] && get.call(this, this[0]);
785 let [fn, self] = args;
787 fn = function () args[0];
789 return this.each(function (elem, i) {
790 set.call(this, elem, fn.call(self || this, elem, i));
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; }));
800 text: function text(txt, self) {
801 return this.getSet(arguments,
802 function (elem) elem.textContent,
803 function (elem, val) { elem.textContent = val; });
806 val: function val(txt) {
807 return this.getSet(arguments,
808 function (elem) elem.value,
809 function (elem, val) { elem.value = val == null ? "" : val; });
812 listen: function listen(event, listener, capture) {
816 event = array.toObject([[event, listener]]);
818 for (let [evt, callback] in Iterator(event))
819 event[evt] = util.wrapCallback(callback, true);
821 return this.each(function (elem) {
822 for (let [evt, callback] in Iterator(event))
823 elem.addEventListener(evt, callback, capture);
826 unlisten: function unlisten(event, listener, capture) {
830 event = array.toObject([[event, listener]]);
832 return this.each(function (elem) {
833 for (let [k, v] in Iterator(event))
834 elem.removeEventListener(k, v.wrapper || v, capture);
837 once: function once(event, listener, capture) {
841 event = array.toObject([[event, listener]]);
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);
851 return this.each(function (elem) {
852 for (let [k, v] in Iterator(event))
853 elem.addEventListener(k, v, capture);
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;
866 focus: function focus(arg, extra) {
868 return this.listen("focus", arg, extra);
871 let flags = arg || services.focus.FLAG_BYMOUSE;
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);
889 blur: function blur(arg, extra) {
891 return this.listen("blur", arg, extra);
892 return this.each(function (elem) { elem.blur(); }, this);
896 * Scrolls an element into view if and only if it's not already
899 scrollIntoView: function scrollIntoView(alignWithTop) {
900 return this.each(function (elem) {
901 function getAlignment(viewport) {
902 if (alignWithTop !== undefined)
904 if (rect.bottom < viewport.top)
906 if (rect.top > viewport.bottom)
908 return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom);
912 function fix(parent) {
913 if (!(parent[0] instanceof Ci.nsIDOMWindow)
914 && parent.style.overflow == "visible")
917 ({ rect }) = DOM(elem);
918 let { viewport } = parent;
919 let isect = util.intersection(rect, viewport);
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));
926 parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
931 for (let parent in this.ancestors.items)
934 fix(DOM(this.document.defaultView));
939 * Creates an actual event from a pseudo-event object.
941 * The pseudo-event object (such as may be retrieved from
942 * DOM.Event.parse) should have any properties you want the event to
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
949 Event: Class("Event", {
950 init: function Event(doc, type, opts, target) {
953 type: type, bubbles: true, cancelable: false
957 bubbles: true, cancelable: true,
958 view: doc.defaultView,
959 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
960 keyCode: 0, charCode: 0
964 bubbles: true, cancelable: true,
965 view: doc.defaultView,
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)),
973 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
980 var t = this.constructor.types[type] || "";
981 var evt = doc.createEvent(t + "Events");
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)));
988 evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
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>") { ... }
998 add: ["+", "Plus", "Add"],
1004 close_bracket: ["]"],
1007 escape: ["Esc", "Escape"],
1008 insert: ["Insert", "Ins"],
1010 left_shift: ["LT", "<"],
1012 open_bracket: ["["],
1016 return: ["Return", "CR", "Enter"],
1020 space: ["Space", " "],
1021 subtract: ["-", "Minus", "Subtract"]
1027 this.code_nativeKey = {};
1029 for (let list in values(this.keyTable))
1030 for (let v in values(list)) {
1032 v = v.toLowerCase();
1033 this.key_key[v.toLowerCase()] = v;
1036 for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
1037 if (!/^DOM_VK_/.test(k))
1040 this.code_nativeKey[v] = k.substr(4);
1042 k = k.substr(7).toLowerCase();
1043 let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
1044 .replace(/^NUMPAD/, "k")];
1046 if (names[0].length == 1)
1047 names[0] = names[0].toLowerCase();
1049 if (k in this.keyTable)
1050 names = this.keyTable[k];
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;
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";
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"]),
1077 * Converts a user-input string of keys into a canonical
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>
1084 * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
1087 * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
1090 * @param {string} keys Messy form.
1091 * @param {boolean} unknownOk Whether unknown keys are passed
1092 * through rather than being converted to <lt>keyname>.
1094 * @returns {string} Canonical form.
1096 canonicalKeys: function canonicalKeys(keys, unknownOk) {
1097 if (arguments.length === 1)
1099 return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
1102 iterKeys: function iterKeys(keys) iter(function () {
1103 let match, re = /<.*?>?>|[^<]/g;
1104 while (match = re.exec(keys))
1109 * Converts an event string into an array of pseudo-event objects.
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.
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.
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>.
1126 * @returns {Array[Object]}
1128 parse: function parse(input, unknownOk) {
1130 return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
1132 if (arguments.length === 1)
1136 for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1137 let evt_str = match[0];
1139 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1140 keyCode: 0, charCode: 0, type: "keypress" };
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();
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));
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;
1164 if (keyname.length == 1) { // normal characters
1165 if (evt_obj.shiftKey)
1166 keyname = keyname.toUpperCase();
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()];
1172 else if (Set.has(this.pseudoKeys, keyname)) {
1173 evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
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;
1181 else { // spaces, control characters, and <
1182 evt_obj.keyCode = this.key_code[keyname];
1183 evt_obj.charCode = 0;
1186 else { // an invalid sequence starting with <, treat as a literal
1187 out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
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>
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);
1209 * Converts the specified event to a string in dactyl key-code
1210 * notation. Returns null for an unknown event.
1212 * @param {Event} event
1215 stringify: function stringify(event) {
1217 return event.map(function (e) this.stringify(e), this).join("");
1219 if (event.dactylString)
1220 return event.dactylString;
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];
1240 if (event.shiftKey && (key.length > 1 || key.toUpperCase() == key.toLowerCase()
1241 || event.ctrlKey || event.altKey || event.metaKey)
1242 || event.dactylShift)
1244 else if (!modifier && key.length === 1)
1246 key = key.toUpperCase();
1248 key = key.toLowerCase();
1250 if (!modifier && key.length == 1)
1254 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1255 // (i.e., cntrl codes 27--31)
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
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.
1272 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1273 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1275 modifier = modifier.replace("C-", "");
1277 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1278 key = String.fromCharCode(charCode + 64);
1280 // a normal key like a, b, c, 0, etc.
1281 else if (charCode) {
1282 key = String.fromCharCode(charCode);
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)
1289 key = this.code_key[this.key_code[key]];
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)
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)
1305 key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1310 else if (event.type == "click" || event.type == "dblclick") {
1313 if (event.type == "dblclick")
1315 // TODO: triple and quadruple click
1317 switch (event.button) {
1322 key = "MiddleMouse";
1333 return "<" + modifier + key + ">";
1337 load: { bubbles: false },
1338 submit: { cancelable: true }
1341 types: Class.Memoize(function () iter(
1343 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1345 "popupshowing popupshown popuphiding popuphidden " +
1347 Key: "keydown keypress keyup",
1348 "": "change command dactyl-input input submit " +
1349 "load unload pageshow pagehide DOMContentLoaded " +
1352 ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
1357 * Dispatches an event to an element as if it were a native event.
1359 * @param {Node} target The DOM node to which to dispatch the event.
1360 * @param {Event} event The event to dispatch.
1362 dispatch: Class.Memoize(function ()
1363 config.haveGecko("2b")
1364 ? function dispatch(target, event, extra) {
1366 this.feedingEvent = extra;
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);
1374 target.dispatchEvent(event);
1375 return !event.getPreventDefault();
1379 util.reportError(e);
1382 this.feedingEvent = null;
1385 : function dispatch(target, event, extra) {
1387 this.feedingEvent = extra;
1388 target.dispatchEvent(update(event, extra));
1391 this.feedingEvent = null;
1396 createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
1397 || function (elem) {}),
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),
1403 isJSONXML: function isJSONXML(val) isArray(val) && isinstance(val[0], ["String", "Array", "XML", DOM.DOMString])
1404 || isObject(val) && "toDOM" in val,
1406 DOMString: function DOMString(val) ({
1407 __proto__: DOMString.prototype,
1409 toDOM: function toDOM(doc) doc.createTextNode(val),
1411 toString: function () val
1415 * The set of input element type attribute values that mark the element as
1416 * an editable field.
1418 editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
1419 "month", "number", "password", "range", "search",
1420 "tel", "text", "time", "url", "week"]),
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
1427 * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1429 * @param {boolean} html Whether the output should be HTML rather
1430 * than presentation text.
1432 stringify: function stringify(node, html) {
1433 if (node instanceof Ci.nsISelection && node.isCollapsed)
1436 if (node instanceof Ci.nsIDOMNode) {
1437 let range = node.ownerDocument.createRange();
1438 range.selectNode(node);
1441 let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1442 doc = doc.ownerDocument || doc;
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);
1451 let str = services.String(encoder.encodeToString());
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;
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.
1467 * @param {[string]} list The list of patterns to match.
1468 * @returns {function(Node)}
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));
1479 function matcher(node) {
1481 for (let elem in DOM.XPath(matcher.xpath, node))
1485 for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1488 css: css.join(", "),
1489 xpath: xpath.join(" | ")
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
1498 * @param {[string]} list The list of patterns to test
1499 * @returns {boolean} True when the patterns are all valid.
1501 validateMatcher: function validateMatcher(list) {
1502 return this.testValues(list, DOM.closure.testMatcher);
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);
1511 util.withProperErrors("querySelector", node, value);
1516 * Converts HTML special characters in *str* to the equivalent HTML
1519 * @param {string} str
1520 * @param {boolean} simple If true, only escape for the simple case
1524 escapeHTML: function escapeHTML(str, simple) {
1525 let map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" };
1526 let regexp = simple ? /[<>]/g : /['"&<>]/g;
1527 return str.replace(regexp, function (m) map[m]);
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.
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.
1541 fromXML: deprecated("DOM.fromJSON", { get: function fromXML()
1542 prefs.get("javascript.options.xml.chrome") !== false
1543 && require("dom-e4x").fromXML }),
1545 fromJSON: update(function fromJSON(xml, doc, nodes, namespaces) {
1549 function tag(args, namespaces) {
1550 let _namespaces = namespaces;
1552 // Deal with common error case
1554 util.reportError(Error("Unexpected null when processing XML."));
1555 args = ["html:i", {}, "[NULL]"];
1558 if (isinstance(args, ["String", "Number", "Boolean", _]))
1559 return doc.createTextNode(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)
1566 if (args instanceof DOM)
1567 return args.fragment();
1568 if ("toJSONXML" in args)
1569 args = args.toJSONXML();
1571 let [name, attr] = args;
1573 if (!isString(name) || args.length == 0 || name === "") {
1574 var frag = doc.createDocumentFragment();
1575 Array.forEach(args, function (arg) {
1576 if (!isArray(arg[0]))
1578 arg.forEach(function (arg) {
1579 frag.appendChild(tag(arg, namespaces));
1587 function parseNamespace(name) DOM.parseNamespace(name, namespaces);
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);
1595 namespaces[key.substr(6)] = namespaces[attr[key]] || attr[key];
1598 var args = Array.slice(args, 2);
1599 var vals = parseNamespace(name);
1600 var elem = doc.createElementNS(vals[0] || namespaces[""],
1603 for (var key in attr)
1604 if (!/^xmlns(?:$|:)/.test(key)) {
1605 var val = attr[key];
1606 if (nodes && key == "key")
1609 vals = parseNamespace(key);
1610 if (key == "highlight")
1612 else if (typeof val == "function")
1613 elem.addEventListener(key.replace(/^on/, ""), val, false);
1615 elem.setAttributeNS(vals[0] || "", key, val);
1617 args.forEach(function (e) {
1618 elem.appendChild(tag(e, namespaces));
1621 if ("highlight" in attr)
1622 highlight.highlightNode(elem, attr.highlight, nodes || true);
1627 namespaces = update({}, fromJSON.namespaces, namespaces);
1629 namespaces = fromJSON.namespaces;
1631 return tag(xml, namespaces);
1634 "": "http://www.w3.org/1999/xhtml",
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"
1642 toXML: function toXML(xml) {
1644 let doc = services.XMLDocument();
1645 let node = this.fromJSON(xml, doc);
1646 return services.XMLSerializer()
1647 .serializeToString(node);
1650 toPrettyXML: function toPrettyXML(xml, asXML, indent, namespaces) {
1651 const INDENT = indent || " ";
1653 const EMPTY = Set("area base basefont br col frame hr img input isindex link meta param"
1656 function namespaced(namespaces, namespace, localName) {
1657 for (let [k, v] in Iterator(namespaces))
1659 return (k ? k + ":" + localName : localName);
1661 throw Error("No such namespace");
1664 function isFragment(args) !isString(args[0]) || args.length == 0 || args[0] === "";
1666 function hasString(args) {
1667 return args.some(function (a) isString(a) || isFragment(a) && hasString(a));
1670 function isStrings(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));
1676 function tag(args, namespaces, indent) {
1677 let _namespaces = namespaces;
1682 if (isinstance(args, ["String", "Number", "Boolean", _, DOM.DOMString]))
1684 DOM.escapeHTML(String(args), true);
1689 .replace(/^/m, indent);
1691 if (isObject(args) && "toDOM" in args)
1693 services.XMLSerializer()
1694 .serializeToString(args.toDOM(services.XMLDocument()))
1695 .replace(/^/m, indent);
1697 if (args instanceof Ci.nsIDOMNode)
1699 services.XMLSerializer()
1700 .serializeToString(args)
1701 .replace(/^/m, indent);
1703 if ("toJSONXML" in args)
1704 args = args.toJSONXML();
1706 // Deal with common error case
1708 util.reportError(Error("Unexpected null when processing XML."));
1712 let [name, attr] = args;
1714 if (isFragment(args)) {
1716 let join = isArray(args) && isStrings(args) ? "" : "\n";
1717 Array.forEach(args, function (arg) {
1718 if (!isArray(arg[0]))
1722 arg.forEach(function (arg) {
1723 let string = tag(arg, namespaces, indent);
1725 contents.push(string);
1727 if (contents.length)
1728 res.push(contents.join("\n"), join);
1730 if (res[res.length - 1] == join)
1732 return res.join("");
1737 function parseNamespace(name) {
1738 var m = /^(?:(.*):)?(.*)$/.exec(name);
1739 return [namespaces[m[1]], m[2]];
1742 // FIXME: Surely we can do better.
1744 for (var key in attr) {
1745 if (/^xmlns(?:$|:)/.test(key)) {
1746 if (_namespaces === namespaces)
1747 namespaces = update({}, namespaces);
1749 let ns = namespaces[attr[key]] || attr[key];
1750 if (ns == namespaces[key.substr(6)])
1751 skipAttr[key] = true;
1753 attr[key] = namespaces[key.substr(6)] = ns;
1756 var args = Array.slice(args, 2);
1757 var vals = parseNamespace(name);
1759 let res = [indent, "<", name];
1761 for (let [key, val] in Iterator(attr)) {
1762 if (Set.has(skipAttr, key))
1765 let vals = parseNamespace(key);
1766 if (typeof val == "function") {
1767 key = key.replace(/^(?:on)?/, "on");
1768 val = val.toSource() + "(event)";
1771 if (key != "highlight" || vals[0] == String(NS))
1772 res.push(" ", key, '="', DOM.escapeHTML(val), '"');
1774 res.push(" ", namespaced(namespaces, String(NS), "highlight"),
1775 '="', DOM.escapeHTML(val), '"');
1778 if ((vals[0] || namespaces[""]) == String(XHTML) && Set.has(EMPTY, vals[1])
1779 || asXML && !args.length)
1784 if (isStrings(args))
1785 res.push(args.map(function (e) tag(e, namespaces, "")).join(""),
1789 args.forEach(function (e) {
1790 let string = tag(e, namespaces, indent + INDENT);
1792 contents.push(string);
1795 res.push("\n", contents.join("\n"), "\n", indent, "</", name, ">");
1799 return res.join("");
1803 namespaces = update({}, DOM.fromJSON.namespaces, namespaces);
1805 namespaces = DOM.fromJSON.namespaces;
1807 return tag(xml, namespaces, "");
1810 parseNamespace: function parseNamespace(name, namespaces) {
1811 if (name == "xmlns")
1812 return [DOM.fromJSON.namespaces.xmlns, "xmlns"];
1814 var m = /^(?:(.*):)?(.*)$/.exec(name);
1815 return [(namespaces || DOM.fromJSON.namespaces)[m[1]],
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.
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
1828 * @param {object} namespaces Additional namespaces to recognize.
1830 * @returns {Object} Iterable result of the evaluation.
1833 function XPath(expression, elem, asIterator, namespaces) {
1835 let doc = elem.ownerDocument || elem;
1837 if (isArray(expression))
1838 expression = DOM.makeXPath(expression);
1840 let resolver = XPath.resolver;
1842 namespaces = update({}, DOM.namespaces, namespaces);
1843 resolver = function (prefix) namespaces[prefix] || null;
1846 let result = doc.evaluate(expression, elem,
1848 asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1853 iterateNext: function () result.iterateNext(),
1854 get resultType() result.resultType,
1855 get snapshotLength() result.snapshotLength,
1856 snapshotItem: function (i) result.snapshotItem(i),
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); }
1864 throw e.stack ? e : Error(e);
1868 resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
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}.
1876 * @param nodes {Array(string)}
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(" | ");
1889 xhtml2: "http://www.w3.org/2002/06/xhtml2",
1893 namespaceNames: Class.Memoize(function ()
1894 iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
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);
1910 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1912 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: