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 this.lazyRequire("highlight", ["highlight"]);
14 this.lazyRequire("template", ["template"]);
16 var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
17 var XHTML = Namespace("html", "http://www.w3.org/1999/xhtml");
18 var XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
19 var NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator");
20 default xml namespace = XHTML;
22 function BooleanAttribute(attr) ({
23 get: function (elem) elem.getAttribute(attr) == "true",
24 set: function (elem, val) {
25 if (val === "false" || !val)
26 elem.removeAttribute(attr);
28 elem.setAttribute(attr, true);
35 * A jQuery-inspired DOM utility framework.
37 * Please note that while this currently implements an Array-like
38 * interface, this is *not a defined interface* and is very likely to
39 * change in the near future.
41 var DOM = Class("DOM", {
42 init: function init(val, context, nodes) {
49 if (context instanceof Ci.nsIDOMDocument)
50 this.document = context;
52 if (typeof val == "string")
53 val = context.querySelectorAll(val);
57 else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument)
58 this[length++] = DOM.fromXML(val, context, this.nodes);
59 else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
61 else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
63 this[length++] = elem;
64 else if ("length" in val)
65 for (let i = 0; i < val.length; i++)
66 this[length++] = val[i];
74 __iterator__: function __iterator__() {
75 for (let i = 0; i < this.length; i++)
79 Empty: function Empty() this.constructor(null, this.document),
81 nodes: Class.Memoize(function () ({})),
84 for (let i = 0; i < this.length; i++)
88 get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
89 set document(val) this._document = val,
91 attrHooks: array.toObject([
93 href: { get: function (elem) elem.href || elem.getAttribute("href") },
94 src: { get: function (elem) elem.src || elem.getAttribute("src") },
95 checked: { get: function (elem) elem.hasAttribute("checked") ? elem.getAttribute("checked") == "true" : elem.checked,
96 set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val } },
97 collapsed: BooleanAttribute("collapsed"),
98 disabled: BooleanAttribute("disabled"),
99 hidden: BooleanAttribute("hidden"),
100 readonly: BooleanAttribute("readonly")
104 matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
106 each: function each(fn, self) {
107 let obj = self || this.Empty();
108 for (let i = 0; i < this.length; i++)
109 fn.call(self || update(obj, [this[i]]), this[i], i);
113 eachDOM: function eachDOM(val, fn, self) {
114 XML.prettyPrinting = XML.ignoreWhitespace = false;
118 if (typeof val == "xml")
119 return this.each(function (elem, i) {
120 fn.call(this, DOM.fromXML(val, elem.ownerDocument), elem, i);
124 function munge(val, container, idx) {
125 if (val instanceof Ci.nsIDOMRange)
126 return val.extractContents();
127 if (val instanceof Ci.nsIDOMNode)
130 if (typeof val == "xml") {
131 val = dom.constructor(val, dom.document);
133 container[idx] = val[0];
136 if (isObject(val) && "length" in val) {
137 let frag = dom.document.createDocumentFragment();
138 for (let i = 0; i < val.length; i++)
139 frag.appendChild(munge(val[i], val, i));
146 return this.each(function (elem, i) {
147 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
151 util.withProperErrors(fn, self || this, munge(val), this[0], 0);
155 eq: function eq(idx) {
156 return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
159 find: function find(val) {
160 return this.map(function (elem) elem.querySelectorAll(val));
163 findAnon: function findAnon(attr, val) {
164 return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
167 filter: function filter(val, self) {
168 let res = this.Empty();
171 val = this.matcher(val);
173 this.constructor(Array.filter(this, val, self || this));
174 let obj = self || this.Empty();
175 for (let i = 0; i < this.length; i++)
176 if (val.call(self || update(obj, [this[i]]), this[i], i))
177 res[res.length++] = this[i];
182 is: function is(val) {
183 return this.some(this.matcher(val));
186 reverse: function reverse() {
191 all: function all(fn, self) {
192 let res = this.Empty();
194 this.each(function (elem) {
196 elem = fn.call(this, elem)
197 if (elem instanceof Ci.nsIDOMNode)
198 res[res.length++] = elem;
199 else if (elem && "length" in elem)
200 for (let i = 0; i < elem.length; i++)
201 res[res.length++] = elem[j];
209 map: function map(fn, self) {
210 let res = this.Empty();
211 let obj = self || this.Empty();
213 for (let i = 0; i < this.length; i++) {
214 let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
215 if (isObject(tmp) && "length" in tmp)
216 for (let j = 0; j < tmp.length; j++)
217 res[res.length++] = tmp[j];
218 else if (tmp != null)
219 res[res.length++] = tmp;
225 slice: function eq(start, end) {
226 return this.constructor(Array.slice(this, start, end));
229 some: function some(fn, self) {
230 for (let i = 0; i < this.length; i++)
231 if (fn.call(self || this, this[i], i))
236 get parent() this.map(function (elem) elem.parentNode, this),
238 get offsetParent() this.map(function (elem) {
240 var parent = elem.offsetParent;
241 if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
247 get ancestors() this.all(function (elem) elem.parentNode),
249 get children() this.map(function (elem) Array.filter(elem.childNodes,
250 function (e) e instanceof Ci.nsIDOMElement),
253 get contents() this.map(function (elem) elem.childNodes, this),
255 get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
256 function (e) e != elem && e instanceof Ci.nsIDOMElement),
259 get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
260 get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
262 get allSiblingsBefore() this.all(function (elem) elem.previousSibling),
263 get allSiblingsAfter() this.all(function (elem) elem.nextSibling),
265 get class() let (self = this) ({
266 toString: function () self[0].className,
268 get list() Array.slice(self[0].classList),
269 set list(val) self.attr("class", val.join(" ")),
271 each: function each(meth, arg) {
272 return self.each(function (elem) {
273 elem.classList[meth](arg);
277 add: function add(cls) this.each("add", cls),
278 remove: function remove(cls) this.each("remove", cls),
279 toggle: function toggle(cls, val, thisObj) {
281 return self.each(function (elem, i) {
282 this.class.toggle(cls, val.call(thisObj || this, elem, i));
284 return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
287 has: function has(cls) this[0].classList.has(cls)
290 get highlight() let (self = this) ({
291 toString: function () self.attrNS(NS, "highlight") || "",
293 get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
295 let str = array.uniq(val).join(" ").trim();
296 self.attrNS(NS, "highlight", str || null);
299 has: function has(hl) ~this.list.indexOf(hl),
301 add: function add(hl) self.each(function () {
302 highlight.loaded[hl] = true;
303 this.highlight.list = this.highlight.list.concat(hl);
306 remove: function remove(hl) self.each(function () {
307 this.highlight.list = this.highlight.list.filter(function (h) h != hl);
310 toggle: function toggle(hl, val, thisObj) self.each(function (elem, i) {
311 let { highlight } = this;
312 let v = callable(val) ? val.call(thisObj || this, elem, i) : val;
314 highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl)
318 get rect() this[0] instanceof Ci.nsIDOMWindow ? { width: this[0].scrollMaxX + this[0].innerWidth,
319 height: this[0].scrollMaxY + this[0].innerHeight,
320 get right() this.width + this.left,
321 get bottom() this.height + this.top,
322 top: -this[0].scrollY,
323 left: -this[0].scrollX } :
324 this[0] ? this[0].getBoundingClientRect() : {},
328 if (node instanceof Ci.nsIDOMDocument)
329 node = node.defaultView;
331 if (node instanceof Ci.nsIDOMWindow)
333 get width() this.right - this.left,
334 get height() this.bottom - this.top,
335 bottom: node.innerHeight,
336 right: node.innerWidth,
342 width: node.clientWidth,
343 height: node.clientHeight,
344 top: r.top + node.clientTop,
345 get bottom() this.top + this.height,
346 left: r.left + node.clientLeft,
347 get right() this.left + this.width
351 scrollPos: function scrollPos(left, top) {
352 if (arguments.length == 0) {
353 if (this[0] instanceof Ci.nsIDOMElement)
354 return { top: this[0].scrollTop, left: this[0].scrollLeft,
355 height: this[0].scrollHeight, width: this[0].scrollWidth,
356 innerHeight: this[0].clientHeight, innerWidth: this[0].innerWidth };
358 if (this[0] instanceof Ci.nsIDOMWindow)
359 return { top: this[0].scrollY, left: this[0].scrollX,
360 height: this[0].scrollMaxY + this[0].innerHeight,
361 width: this[0].scrollMaxX + this[0].innerWidth,
362 innerHeight: this[0].innerHeight, innerWidth: this[0].innerWidth };
366 let func = callable(left) && left;
368 return this.each(function (elem, i) {
370 ({ left, top }) = func.call(this, elem, i);
372 if (elem instanceof Ci.nsIDOMWindow)
373 elem.scrollTo(left == null ? elem.scrollX : left,
374 top == null ? elem.scrollY : top);
377 elem.scrollLeft = left;
379 elem.scrollTop = top;
385 * Returns true if the given DOM node is currently visible.
389 let style = this[0] && this.style;
390 return style && style.visibility == "visible" && style.display != "none";
397 this[0] instanceof Ci.nsIDOMNSEditableElement;
399 if (this[0].editor instanceof Ci.nsIEditor)
400 var editor = this[0].editor;
408 editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
409 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
410 .getEditorForWindow(this[0]);
414 editor instanceof Ci.nsIPlaintextEditor;
415 editor instanceof Ci.nsIHTMLEditor;
419 get isEditable() !!this.editor || this[0] instanceof Ci.nsIDOMElement && this.style.MozUserModify == "read-write",
421 get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
422 Ci.nsIDOMHTMLTextAreaElement,
423 Ci.nsIDOMXULTextBoxElement])
427 * Returns an object representing a Node's computed CSS style.
432 if (node instanceof Ci.nsIDOMWindow)
433 node = node.document;
434 if (node instanceof Ci.nsIDOMDocument)
435 node = node.documentElement;
436 while (node && !(node instanceof Ci.nsIDOMElement) && node.parentNode)
437 node = node.parentNode;
440 var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
445 util.dumpStack(_("error.nullComputedStyle", node));
446 Cu.reportError(Error(_("error.nullComputedStyle", node)));
453 * Parses the fields of a form and returns a URL/POST-data pair
454 * that is the equivalent of submitting the form.
456 * @returns {object} An object with the following elements:
457 * url: The URL the form points to.
458 * postData: A string containing URL-encoded post data, if this
459 * form is to be POSTed
460 * charset: The character set of the GET or POST data.
461 * elements: The key=value pairs used to generate query information.
463 // Nuances gleaned from browser.jar/content/browser/browser.js
465 function encode(name, value, param) {
466 param = param ? "%s" : "";
468 return name + "=" + encodeComponent(value + param);
469 return encodeComponent(name) + "=" + encodeComponent(value) + param;
473 let form = field.form;
474 let doc = form.ownerDocument;
476 let charset = doc.characterSet;
477 let converter = services.CharsetConv(charset);
478 for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
479 let c = services.CharsetConv(cs);
481 converter = services.CharsetConv(cs);
486 let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
487 let url = util.newURI(form.action, charset, uri).spec;
489 let post = form.method.toUpperCase() == "POST";
491 let encodeComponent = encodeURIComponent;
492 if (charset !== "UTF-8")
493 encodeComponent = function encodeComponent(str)
494 escape(converter.ConvertFromUnicode(str) + converter.Finish());
497 if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
498 elems.push(encode(field.name, field.value));
500 for (let [, elem] in iter(form.elements))
501 if (elem.name && !elem.disabled) {
502 if (DOM(elem).isInput
503 || /^(?:hidden|textarea)$/.test(elem.type)
504 || elem.type == "submit" && elem == field
505 || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
506 elems.push(encode(elem.name, elem.value, elem === field));
507 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
508 for (let [, opt] in Iterator(elem.options))
510 elems.push(encode(elem.name, opt.value));
515 return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
516 return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
520 * Generates an XPath expression for the given element.
525 function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
526 if (!(this[0] instanceof Ci.nsIDOMElement))
530 let doc = this.document;
531 for (let elem = this[0];; elem = elem.parentNode) {
532 if (!(elem instanceof Ci.nsIDOMElement))
535 res.push("id(" + quote(elem.id) + ")");
537 let name = elem.localName;
538 if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
539 if (elem.namespaceURI in DOM.namespaceNames)
540 name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
542 name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
544 res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
550 return res.reverse().join("/");
554 * Returns a string or XML representation of this node.
556 * @param {boolean} color If true, return a colored, XML
557 * representation of this node.
559 repr: function repr(color) {
560 XML.ignoreWhitespace = XML.prettyPrinting = false;
562 function namespaced(node) {
563 var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
565 return node.localName;
567 return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
568 return ns + ":" + node.localName;
572 this.each(function (elem) {
574 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
576 res.push(<span highlight="HelpXML"><span highlight="HelpXMLTagStart"><{
578 template.map(array.iterValues(elem.attributes),
580 <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
581 <span highlight="HelpXMLString">{attr.value}</span>,
583 }{ !hasChildren ? "/>" : ">"
584 }</span>{ !hasChildren ? "" : <>...</> +
585 <span highlight="HtmlTagEnd"><{namespaced(elem)}></span>
588 let tag = "<" + [namespaced(elem)].concat(
589 [namespaced(a) + "=" + template.highlight(a.value, true)
590 for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
592 res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
596 res.push({}.toString.call(elem));
599 return template.map(res, util.identity, <>,</>);
602 attr: function attr(key, val) {
603 return this.attrNS("", key, val);
606 attrNS: function attrNS(ns, key, val) {
607 if (val !== undefined)
608 key = array.toObject([[key, val]]);
610 let hooks = this.attrHooks[ns] || {};
613 return this.each(function (elem, i) {
614 for (let [k, v] in Iterator(key)) {
616 v = v.call(this, elem, i);
618 if (Set.has(hooks, k) && hooks[k].set)
619 hooks[k].set.call(this, elem, v, k);
621 elem.removeAttributeNS(ns, k);
623 elem.setAttributeNS(ns, k, v);
630 if (Set.has(hooks, key) && hooks[key].get)
631 return hooks[key].get.call(this, this[0], key);
633 if (!this[0].hasAttributeNS(ns, key))
636 return this[0].getAttributeNS(ns, key);
639 css: update(function css(key, val) {
640 if (val !== undefined)
641 key = array.toObject([[key, val]]);
644 return this.each(function (elem) {
645 for (let [k, v] in Iterator(key))
646 elem.style[css.property(k)] = v;
649 return this[0].style[css.property(key)];
651 name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
653 property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
656 append: function append(val) {
657 return this.eachDOM(val, function (elem, target) {
658 target.appendChild(elem);
662 prepend: function prepend(val) {
663 return this.eachDOM(val, function (elem, target) {
664 target.insertBefore(elem, target.firstChild);
668 before: function before(val) {
669 return this.eachDOM(val, function (elem, target) {
670 target.parentNode.insertBefore(elem, target);
674 after: function after(val) {
675 return this.eachDOM(val, function (elem, target) {
676 target.parentNode.insertBefore(elem, target.nextSibling);
680 appendTo: function appendTo(elem) {
681 if (!(elem instanceof this.constructor))
682 elem = this.constructor(elem, this.document);
687 prependTo: function prependTo(elem) {
688 if (!(elem instanceof this.constructor))
689 elem = this.constructor(elem, this.document);
694 insertBefore: function insertBefore(elem) {
695 if (!(elem instanceof this.constructor))
696 elem = this.constructor(elem, this.document);
701 insertAfter: function insertAfter(elem) {
702 if (!(elem instanceof this.constructor))
703 elem = this.constructor(elem, this.document);
708 remove: function remove() {
709 return this.each(function (elem) {
711 elem.parentNode.removeChild(elem);
715 empty: function empty() {
716 return this.each(function (elem) {
717 while (elem.firstChild)
718 elem.removeChild(elem.firstChild);
722 toggle: function toggle(val, self) {
724 return this.each(function (elem, i) {
725 this[val.call(self || this, elem, i) ? "show" : "hide"]();
728 if (arguments.length)
729 return this[val ? "show" : "hide"]();
731 let hidden = this.map(function (elem) elem.style.display == "none");
732 return this.each(function (elem, i) {
733 this[hidden[i] ? "show" : "hide"]();
736 hide: function hide() {
737 return this.each(function (elem) { elem.style.display = "none"; }, this);
739 show: function show() {
740 for (let i = 0; i < this.length; i++)
741 if (!this[i].dactylDefaultDisplay && this[i].style.display)
742 this[i].style.display = "";
744 this.each(function (elem) {
745 if (!elem.dactylDefaultDisplay)
746 elem.dactylDefaultDisplay = this.style.display;
749 return this.each(function (elem) {
750 elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
754 createContents: function createContents()
755 this.each(DOM.createContents, this),
757 isScrollable: function isScrollable(direction)
758 this.length && DOM.isScrollable(this[0], direction),
760 getSet: function getSet(args, get, set) {
762 return this[0] && get.call(this, this[0]);
764 let [fn, self] = args;
766 fn = function () args[0];
768 return this.each(function (elem, i) {
769 set.call(this, elem, fn.call(self || this, elem, i));
773 html: function html(txt, self) {
774 return this.getSet(arguments,
775 function (elem) elem.innerHTML,
776 function (elem, val) { elem.innerHTML = val });
779 text: function text(txt, self) {
780 return this.getSet(arguments,
781 function (elem) elem.textContent,
782 function (elem, val) { elem.textContent = val });
785 val: function val(txt) {
786 return this.getSet(arguments,
787 function (elem) elem.value,
788 function (elem, val) { elem.value = val == null ? "" : val });
791 listen: function listen(event, listener, capture) {
795 event = array.toObject([[event, listener]]);
797 for (let [evt, callback] in Iterator(event))
798 event[evt] = util.wrapCallback(callback, true);
800 return this.each(function (elem) {
801 for (let [evt, callback] in Iterator(event))
802 elem.addEventListener(evt, callback, capture);
805 unlisten: function unlisten(event, listener, capture) {
809 event = array.toObject([[event, listener]]);
811 return this.each(function (elem) {
812 for (let [k, v] in Iterator(event))
813 elem.removeEventListener(k, v.wrapper || v, capture);
816 once: function once(event, listener, capture) {
820 event = array.toObject([[event, listener]]);
822 for (let pair in Iterator(event)) {
823 let [evt, callback] = pair;
824 event[evt] = util.wrapCallback(function wrapper(event) {
825 this.removeEventListener(evt, wrapper.wrapper, capture);
826 return callback.apply(this, arguments);
830 return this.each(function (elem) {
831 for (let [k, v] in Iterator(event))
832 elem.addEventListener(k, v, capture);
836 dispatch: function dispatch(event, params, extraProps) {
837 this.canceled = false;
838 return this.each(function (elem) {
839 let evt = DOM.Event(this.document, event, params, elem);
840 if (!DOM.Event.dispatch(elem, evt, extraProps))
841 this.canceled = true;
845 focus: function focus(arg, extra) {
847 return this.listen("focus", arg, extra);
850 let flags = arg || services.focus.FLAG_BYMOUSE;
852 if (elem instanceof Ci.nsIDOMDocument)
853 elem = elem.defaultView;
854 if (elem instanceof Ci.nsIDOMElement)
855 services.focus.setFocus(elem, flags);
856 else if (elem instanceof Ci.nsIDOMWindow) {
857 services.focus.focusedWindow = elem;
858 if (services.focus.focusedWindow != elem)
859 services.focus.clearFocus(elem);
868 blur: function blur(arg, extra) {
870 return this.listen("blur", arg, extra);
871 return this.each(function (elem) { elem.blur(); }, this);
875 * Scrolls an element into view if and only if it's not already
878 scrollIntoView: function scrollIntoView(alignWithTop) {
879 return this.each(function (elem) {
880 function getAlignment(viewport) {
881 if (alignWithTop !== undefined)
883 if (rect.bottom < viewport.top)
885 if (rect.top > viewport.bottom)
887 return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom)
891 function fix(parent) {
892 if (!(parent[0] instanceof Ci.nsIDOMWindow)
893 && parent.style.overflow == "visible")
896 ({ rect }) = DOM(elem);
897 let { viewport } = parent;
898 let isect = util.intersection(rect, viewport);
900 if (isect.height < Math.min(viewport.height, rect.height)) {
901 let { top } = parent.scrollPos();
902 if (getAlignment(viewport))
903 parent.scrollPos(null, top - (viewport.top - rect.top));
905 parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
910 for (let parent in this.ancestors.items)
913 fix(DOM(this.document.defaultView));
918 * Creates an actual event from a pseudo-event object.
920 * The pseudo-event object (such as may be retrieved from
921 * DOM.Event.parse) should have any properties you want the event to
924 * @param {Document} doc The DOM document to associate this event with
925 * @param {Type} type The type of event (keypress, click, etc.)
926 * @param {Object} opts The pseudo-event. @optional
928 Event: Class("Event", {
929 init: function Event(doc, type, opts, target) {
932 type: type, bubbles: true, cancelable: false
936 bubbles: true, cancelable: true,
937 view: doc.defaultView,
938 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
939 keyCode: 0, charCode: 0
943 bubbles: true, cancelable: true,
944 view: doc.defaultView,
946 get screenX() this.view.mozInnerScreenX
947 + Math.max(0, this.clientX + (DOM(target || opts.target).rect.left || 0)),
948 get screenY() this.view.mozInnerScreenY
949 + Math.max(0, this.clientY + (DOM(target || opts.target).rect.top || 0)),
952 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
959 var t = this.constructor.types[type] || "";
960 var evt = doc.createEvent(t + "Events");
962 let params = DEFAULTS[t || "HTML"];
963 let args = Object.keys(params);
964 update(params, this.constructor.defaults[type],
965 iter.toObject([k, opts[k]] for (k in opts) if (k in params)));
967 evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
971 init: function init() {
972 // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
973 // matters, so use that string as the first item, that you
974 // want to refer to within dactyl's source code for
975 // comparisons like if (key == "<Esc>") { ... }
977 add: ["+", "Plus", "Add"],
983 close_bracket: ["]"],
986 escape: ["Esc", "Escape"],
987 insert: ["Insert", "Ins"],
989 left_shift: ["LT", "<"],
995 return: ["Return", "CR", "Enter"],
999 space: ["Space", " "],
1000 subtract: ["-", "Minus", "Subtract"]
1006 this.code_nativeKey = {};
1008 for (let list in values(this.keyTable))
1009 for (let v in values(list)) {
1011 v = v.toLowerCase();
1012 this.key_key[v.toLowerCase()] = v;
1015 for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
1016 this.code_nativeKey[v] = k.substr(4);
1018 k = k.substr(7).toLowerCase();
1019 let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
1020 .replace(/^NUMPAD/, "k")];
1022 if (names[0].length == 1)
1023 names[0] = names[0].toLowerCase();
1025 if (k in this.keyTable)
1026 names = this.keyTable[k];
1028 this.code_key[v] = names[0];
1029 for (let [, name] in Iterator(names)) {
1030 this.key_key[name.toLowerCase()] = name;
1031 this.key_code[name.toLowerCase()] = v;
1035 // HACK: as Gecko does not include an event for <, we must add this in manually.
1036 if (!("<" in this.key_code)) {
1037 this.key_code["<"] = 60;
1038 this.key_code["lt"] = 60;
1039 this.code_key[60] = "lt";
1046 code_key: Class.Memoize(function (prop) this.init()[prop]),
1047 code_nativeKey: Class.Memoize(function (prop) this.init()[prop]),
1048 keyTable: Class.Memoize(function (prop) this.init()[prop]),
1049 key_code: Class.Memoize(function (prop) this.init()[prop]),
1050 key_key: Class.Memoize(function (prop) this.init()[prop]),
1051 pseudoKeys: Set(["count", "leader", "nop", "pass"]),
1054 * Converts a user-input string of keys into a canonical
1057 * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
1058 * <C- > maps to <C-Space>, <S-a> maps to A
1059 * << maps to <lt><lt>
1061 * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
1064 * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
1067 * @param {string} keys Messy form.
1068 * @param {boolean} unknownOk Whether unknown keys are passed
1069 * through rather than being converted to <lt>keyname>.
1071 * @returns {string} Canonical form.
1073 canonicalKeys: function canonicalKeys(keys, unknownOk) {
1074 if (arguments.length === 1)
1076 return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
1079 iterKeys: function iterKeys(keys) iter(function () {
1080 let match, re = /<.*?>?>|[^<]/g;
1081 while (match = re.exec(keys))
1086 * Converts an event string into an array of pseudo-event objects.
1088 * These objects can be used as arguments to {@link #stringify} or
1089 * {@link DOM.Event}, though they are unlikely to be much use for other
1090 * purposes. They have many of the properties you'd expect to find on a
1091 * real event, but none of the methods.
1093 * Also may contain two "special" parameters, .dactylString and
1094 * .dactylShift these are set for characters that can never by
1095 * typed, but may appear in mappings, for example <Nop> is passed as
1096 * dactylString, and dactylShift is set when a user specifies
1097 * <S-@> where @ is a non-case-changeable, non-space character.
1099 * @param {string} keys The string to parse.
1100 * @param {boolean} unknownOk Whether unknown keys are passed
1101 * through rather than being converted to <lt>keyname>.
1103 * @returns {Array[Object]}
1105 parse: function parse(input, unknownOk) {
1107 return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
1109 if (arguments.length === 1)
1113 for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1114 let evt_str = match[0];
1116 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1117 keyCode: 0, charCode: 0, type: "keypress" };
1119 if (evt_str.length == 1) {
1120 evt_obj.charCode = evt_str.charCodeAt(0);
1121 evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
1122 evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
1125 let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
1126 modifier = Set(modifier.toUpperCase());
1127 keyname = keyname.toLowerCase();
1128 evt_obj.dactylKeyname = keyname;
1129 if (/^u[0-9a-f]+$/.test(keyname))
1130 keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
1132 if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
1133 this.key_code[keyname] || Set.has(this.pseudoKeys, keyname))) {
1134 evt_obj.globKey ="*" in modifier;
1135 evt_obj.ctrlKey ="C" in modifier;
1136 evt_obj.altKey ="A" in modifier;
1137 evt_obj.shiftKey ="S" in modifier;
1138 evt_obj.metaKey ="M" in modifier || "⌘" in modifier;
1139 evt_obj.dactylShift = evt_obj.shiftKey;
1141 if (keyname.length == 1) { // normal characters
1142 if (evt_obj.shiftKey)
1143 keyname = keyname.toUpperCase();
1145 evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
1146 evt_obj.charCode = keyname.charCodeAt(0);
1147 evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
1149 else if (Set.has(this.pseudoKeys, keyname)) {
1150 evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
1152 else if (/mouse$/.test(keyname)) { // mouse events
1153 evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
1154 evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
1155 delete evt_obj.keyCode;
1156 delete evt_obj.charCode;
1158 else { // spaces, control characters, and <
1159 evt_obj.keyCode = this.key_code[keyname];
1160 evt_obj.charCode = 0;
1163 else { // an invalid sequence starting with <, treat as a literal
1164 out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
1169 // TODO: make a list of characters that need keyCode and charCode somewhere
1170 if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
1171 evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
1172 if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
1173 evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
1175 evt_obj.modifiers = (evt_obj.ctrlKey && Ci.nsIDOMNSEvent.CONTROL_MASK)
1176 | (evt_obj.altKey && Ci.nsIDOMNSEvent.ALT_MASK)
1177 | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
1178 | (evt_obj.metaKey && Ci.nsIDOMNSEvent.META_MASK);
1186 * Converts the specified event to a string in dactyl key-code
1187 * notation. Returns null for an unknown event.
1189 * @param {Event} event
1192 stringify: function stringify(event) {
1194 return event.map(function (e) this.stringify(e), this).join("");
1196 if (event.dactylString)
1197 return event.dactylString;
1211 if (/^key/.test(event.type)) {
1212 let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
1213 if (charCode == 0) {
1214 if (event.keyCode in this.code_key) {
1215 key = this.code_key[event.keyCode];
1217 if (event.shiftKey && (key.length > 1 || key.toUpperCase() == key.toLowerCase()
1218 || event.ctrlKey || event.altKey || event.metaKey)
1219 || event.dactylShift)
1221 else if (!modifier && key.length === 1)
1223 key = key.toUpperCase();
1225 key = key.toLowerCase();
1227 if (!modifier && key.length == 1)
1231 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1232 // (i.e., cntrl codes 27--31)
1234 // For more information, see:
1235 // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
1236 // [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
1237 // https://bugzilla.mozilla.org/show_bug.cgi?id=416227
1238 // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
1239 // https://bugzilla.mozilla.org/show_bug.cgi?id=432951
1242 // The following fixes are only activated if config.OS.isMacOSX.
1243 // Technically, they prevent mappings from <C-Esc> (and
1244 // <C-C-]> if your fancy keyboard permits such things<?>), but
1245 // these <C-control> mappings are probably pathological (<C-Esc>
1246 // certainly is on Windows), and so it is probably
1247 // harmless to remove the config.OS.isMacOSX if desired.
1249 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1250 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1252 modifier = modifier.replace("C-", "");
1254 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1255 key = String.fromCharCode(charCode + 64);
1257 // a normal key like a, b, c, 0, etc.
1258 else if (charCode) {
1259 key = String.fromCharCode(charCode);
1261 if (!/^[^<\s]$/i.test(key) && key in this.key_code) {
1262 // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
1263 if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
1266 key = this.code_key[this.key_code[key]];
1269 // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
1270 // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
1271 if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1273 if (/^\s$/.test(key))
1274 key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
1275 else if (modifier.length == 0)
1282 key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1287 else if (event.type == "click" || event.type == "dblclick") {
1290 if (event.type == "dblclick")
1292 // TODO: triple and quadruple click
1294 switch (event.button) {
1299 key = "MiddleMouse";
1310 return "<" + modifier + key + ">";
1315 load: { bubbles: false },
1316 submit: { cancelable: true }
1319 types: Class.Memoize(function () iter(
1321 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1323 "popupshowing popupshown popuphiding popuphidden " +
1325 Key: "keydown keypress keyup",
1326 "": "change command dactyl-input input submit " +
1327 "load unload pageshow pagehide DOMContentLoaded " +
1330 ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
1335 * Dispatches an event to an element as if it were a native event.
1337 * @param {Node} target The DOM node to which to dispatch the event.
1338 * @param {Event} event The event to dispatch.
1340 dispatch: Class.Memoize(function ()
1341 config.haveGecko("2b")
1342 ? function dispatch(target, event, extra) {
1344 this.feedingEvent = extra;
1346 if (target instanceof Ci.nsIDOMElement)
1347 // This causes a crash on Gecko<2.0, it seems.
1348 return (target.ownerDocument || target.document || target).defaultView
1349 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
1350 .dispatchDOMEventViaPresShell(target, event, true);
1352 target.dispatchEvent(event);
1353 return !event.getPreventDefault();
1357 util.reportError(e);
1360 this.feedingEvent = null;
1363 : function dispatch(target, event, extra) {
1365 this.feedingEvent = extra;
1366 target.dispatchEvent(update(event, extra));
1369 this.feedingEvent = null;
1374 createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
1375 || function (elem) {}),
1377 isScrollable: Class.Memoize(function () services.has("dactyl") && services.dactyl.getScrollable
1378 ? function (elem, dir) services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
1379 : function (elem, dir) true),
1382 * The set of input element type attribute values that mark the element as
1383 * an editable field.
1385 editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
1386 "month", "number", "password", "range", "search",
1387 "tel", "text", "time", "url", "week"]),
1390 * Converts a given DOM Node, Range, or Selection to a string. If
1391 * *html* is true, the output is HTML, otherwise it is presentation
1394 * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1396 * @param {boolean} html Whether the output should be HTML rather
1397 * than presentation text.
1399 stringify: function stringify(node, html) {
1400 if (node instanceof Ci.nsISelection && node.isCollapsed)
1403 if (node instanceof Ci.nsIDOMNode) {
1404 let range = node.ownerDocument.createRange();
1405 range.selectNode(node);
1408 let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1409 doc = doc.ownerDocument || doc;
1411 let encoder = services.HtmlEncoder();
1412 encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1413 if (node instanceof Ci.nsISelection)
1414 encoder.setSelection(node);
1415 else if (node instanceof Ci.nsIDOMRange)
1416 encoder.setRange(node);
1418 let str = services.String(encoder.encodeToString());
1422 let [result, length] = [{}, {}];
1423 services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
1424 return result.value.QueryInterface(Ci.nsISupportsString).data;
1428 * Compiles a CSS spec and XPath pattern matcher based on the given
1429 * list. List elements prefixed with "xpath:" are parsed as XPath
1430 * patterns, while other elements are parsed as CSS specs. The
1431 * returned function will, given a node, return an iterator of all
1432 * descendants of that node which match the given specs.
1434 * @param {[string]} list The list of patterns to match.
1435 * @returns {function(Node)}
1437 compileMatcher: function compileMatcher(list) {
1438 let xpath = [], css = [];
1439 for (let elem in values(list))
1440 if (/^xpath:/.test(elem))
1441 xpath.push(elem.substr(6));
1446 function matcher(node) {
1448 for (let elem in DOM.XPath(matcher.xpath, node))
1452 for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1455 css: css.join(", "),
1456 xpath: xpath.join(" | ")
1461 * Validates a list as input for {@link #compileMatcher}. Returns
1462 * true if and only if every element of the list is a valid XPath or
1465 * @param {[string]} list The list of patterns to test
1466 * @returns {boolean} True when the patterns are all valid.
1468 validateMatcher: function validateMatcher(list) {
1469 return this.testValues(list, DOM.closure.testMatcher);
1472 testMatcher: function testMatcher(value) {
1473 let evaluator = services.XPathEvaluator();
1474 let node = services.XMLDocument();
1475 if (/^xpath:/.test(value))
1476 util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
1478 util.withProperErrors("querySelector", node, value);
1483 * Converts HTML special characters in *str* to the equivalent HTML
1486 * @param {string} str
1489 escapeHTML: function escapeHTML(str) {
1490 let map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" };
1491 return str.replace(/['"&<>]/g, function (m) map[m]);
1495 * Converts an E4X XML literal to a DOM node. Any attribute named
1496 * highlight is present, it is transformed into dactyl:highlight,
1497 * and the named highlight groups are guaranteed to be loaded.
1499 * @param {Node} node
1500 * @param {Document} doc
1501 * @param {Object} nodes If present, nodes with the "key" attribute are
1502 * stored here, keyed to the value thereof.
1505 fromXML: function fromXML(node, doc, nodes) {
1506 XML.ignoreWhitespace = XML.prettyPrinting = false;
1507 if (typeof node === "string") // Sandboxes can't currently pass us XML objects.
1510 if (node.length() != 1) {
1511 let domnode = doc.createDocumentFragment();
1512 for each (let child in node)
1513 domnode.appendChild(fromXML(child, doc, nodes));
1517 switch (node.nodeKind()) {
1519 return doc.createTextNode(String(node));
1521 let domnode = doc.createElementNS(node.namespace(), node.localName());
1523 for each (let attr in node.@*::*)
1524 if (attr.name() != "highlight")
1525 domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
1527 for each (let child in node.*::*)
1528 domnode.appendChild(fromXML(child, doc, nodes));
1529 if (nodes && node.@key)
1530 nodes[node.@key] = domnode;
1532 if ("@highlight" in node)
1533 highlight.highlightNode(domnode, String(node.@highlight), nodes || true);
1541 * Evaluates an XPath expression in the current or provided
1542 * document. It provides the xhtml, xhtml2 and dactyl XML
1543 * namespaces. The result may be used as an iterator.
1545 * @param {string} expression The XPath expression to evaluate.
1546 * @param {Node} elem The context element.
1547 * @param {boolean} asIterator Whether to return the results as an
1549 * @param {object} namespaces Additional namespaces to recognize.
1551 * @returns {Object} Iterable result of the evaluation.
1554 function XPath(expression, elem, asIterator, namespaces) {
1556 let doc = elem.ownerDocument || elem;
1558 if (isArray(expression))
1559 expression = DOM.makeXPath(expression);
1561 let resolver = XPath.resolver;
1563 namespaces = update({}, DOM.namespaces, namespaces);
1564 resolver = function (prefix) namespaces[prefix] || null;
1567 let result = doc.evaluate(expression, elem,
1569 asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1574 iterateNext: function () result.iterateNext(),
1575 get resultType() result.resultType,
1576 get snapshotLength() result.snapshotLength,
1577 snapshotItem: function (i) result.snapshotItem(i),
1579 asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
1580 : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
1585 throw e.stack ? e : Error(e);
1589 resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
1593 * Returns an XPath union expression constructed from the specified node
1594 * tests. An expression is built with node tests for both the null and
1595 * XHTML namespaces. See {@link DOM.XPath}.
1597 * @param nodes {Array(string)}
1600 makeXPath: function makeXPath(nodes) {
1601 return array(nodes).map(util.debrace).flatten()
1602 .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
1603 .map(function (node) "//" + node).join(" | ");
1610 xhtml2: "http://www.w3.org/2002/06/xhtml2",
1614 namespaceNames: Class.Memoize(function ()
1615 iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
1618 Object.keys(DOM.Event.types).forEach(function (event) {
1619 let name = event.replace(/-(.)/g, function (m, m1) m1.toUpperCase());
1620 if (!Set.has(DOM.prototype, name))
1621 DOM.prototype[name] =
1622 function _event(arg, extra) {
1623 return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
1631 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1633 // vim: set sw=4 ts=4 et ft=javascript: