1 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2008-2011 by 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.
8 Components.utils.import("resource://dactyl/bootstrap.jsm");
10 exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
13 var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
14 var XHTML = Namespace("html", "http://www.w3.org/1999/xhtml");
15 var XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
16 var NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator");
17 default xml namespace = XHTML;
19 function BooleanAttribute(attr) ({
20 get: function (elem) elem.getAttribute(attr) == "true",
21 set: function (elem, val) {
22 if (val === "false" || !val)
23 elem.removeAttribute(attr);
25 elem.setAttribute(attr, true);
32 * A jQuery-inspired DOM utility framework.
34 * Please note that while this currently implements an Array-like
35 * interface, this is *not a defined interface* and is very likely to
36 * change in the near future.
38 var DOM = Class("DOM", {
39 init: function init(val, context, nodes) {
46 if (context instanceof Ci.nsIDOMDocument)
47 this.document = context;
49 if (typeof val == "string")
50 val = context.querySelectorAll(val);
54 else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument)
55 this[length++] = DOM.fromXML(val, context, this.nodes);
56 else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
58 else if ("length" in val)
59 for (let i = 0; i < val.length; i++)
60 this[length++] = val[i];
61 else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
63 this[length++] = elem;
71 __iterator__: function __iterator__() {
72 for (let i = 0; i < this.length; i++)
76 Empty: function Empty() this.constructor(null, this.document),
78 nodes: Class.Memoize(function () ({})),
81 for (let i = 0; i < this.length; i++)
85 get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
86 set document(val) this._document = val,
88 attrHooks: array.toObject([
90 href: { get: function (elem) elem.href || elem.getAttribute("href") },
91 src: { get: function (elem) elem.src || elem.getAttribute("src") },
92 checked: { get: function (elem) elem.hasAttribute("checked") ? elem.getAttribute("checked") == "true" : elem.checked,
93 set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val } },
94 collapsed: BooleanAttribute("collapsed"),
95 disabled: BooleanAttribute("disabled"),
96 hidden: BooleanAttribute("hidden"),
97 readonly: BooleanAttribute("readonly")
101 matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
103 each: function each(fn, self) {
104 let obj = self || this.Empty();
105 for (let i = 0; i < this.length; i++)
106 fn.call(self || update(obj, [this[i]]), this[i], i);
110 eachDOM: function eachDOM(val, fn, self) {
111 XML.prettyPrinting = XML.ignoreWhitespace = false;
115 if (typeof val == "xml")
116 return this.each(function (elem, i) {
117 fn.call(this, DOM.fromXML(val, elem.ownerDocument), elem, i);
121 function munge(val, container, idx) {
122 if (val instanceof Ci.nsIDOMRange)
123 return val.extractContents();
124 if (val instanceof Ci.nsIDOMNode)
127 if (typeof val == "xml") {
128 val = dom.constructor(val, dom.document);
130 container[idx] = val[0];
133 if (isObject(val) && "length" in val) {
134 let frag = dom.document.createDocumentFragment();
135 for (let i = 0; i < val.length; i++)
136 frag.appendChild(munge(val[i], val, i));
143 return this.each(function (elem, i) {
144 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
148 util.withProperErrors(fn, self || this, munge(val), this[0], 0);
152 eq: function eq(idx) {
153 return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
156 find: function find(val) {
157 return this.map(function (elem) elem.querySelectorAll(val));
160 findAnon: function findAnon(attr, val) {
161 return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
164 filter: function filter(val, self) {
165 let res = this.Empty();
168 val = this.matcher(val);
170 this.constructor(Array.filter(this, val, self || this));
171 let obj = self || this.Empty();
172 for (let i = 0; i < this.length; i++)
173 if (val.call(self || update(obj, [this[i]]), this[i], i))
174 res[res.length++] = this[i];
179 is: function is(val) {
180 return this.some(this.matcher(val));
183 reverse: function reverse() {
188 all: function all(fn, self) {
189 let res = this.Empty();
191 this.each(function (elem) {
193 elem = fn.call(this, elem)
194 if (elem instanceof Ci.nsIDOMElement)
195 res[res.length++] = elem;
196 else if (elem && "length" in elem)
197 for (let i = 0; i < tmp.length; i++)
198 res[res.length++] = tmp[j];
206 map: function map(fn, self) {
207 let res = this.Empty();
208 let obj = self || this.Empty();
210 for (let i = 0; i < this.length; i++) {
211 let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
212 if (isObject(tmp) && "length" in tmp)
213 for (let j = 0; j < tmp.length; j++)
214 res[res.length++] = tmp[j];
215 else if (tmp != null)
216 res[res.length++] = tmp;
222 slice: function eq(start, end) {
223 return this.constructor(Array.slice(this, start, end));
226 some: function some(fn, self) {
227 for (let i = 0; i < this.length; i++)
228 if (fn.call(self || this, this[i], i))
233 get parent() this.map(function (elem) elem.parentNode, this),
235 get offsetParent() this.map(function (elem) {
237 var parent = elem.offsetParent;
238 if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
244 get ancestors() this.all(function (elem) elem.parentNode),
246 get children() this.map(function (elem) Array.filter(elem.childNodes,
247 function (e) e instanceof Ci.nsIDOMElement),
250 get contents() this.map(function (elem) elem.childNodes, this),
252 get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
253 function (e) e != elem && e instanceof Ci.nsIDOMElement),
256 get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
257 get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
259 get class() let (self = this) ({
260 toString: function () self[0].className,
262 get list() Array.slice(self[0].classList),
263 set list(val) self.attr("class", val.join(" ")),
265 each: function each(meth, arg) {
266 return self.each(function (elem) {
267 elem.classList[meth](arg);
271 add: function add(cls) this.each("add", cls),
272 remove: function remove(cls) this.each("remove", cls),
273 toggle: function toggle(cls, val, thisObj) {
275 return self.each(function (elem, i) {
276 this.class.toggle(cls, val.call(thisObj || this, elem, i));
278 return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
281 has: function has(cls) this[0].classList.has(cls)
284 get highlight() let (self = this) ({
285 toString: function () self.attrNS(NS, "highlight") || "",
287 get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
289 let str = array.uniq(val).join(" ").trim();
290 self.attrNS(NS, "highlight", str || null);
293 has: function has(hl) ~this.list.indexOf(hl),
295 add: function add(hl) self.each(function () {
296 highlight.loaded[hl] = true;
297 this.highlight.list = this.highlight.list.concat(hl);
300 remove: function remove(hl) self.each(function () {
301 this.highlight.list = this.highlight.list.filter(function (h) h != hl);
304 toggle: function toggle(hl, val, thisObj) self.each(function (elem, i) {
305 let { highlight } = this;
306 let v = callable(val) ? val.call(thisObj || this, elem, i) : val;
308 highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl)
312 get rect() this[0] instanceof Ci.nsIDOMWindow ? { width: this[0].scrollMaxX + this[0].innerWidth,
313 height: this[0].scrollMaxY + this[0].innerHeight,
314 get right() this.width + this.left,
315 get bottom() this.height + this.top,
316 top: -this[0].scrollY,
317 left: -this[0].scrollX } :
318 this[0] ? this[0].getBoundingClientRect() : {},
321 if (this[0] instanceof Ci.nsIDOMWindow)
323 get width() this.right - this.left,
324 get height() this.bottom - this.top,
325 bottom: this[0].innerHeight,
326 right: this[0].innerWidth,
332 width: this[0].clientWidth,
333 height: this[0].clientHeight,
334 top: r.top + this[0].clientTop,
335 get bottom() this.top + this.height,
336 left: r.left + this[0].clientLeft,
337 get right() this.left + this.width
341 scrollPos: function scrollPos(left, top) {
342 if (arguments.length == 0) {
343 if (this[0] instanceof Ci.nsIDOMElement)
344 return { top: this[0].scrollTop, left: this[0].scrollLeft,
345 height: this[0].scrollHeight, width: this[0].scrollWidth,
346 innerHeight: this[0].clientHeight, innerWidth: this[0].innerWidth };
348 if (this[0] instanceof Ci.nsIDOMWindow)
349 return { top: this[0].scrollY, left: this[0].scrollX,
350 height: this[0].scrollMaxY + this[0].innerHeight,
351 width: this[0].scrollMaxX + this[0].innerWidth,
352 innerHeight: this[0].innerHeight, innerWidth: this[0].innerWidth };
356 let func = callable(left) && left;
358 return this.each(function (elem, i) {
360 ({ left, top }) = func.call(this, elem, i);
362 if (elem instanceof Ci.nsIDOMWindow)
363 elem.scrollTo(left == null ? elem.scrollX : left,
364 top == null ? elem.scrollY : top);
367 elem.scrollLeft = left;
369 elem.scrollTop = top;
375 * Returns true if the given DOM node is currently visible.
379 let style = this[0] && this.style;
380 return style && style.visibility == "visible" && style.display != "none";
387 this[0] instanceof Ci.nsIDOMNSEditableElement;
389 if (this[0].editor instanceof Ci.nsIEditor)
390 var editor = this[0].editor;
398 editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
399 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
400 .getEditorForWindow(this[0]);
404 editor instanceof Ci.nsIPlaintextEditor;
405 editor instanceof Ci.nsIHTMLEditor;
409 get isEditable() !!this.editor,
411 get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
412 Ci.nsIDOMHTMLTextAreaElement,
413 Ci.nsIDOMXULTextBoxElement])
417 * Returns an object representing a Node's computed CSS style.
422 if (node instanceof Ci.nsIDOMWindow)
423 node = node.document;
424 if (node instanceof Ci.nsIDOMDocument)
425 node = node.documentElement;
426 while (node && !(node instanceof Ci.nsIDOMElement) && node.parentNode)
427 node = node.parentNode;
430 var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
435 util.dumpStack(_("error.nullComputedStyle", node));
436 Cu.reportError(Error(_("error.nullComputedStyle", node)));
443 * Parses the fields of a form and returns a URL/POST-data pair
444 * that is the equivalent of submitting the form.
446 * @returns {object} An object with the following elements:
447 * url: The URL the form points to.
448 * postData: A string containing URL-encoded post data, if this
449 * form is to be POSTed
450 * charset: The character set of the GET or POST data.
451 * elements: The key=value pairs used to generate query information.
453 // Nuances gleaned from browser.jar/content/browser/browser.js
455 function encode(name, value, param) {
456 param = param ? "%s" : "";
458 return name + "=" + encodeComponent(value + param);
459 return encodeComponent(name) + "=" + encodeComponent(value) + param;
463 let form = field.form;
464 let doc = form.ownerDocument;
466 let charset = doc.characterSet;
467 let converter = services.CharsetConv(charset);
468 for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
469 let c = services.CharsetConv(cs);
471 converter = services.CharsetConv(cs);
476 let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
477 let url = util.newURI(form.action, charset, uri).spec;
479 let post = form.method.toUpperCase() == "POST";
481 let encodeComponent = encodeURIComponent;
482 if (charset !== "UTF-8")
483 encodeComponent = function encodeComponent(str)
484 escape(converter.ConvertFromUnicode(str) + converter.Finish());
487 if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
488 elems.push(encode(field.name, field.value));
490 for (let [, elem] in iter(form.elements))
491 if (elem.name && !elem.disabled) {
492 if (DOM(elem).isInput
493 || /^(?:hidden|textarea)$/.test(elem.type)
494 || elem.type == "submit" && elem == field
495 || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
496 elems.push(encode(elem.name, elem.value, elem === field));
497 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
498 for (let [, opt] in Iterator(elem.options))
500 elems.push(encode(elem.name, opt.value));
505 return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
506 return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
510 * Generates an XPath expression for the given element.
515 function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
516 if (!(this[0] instanceof Ci.nsIDOMElement))
520 let doc = this.document;
521 for (let elem = this[0];; elem = elem.parentNode) {
522 if (!(elem instanceof Ci.nsIDOMElement))
525 res.push("id(" + quote(elem.id) + ")");
527 let name = elem.localName;
528 if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
529 if (elem.namespaceURI in DOM.namespaceNames)
530 name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
532 name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
534 res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
540 return res.reverse().join("/");
544 * Returns a string or XML representation of this node.
546 * @param {boolean} color If true, return a colored, XML
547 * representation of this node.
549 repr: function repr(color) {
550 XML.ignoreWhitespace = XML.prettyPrinting = false;
552 function namespaced(node) {
553 var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
555 return node.localName;
557 return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
558 return ns + ":" + node.localName;
562 this.each(function (elem) {
564 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
566 res.push(<span highlight="HelpXML"><span highlight="HelpXMLTagStart"><{
568 template.map(array.iterValues(elem.attributes),
570 <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
571 <span highlight="HelpXMLString">{attr.value}</span>,
573 }{ !hasChildren ? "/>" : ">"
574 }</span>{ !hasChildren ? "" : <>...</> +
575 <span highlight="HtmlTagEnd"><{namespaced(elem)}></span>
578 let tag = "<" + [namespaced(elem)].concat(
579 [namespaced(a) + "=" + template.highlight(a.value, true)
580 for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
582 res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
586 res.push({}.toString.call(elem));
589 return template.map(res, util.identity, <>,</>);
592 attr: function attr(key, val) {
593 return this.attrNS("", key, val);
596 attrNS: function attrNS(ns, key, val) {
597 if (val !== undefined)
598 key = array.toObject([[key, val]]);
600 let hooks = this.attrHooks[ns] || {};
603 return this.each(function (elem, i) {
604 for (let [k, v] in Iterator(key)) {
606 v = v.call(this, elem, i);
608 if (Set.has(hooks, k) && hooks[k].set)
609 hooks[k].set.call(this, elem, v, k);
611 elem.removeAttributeNS(ns, k);
613 elem.setAttributeNS(ns, k, v);
620 if (Set.has(hooks, key) && hooks[key].get)
621 return hooks[key].get.call(this, this[0], key);
623 if (!this[0].hasAttributeNS(ns, key))
626 return this[0].getAttributeNS(ns, key);
629 css: update(function css(key, val) {
630 if (val !== undefined)
631 key = array.toObject([[key, val]]);
634 return this.each(function (elem) {
635 for (let [k, v] in Iterator(key))
636 elem.style[css.property(k)] = v;
639 return this[0].style[css.property(key)];
641 name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
643 property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
646 append: function append(val) {
647 return this.eachDOM(val, function (elem, target) {
648 target.appendChild(elem);
652 prepend: function prepend(val) {
653 return this.eachDOM(val, function (elem, target) {
654 target.insertBefore(elem, target.firstChild);
658 before: function before(val) {
659 return this.eachDOM(val, function (elem, target) {
660 target.parentNode.insertBefore(elem, target);
664 after: function after(val) {
665 return this.eachDOM(val, function (elem, target) {
666 target.parentNode.insertBefore(elem, target.nextSibling);
670 appendTo: function appendTo(elem) {
671 if (!(elem instanceof this.constructor))
672 elem = this.constructor(elem, this.document);
677 prependTo: function prependTo(elem) {
678 if (!(elem instanceof this.constructor))
679 elem = this.constructor(elem, this.document);
684 insertBefore: function insertBefore(elem) {
685 if (!(elem instanceof this.constructor))
686 elem = this.constructor(elem, this.document);
691 insertAfter: function insertAfter(elem) {
692 if (!(elem instanceof this.constructor))
693 elem = this.constructor(elem, this.document);
698 remove: function remove() {
699 return this.each(function (elem) {
701 elem.parentNode.removeChild(elem);
705 empty: function empty() {
706 return this.each(function (elem) {
707 while (elem.firstChild)
708 elem.removeChild(elem.firstChild);
712 toggle: function toggle(val, self) {
714 return this.each(function (elem, i) {
715 this[val.call(self || this, elem, i) ? "show" : "hide"]();
718 if (arguments.length)
719 return this[val ? "show" : "hide"]();
721 let hidden = this.map(function (elem) elem.style.display == "none");
722 return this.each(function (elem, i) {
723 this[hidden[i] ? "show" : "hide"]();
726 hide: function hide() {
727 return this.each(function (elem) { elem.style.display = "none"; }, this);
729 show: function show() {
730 for (let i = 0; i < this.length; i++)
731 if (!this[i].dactylDefaultDisplay && this[i].style.display)
732 this[i].style.display = "";
734 this.each(function (elem) {
735 if (!elem.dactylDefaultDisplay)
736 elem.dactylDefaultDisplay = this.style.display;
739 return this.each(function (elem) {
740 elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
744 createContents: function createContents()
745 this.each(DOM.createContents, this),
747 isScrollable: function isScrollable(direction)
748 this.length && DOM.isScrollable(this[0], direction),
750 getSet: function getSet(args, get, set) {
752 return this[0] && get.call(this, this[0]);
754 let [fn, self] = args;
756 fn = function () args[0];
758 return this.each(function (elem, i) {
759 set.call(this, elem, fn.call(self || this, elem, i));
763 html: function html(txt, self) {
764 return this.getSet(arguments,
765 function (elem) elem.innerHTML,
766 function (elem, val) { elem.innerHTML = val });
769 text: function text(txt, self) {
770 return this.getSet(arguments,
771 function (elem) elem.textContent,
772 function (elem, val) { elem.textContent = val });
775 val: function val(txt) {
776 return this.getSet(arguments,
777 function (elem) elem.value,
778 function (elem, val) { elem.value = val == null ? "" : val });
781 listen: function listen(event, listener, capture) {
785 event = array.toObject([[event, listener]]);
787 for (let [k, v] in Iterator(event))
788 event[k] = util.wrapCallback(v, true);
790 return this.each(function (elem) {
791 for (let [k, v] in Iterator(event))
792 elem.addEventListener(k, v, capture);
795 unlisten: function unlisten(event, listener, capture) {
799 event = array.toObject([[key, val]]);
801 return this.each(function (elem) {
802 for (let [k, v] in Iterator(event))
803 elem.removeEventListener(k, v.wrapper || v, capture);
807 dispatch: function dispatch(event, params, extraProps) {
808 this.canceled = false;
809 return this.each(function (elem) {
810 let evt = DOM.Event(this.document, event, params, elem);
811 if (!DOM.Event.dispatch(elem, evt, extraProps))
812 this.canceled = true;
816 focus: function focus(arg, extra) {
818 return this.listen("focus", arg, extra);
821 let flags = arg || services.focus.FLAG_BYMOUSE;
823 if (elem instanceof Ci.nsIDOMDocument)
824 elem = elem.defaultView;
825 if (elem instanceof Ci.nsIDOMElement)
826 services.focus.setFocus(elem, flags);
827 else if (elem instanceof Ci.nsIDOMWindow) {
828 services.focus.focusedWindow = elem;
829 if (services.focus.focusedWindow != elem)
830 services.focus.clearFocus(elem);
839 blur: function blur(arg, extra) {
841 return this.listen("blur", arg, extra);
842 return this.each(function (elem) { elem.blur(); }, this);
846 * Scrolls an element into view if and only if it's not already
849 scrollIntoView: function scrollIntoView(alignWithTop) {
850 return this.each(function (elem) {
851 function getAlignment(viewport) {
852 if (alignWithTop !== undefined)
854 if (rect.bottom < viewport.top)
856 if (rect.top > viewport.bottom)
858 return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom)
862 function fix(parent) {
863 if (!(parent[0] instanceof Ci.nsIDOMWindow)
864 && parent.style.overflow == "visible")
867 ({ rect }) = DOM(elem);
868 let { viewport } = parent;
869 let isect = util.intersection(rect, viewport);
871 if (isect.height < Math.min(viewport.height, rect.height)) {
872 let { top } = parent.scrollPos();
873 if (getAlignment(viewport))
874 parent.scrollPos(null, top - (viewport.top - rect.top));
876 parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
881 for (let parent in this.ancestors.items)
884 fix(DOM(this.document.defaultView));
889 * Creates an actual event from a pseudo-event object.
891 * The pseudo-event object (such as may be retrieved from
892 * DOM.Event.parse) should have any properties you want the event to
895 * @param {Document} doc The DOM document to associate this event with
896 * @param {Type} type The type of event (keypress, click, etc.)
897 * @param {Object} opts The pseudo-event. @optional
899 Event: Class("Event", {
900 init: function Event(doc, type, opts, target) {
903 type: type, bubbles: true, cancelable: false
907 bubbles: true, cancelable: true,
908 view: doc.defaultView,
909 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
910 keyCode: 0, charCode: 0
914 bubbles: true, cancelable: true,
915 view: doc.defaultView,
917 get screenX() this.view.mozInnerScreenX
918 + Math.max(0, this.clientX + (DOM(target || opts.target).rect.left || 0)),
919 get screenY() this.view.mozInnerScreenY
920 + Math.max(0, this.clientY + (DOM(target || opts.target).rect.top || 0)),
923 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
930 var t = this.constructor.types[type] || "";
931 var evt = doc.createEvent(t + "Events");
933 let params = DEFAULTS[t || "HTML"];
934 let args = Object.keys(params);
935 update(params, this.constructor.defaults[type],
936 iter.toObject([k, opts[k]] for (k in opts) if (k in params)));
938 evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
942 init: function init() {
943 // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
944 // matters, so use that string as the first item, that you
945 // want to refer to within dactyl's source code for
946 // comparisons like if (key == "<Esc>") { ... }
948 add: ["Plus", "Add"],
952 escape: ["Esc", "Escape"],
953 insert: ["Insert", "Ins"],
955 left_shift: ["LT", "<"],
958 return: ["Return", "CR", "Enter"],
961 space: ["Space", " "],
962 subtract: ["Minus", "Subtract"]
968 this.code_nativeKey = {};
970 for (let list in values(this.keyTable))
971 for (let v in values(list)) {
974 this.key_key[v.toLowerCase()] = v;
977 for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
978 this.code_nativeKey[v] = k.substr(4);
980 k = k.substr(7).toLowerCase();
981 let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
982 .replace(/^NUMPAD/, "k")];
984 if (names[0].length == 1)
985 names[0] = names[0].toLowerCase();
987 if (k in this.keyTable)
988 names = this.keyTable[k];
990 this.code_key[v] = names[0];
991 for (let [, name] in Iterator(names)) {
992 this.key_key[name.toLowerCase()] = name;
993 this.key_code[name.toLowerCase()] = v;
997 // HACK: as Gecko does not include an event for <, we must add this in manually.
998 if (!("<" in this.key_code)) {
999 this.key_code["<"] = 60;
1000 this.key_code["lt"] = 60;
1001 this.code_key[60] = "lt";
1008 code_key: Class.Memoize(function (prop) this.init()[prop]),
1009 code_nativeKey: Class.Memoize(function (prop) this.init()[prop]),
1010 keyTable: Class.Memoize(function (prop) this.init()[prop]),
1011 key_code: Class.Memoize(function (prop) this.init()[prop]),
1012 key_key: Class.Memoize(function (prop) this.init()[prop]),
1013 pseudoKeys: Set(["count", "leader", "nop", "pass"]),
1016 * Converts a user-input string of keys into a canonical
1019 * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
1020 * <C- > maps to <C-Space>, <S-a> maps to A
1021 * << maps to <lt><lt>
1023 * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
1026 * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
1029 * @param {string} keys Messy form.
1030 * @param {boolean} unknownOk Whether unknown keys are passed
1031 * through rather than being converted to <lt>keyname>.
1033 * @returns {string} Canonical form.
1035 canonicalKeys: function canonicalKeys(keys, unknownOk) {
1036 if (arguments.length === 1)
1038 return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
1041 iterKeys: function iterKeys(keys) iter(function () {
1042 let match, re = /<.*?>?>|[^<]/g;
1043 while (match = re.exec(keys))
1048 * Converts an event string into an array of pseudo-event objects.
1050 * These objects can be used as arguments to {@link #stringify} or
1051 * {@link DOM.Event}, though they are unlikely to be much use for other
1052 * purposes. They have many of the properties you'd expect to find on a
1053 * real event, but none of the methods.
1055 * Also may contain two "special" parameters, .dactylString and
1056 * .dactylShift these are set for characters that can never by
1057 * typed, but may appear in mappings, for example <Nop> is passed as
1058 * dactylString, and dactylShift is set when a user specifies
1059 * <S-@> where @ is a non-case-changeable, non-space character.
1061 * @param {string} keys The string to parse.
1062 * @param {boolean} unknownOk Whether unknown keys are passed
1063 * through rather than being converted to <lt>keyname>.
1065 * @returns {Array[Object]}
1067 parse: function parse(input, unknownOk) {
1069 return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
1071 if (arguments.length === 1)
1075 for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1076 let evt_str = match[0];
1078 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1079 keyCode: 0, charCode: 0, type: "keypress" };
1081 if (evt_str.length == 1) {
1082 evt_obj.charCode = evt_str.charCodeAt(0);
1083 evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
1084 evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
1087 let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
1088 modifier = Set(modifier.toUpperCase());
1089 keyname = keyname.toLowerCase();
1090 evt_obj.dactylKeyname = keyname;
1091 if (/^u[0-9a-f]+$/.test(keyname))
1092 keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
1094 if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
1095 this.key_code[keyname] || Set.has(this.pseudoKeys, keyname))) {
1096 evt_obj.globKey ="*" in modifier;
1097 evt_obj.ctrlKey ="C" in modifier;
1098 evt_obj.altKey ="A" in modifier;
1099 evt_obj.shiftKey ="S" in modifier;
1100 evt_obj.metaKey ="M" in modifier || "⌘" in modifier;
1101 evt_obj.dactylShift = evt_obj.shiftKey;
1103 if (keyname.length == 1) { // normal characters
1104 if (evt_obj.shiftKey)
1105 keyname = keyname.toUpperCase();
1107 evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
1108 evt_obj.charCode = keyname.charCodeAt(0);
1109 evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
1111 else if (Set.has(this.pseudoKeys, keyname)) {
1112 evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
1114 else if (/mouse$/.test(keyname)) { // mouse events
1115 evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
1116 evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
1117 delete evt_obj.keyCode;
1118 delete evt_obj.charCode;
1120 else { // spaces, control characters, and <
1121 evt_obj.keyCode = this.key_code[keyname];
1122 evt_obj.charCode = 0;
1125 else { // an invalid sequence starting with <, treat as a literal
1126 out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
1131 // TODO: make a list of characters that need keyCode and charCode somewhere
1132 if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
1133 evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
1134 if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
1135 evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
1137 evt_obj.modifiers = (evt_obj.ctrlKey && Ci.nsIDOMNSEvent.CONTROL_MASK)
1138 | (evt_obj.altKey && Ci.nsIDOMNSEvent.ALT_MASK)
1139 | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
1140 | (evt_obj.metaKey && Ci.nsIDOMNSEvent.META_MASK);
1148 * Converts the specified event to a string in dactyl key-code
1149 * notation. Returns null for an unknown event.
1151 * @param {Event} event
1154 stringify: function stringify(event) {
1156 return event.map(function (e) this.stringify(e), this).join("");
1158 if (event.dactylString)
1159 return event.dactylString;
1173 if (/^key/.test(event.type)) {
1174 let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
1175 if (charCode == 0) {
1176 if (event.keyCode in this.code_key) {
1177 key = this.code_key[event.keyCode];
1179 if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1181 else if (!modifier && key.length === 1)
1183 key = key.toUpperCase();
1185 key = key.toLowerCase();
1187 if (!modifier && key.length == 1)
1191 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1192 // (i.e., cntrl codes 27--31)
1194 // For more information, see:
1195 // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
1196 // [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
1197 // https://bugzilla.mozilla.org/show_bug.cgi?id=416227
1198 // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
1199 // https://bugzilla.mozilla.org/show_bug.cgi?id=432951
1202 // The following fixes are only activated if config.OS.isMacOSX.
1203 // Technically, they prevent mappings from <C-Esc> (and
1204 // <C-C-]> if your fancy keyboard permits such things<?>), but
1205 // these <C-control> mappings are probably pathological (<C-Esc>
1206 // certainly is on Windows), and so it is probably
1207 // harmless to remove the config.OS.isMacOSX if desired.
1209 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1210 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1212 modifier = modifier.replace("C-", "");
1214 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1215 key = String.fromCharCode(charCode + 64);
1217 // a normal key like a, b, c, 0, etc.
1218 else if (charCode) {
1219 key = String.fromCharCode(charCode);
1221 if (!/^[^<\s]$/i.test(key) && key in this.key_code) {
1222 // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
1223 if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
1226 key = this.code_key[this.key_code[key]];
1229 // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
1230 // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
1231 if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1233 if (/^\s$/.test(key))
1234 key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
1235 else if (modifier.length == 0)
1242 key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1247 else if (event.type == "click" || event.type == "dblclick") {
1250 if (event.type == "dblclick")
1252 // TODO: triple and quadruple click
1254 switch (event.button) {
1259 key = "MiddleMouse";
1270 return "<" + modifier + key + ">";
1275 load: { bubbles: false },
1276 submit: { cancelable: true }
1279 types: Class.Memoize(function () iter(
1281 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1283 "popupshowing popupshown popuphiding popuphidden " +
1285 Key: "keydown keypress keyup",
1286 "": "change command dactyl-input input submit " +
1287 "load unload pageshow pagehide DOMContentLoaded " +
1290 ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
1295 * Dispatches an event to an element as if it were a native event.
1297 * @param {Node} target The DOM node to which to dispatch the event.
1298 * @param {Event} event The event to dispatch.
1300 dispatch: Class.Memoize(function ()
1301 config.haveGecko("2b")
1302 ? function dispatch(target, event, extra) {
1304 this.feedingEvent = extra;
1306 if (target instanceof Ci.nsIDOMElement)
1307 // This causes a crash on Gecko<2.0, it seems.
1308 return (target.ownerDocument || target.document || target).defaultView
1309 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
1310 .dispatchDOMEventViaPresShell(target, event, true);
1312 target.dispatchEvent(event);
1313 return !event.getPreventDefault();
1317 util.reportError(e);
1320 this.feedingEvent = null;
1323 : function dispatch(target, event, extra) {
1325 this.feedingEvent = extra;
1326 target.dispatchEvent(update(event, extra));
1329 this.feedingEvent = null;
1334 createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
1335 || function (elem) {}),
1337 isScrollable: Class.Memoize(function () services.has("dactyl") && services.dactyl.getScrollable
1338 ? function (elem, dir) services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
1339 : function (elem, dir) true),
1342 * The set of input element type attribute values that mark the element as
1343 * an editable field.
1345 editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
1346 "month", "number", "password", "range", "search",
1347 "tel", "text", "time", "url", "week"]),
1350 * Converts a given DOM Node, Range, or Selection to a string. If
1351 * *html* is true, the output is HTML, otherwise it is presentation
1354 * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1356 * @param {boolean} html Whether the output should be HTML rather
1357 * than presentation text.
1359 stringify: function stringify(node, html) {
1360 if (node instanceof Ci.nsISelection && node.isCollapsed)
1363 if (node instanceof Ci.nsIDOMNode) {
1364 let range = node.ownerDocument.createRange();
1365 range.selectNode(node);
1368 let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1369 doc = doc.ownerDocument || doc;
1371 let encoder = services.HtmlEncoder();
1372 encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1373 if (node instanceof Ci.nsISelection)
1374 encoder.setSelection(node);
1375 else if (node instanceof Ci.nsIDOMRange)
1376 encoder.setRange(node);
1378 let str = services.String(encoder.encodeToString());
1382 let [result, length] = [{}, {}];
1383 services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
1384 return result.value.QueryInterface(Ci.nsISupportsString).data;
1388 * Compiles a CSS spec and XPath pattern matcher based on the given
1389 * list. List elements prefixed with "xpath:" are parsed as XPath
1390 * patterns, while other elements are parsed as CSS specs. The
1391 * returned function will, given a node, return an iterator of all
1392 * descendants of that node which match the given specs.
1394 * @param {[string]} list The list of patterns to match.
1395 * @returns {function(Node)}
1397 compileMatcher: function compileMatcher(list) {
1398 let xpath = [], css = [];
1399 for (let elem in values(list))
1400 if (/^xpath:/.test(elem))
1401 xpath.push(elem.substr(6));
1406 function matcher(node) {
1408 for (let elem in DOM.XPath(matcher.xpath, node))
1412 for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1415 css: css.join(", "),
1416 xpath: xpath.join(" | ")
1421 * Validates a list as input for {@link #compileMatcher}. Returns
1422 * true if and only if every element of the list is a valid XPath or
1425 * @param {[string]} list The list of patterns to test
1426 * @returns {boolean} True when the patterns are all valid.
1428 validateMatcher: function validateMatcher(list) {
1429 return this.testValues(list, DOM.closure.testMatcher);
1432 testMatcher: function testMatcher(value) {
1433 let evaluator = services.XPathEvaluator();
1434 let node = services.XMLDocument();
1435 if (/^xpath:/.test(value))
1436 util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
1438 util.withProperErrors("querySelector", node, value);
1443 * Converts HTML special characters in *str* to the equivalent HTML
1446 * @param {string} str
1449 escapeHTML: function escapeHTML(str) {
1450 let map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" };
1451 return str.replace(/['"&<>]/g, function (m) map[m]);
1455 * Converts an E4X XML literal to a DOM node. Any attribute named
1456 * highlight is present, it is transformed into dactyl:highlight,
1457 * and the named highlight groups are guaranteed to be loaded.
1459 * @param {Node} node
1460 * @param {Document} doc
1461 * @param {Object} nodes If present, nodes with the "key" attribute are
1462 * stored here, keyed to the value thereof.
1465 fromXML: function fromXML(node, doc, nodes) {
1466 XML.ignoreWhitespace = XML.prettyPrinting = false;
1467 if (typeof node === "string") // Sandboxes can't currently pass us XML objects.
1470 if (node.length() != 1) {
1471 let domnode = doc.createDocumentFragment();
1472 for each (let child in node)
1473 domnode.appendChild(fromXML(child, doc, nodes));
1477 switch (node.nodeKind()) {
1479 return doc.createTextNode(String(node));
1481 let domnode = doc.createElementNS(node.namespace(), node.localName());
1483 for each (let attr in node.@*::*)
1484 if (attr.name() != "highlight")
1485 domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
1487 for each (let child in node.*::*)
1488 domnode.appendChild(fromXML(child, doc, nodes));
1489 if (nodes && node.@key)
1490 nodes[node.@key] = domnode;
1492 if ("@highlight" in node)
1493 highlight.highlightNode(domnode, String(node.@highlight), nodes || true);
1501 * Evaluates an XPath expression in the current or provided
1502 * document. It provides the xhtml, xhtml2 and dactyl XML
1503 * namespaces. The result may be used as an iterator.
1505 * @param {string} expression The XPath expression to evaluate.
1506 * @param {Node} elem The context element.
1507 * @param {boolean} asIterator Whether to return the results as an
1509 * @param {object} namespaces Additional namespaces to recognize.
1511 * @returns {Object} Iterable result of the evaluation.
1514 function XPath(expression, elem, asIterator, namespaces) {
1516 let doc = elem.ownerDocument || elem;
1518 if (isArray(expression))
1519 expression = DOM.makeXPath(expression);
1521 let resolver = XPath.resolver;
1523 namespaces = update({}, DOM.namespaces, namespaces);
1524 resolver = function (prefix) namespaces[prefix] || null;
1527 let result = doc.evaluate(expression, elem,
1529 asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1533 return Object.create(result, {
1535 value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
1536 : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
1541 throw e.stack ? e : Error(e);
1545 resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
1549 * Returns an XPath union expression constructed from the specified node
1550 * tests. An expression is built with node tests for both the null and
1551 * XHTML namespaces. See {@link DOM.XPath}.
1553 * @param nodes {Array(string)}
1556 makeXPath: function makeXPath(nodes) {
1557 return array(nodes).map(util.debrace).flatten()
1558 .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
1559 .map(function (node) "//" + node).join(" | ");
1566 xhtml2: "http://www.w3.org/2002/06/xhtml2",
1570 namespaceNames: Class.Memoize(function ()
1571 iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
1574 Object.keys(DOM.Event.types).forEach(function (event) {
1575 let name = event.replace(/-(.)/g, function (m, m1) m1.toUpperCase());
1576 if (!Set.has(DOM.prototype, name))
1577 DOM.prototype[name] =
1578 function _event(arg, extra) {
1579 return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
1587 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1589 // vim: set sw=4 ts=4 et ft=javascript: