1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2014 Kris Maglione <maglione.k at Gmail>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
9 defineModule("buffer", {
10 exports: ["Buffer", "buffer"],
11 require: ["prefs", "services", "util"]
14 lazyRequire("bookmarkcache", ["bookmarkcache"]);
15 lazyRequire("contexts", ["Group"]);
16 lazyRequire("io", ["io"]);
17 lazyRequire("finder", ["RangeFind"]);
18 lazyRequire("overlay", ["overlay"]);
19 lazyRequire("promises", ["Promise", "promises"]);
20 lazyRequire("sanitizer", ["sanitizer"]);
21 lazyRequire("storage", ["File", "storage"]);
22 lazyRequire("template", ["template"]);
25 * A class to manage the primary web content buffer. The name comes
26 * from Vim's term, 'buffer', which signifies instances of open
30 var Buffer = Module("Buffer", {
31 Local: function Local(dactyl, modules, window) ({
33 return window.content;
35 let win = services.focus.focusedWindow;
36 if (!win || win == window || util.topWindow(win) != window)
37 return window.content;
38 if (win.top == window)
44 init: function init(win) {
49 get addPageInfoSection() Buffer.bound.addPageInfoSection,
51 get pageInfo() Buffer.pageInfo,
53 // called when the active document is scrolled
54 _updateBufferPosition: function _updateBufferPosition() {
55 this.modules.statusline.updateBufferPosition();
56 this.modules.commandline.clear(true);
60 * @property {Array} The alternative style sheets for the current
61 * buffer. Only returns style sheets for the 'screen' media type.
63 get alternateStyleSheets() {
64 let stylesheets = array.flatten(
65 this.allFrames().map(w => Array.slice(w.document.styleSheets)));
67 return stylesheets.filter(
68 s => /^(screen|all|)$/i.test(s.media.mediaText) && !/^\s*$/.test(s.title)
73 * The load context of the window bound to this buffer.
75 get loadContext() sanitizer.getContext(this.win),
78 * Content preference methods.
80 prefs: Class.Memoize(function ()
83 * Returns a promise for the given preference name.
85 * @param {string} pref The name of the preference to return.
86 * @returns {Promise<*>}
88 get: promises.withCallbacks(function get([resolve, reject], pref) {
89 let val = services.contentPrefs.getCachedByDomainAndName(
90 self.uri.spec, pref, self.loadContext);
96 services.contentPrefs.getByDomainAndName(
97 self.uri.spec, pref, self.loadContext,
98 { handleCompletion: () => {
102 handleResult: (pref) => {
106 handleError: reject });
110 * Sets a content preference for the given buffer.
112 * @param {string} pref The preference to set.
113 * @param {string} value The value to store.
115 set: promises.withCallbacks(function set([resolve, reject], pref, value) {
116 services.contentPrefs.set(
117 self.uri.spec, pref, value, self.loadContext,
118 { handleCompletion: () => {},
119 handleResult: resolve,
120 handleError: reject });
124 * Clear a content preference for the given buffer.
126 * @param {string} pref The preference to clear.
128 clear: promises.withCallbacks(function clear([resolve, reject], pref) {
129 services.contentPrefs.removeByDomainAndName(
130 self.uri.spec, pref, self.loadContext,
131 { handleCompletion: () => {},
132 handleResult: resolve,
133 handleError: reject });
138 * Gets a content preference for the given buffer.
140 * @param {string} pref The preference to get.
141 * @param {function(string|number|boolean)} callback The callback to
142 * call with the preference value. @optional
143 * @returns {string|number|boolean} The value of the preference, if
144 * callback is not provided.
146 getPref: deprecated("prefs.get", function getPref(pref, callback) {
147 services.contentPrefs.getPref(this.uri, pref,
148 this.loadContext, callback);
152 * Sets a content preference for the given buffer.
154 * @param {string} pref The preference to set.
155 * @param {string} value The value to store.
157 setPref: deprecated("prefs.set", function setPref(pref, value) {
158 services.contentPrefs.setPref(
159 this.uri, pref, value, this.loadContext);
163 * Clear a content preference for the given buffer.
165 * @param {string} pref The preference to clear.
167 clearPref: deprecated("prefs.clear", function clearPref(pref) {
168 services.contentPrefs.removePref(
169 this.uri, pref, this.loadContext);
172 climbUrlPath: function climbUrlPath(count) {
173 let { dactyl } = this.modules;
175 let url = this.documentURI.clone();
176 dactyl.assert(url instanceof Ci.nsIURL);
178 while (count-- && url.path != "/")
179 url.path = url.path.replace(/[^\/]*\/*$/, "");
181 dactyl.assert(!url.equals(this.documentURI));
182 dactyl.open(url.spec);
185 incrementURL: function incrementURL(count) {
186 let { dactyl } = this.modules;
188 let matches = this.uri.spec.match(/(.*?)(\d+)(\D*)$/);
189 dactyl.assert(matches);
190 let oldNum = matches[2];
192 // disallow negative numbers as trailing numbers are often proceeded by hyphens
193 let newNum = String(Math.max(parseInt(oldNum, 10) + count, 0));
194 if (/^0/.test(oldNum))
195 while (newNum.length < oldNum.length)
196 newNum = "0" + newNum;
199 dactyl.open(matches.slice(1).join(""));
203 * @property {number} True when the buffer is fully loaded.
205 get loaded() Math.min.apply(null,
207 .map(frame => ["loading", "interactive", "complete"]
208 .indexOf(frame.document.readyState))),
211 * @property {Object} The local state store for the currently selected
217 let store = overlay.getData(doc, "buffer", null);
218 if (!store || !this.localStorePrototype.isPrototypeOf(store))
219 store = overlay.setData(doc, "buffer", Object.create(this.localStorePrototype));
220 return store.instance = store;
223 localStorePrototype: memoize({
230 * @property {Node} The last focused input field in the buffer. Used
231 * by the "gi" key binding.
233 get lastInputField() {
234 let field = this.localStore.lastInputField && this.localStore.lastInputField.get();
236 let doc = field && field.ownerDocument;
237 let win = doc && doc.defaultView;
238 return win && doc === win.document ? field : null;
240 set lastInputField(value) { this.localStore.lastInputField = util.weakReference(value); },
243 * @property {nsIURI} The current top-level document.
245 get doc() this.win.document,
247 get docShell() util.docShell(this.win),
249 get modules() this.topWindow.dactyl.modules,
252 topWindow: Class.Memoize(function () util.topWindow(this.win)),
255 * @property {nsIURI} The current top-level document's URI.
257 get uri() util.newURI(this.win.location.href),
260 * @property {nsIURI} The current top-level document's URI, sans any
261 * fragment identifier.
263 get documentURI() this.doc.documentURIObject || util.newURI(this.doc.documentURI),
266 * @property {string} The current top-level document's URL.
268 get URL() update(new String(this.win.location.href), util.newURI(this.win.location.href)),
271 * @property {number} The buffer's height in pixels.
273 get pageHeight() this.win.innerHeight,
275 get contentViewer() this.docShell.contentViewer
276 .QueryInterface(Components.interfaces.nsIMarkupDocumentViewer),
279 * @property {number} The current browser's zoom level, as a
280 * percentage with 100 as 'normal'.
283 let v = this.contentViewer;
284 return v[v.textZoom == 1 ? "fullZoom" : "textZoom"] * 100;
286 set zoomLevel(value) { this.setZoom(value, this.fullZoom); },
289 * @property {boolean} Whether the current browser is using full
290 * zoom, as opposed to text zoom.
292 get fullZoom() this.ZoomManager.useFullZoom,
293 set fullZoom(value) { this.setZoom(this.zoomLevel, value); },
295 get ZoomManager() this.topWindow.ZoomManager,
298 * @property {string} The current document's title.
300 get title() this.doc.title,
303 * @property {number} The buffer's horizontal scroll percentile.
305 get scrollXPercent() {
306 let elem = Buffer.Scrollable(this.findScrollable(0, true));
307 if (elem.scrollWidth - elem.clientWidth === 0)
309 return elem.scrollLeft * 100 / (elem.scrollWidth - elem.clientWidth);
313 * @property {number} The buffer's vertical scroll percentile.
315 get scrollYPercent() {
316 let elem = Buffer.Scrollable(this.findScrollable(0, false));
317 if (elem.scrollHeight - elem.clientHeight === 0)
319 return elem.scrollTop * 100 / (elem.scrollHeight - elem.clientHeight);
323 * @property {{ x: number, y: number }} The buffer's current scroll position
324 * as reported by {@link Buffer.getScrollPosition}.
326 get scrollPosition() Buffer.getScrollPosition(this.findScrollable(0, false)),
329 * Returns a list of all frames in the given window or current buffer.
331 allFrames: function allFrames(win, focusedFirst) {
333 (function rec(frame) {
334 if (true || frame.document.body instanceof Ci.nsIDOMHTMLBodyElement)
336 Array.forEach(frame.frames, rec);
340 return frames.filter(f => f === this.focusedFrame).concat(
341 frames.filter(f => f !== this.focusedFrame));
346 * @property {Window} Returns the currently focused frame.
349 let frame = this.localStore.focusedFrame;
350 return frame && frame.get() || this.win;
352 set focusedFrame(frame) {
353 this.localStore.focusedFrame = util.weakReference(frame);
357 * Returns the currently selected word. If the selection is
358 * null, it tries to guess the word that the caret is
363 get currentWord() Buffer.currentWord(this.focusedFrame),
364 getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() Buffer.currentWord(this.focusedFrame, true)),
367 * Returns true if a scripts are allowed to focus the given input
368 * element or input elements in the given window.
370 * @param {Node|Window}
373 focusAllowed: function focusAllowed(elem) {
374 if (elem instanceof Ci.nsIDOMWindow && !DOM(elem).isEditable)
377 let { options } = this.modules;
379 let doc = elem.ownerDocument || elem.document || elem;
380 switch (options.get("strictfocus").getKey(doc.documentURIObject || util.newURI(doc.documentURI), "moderate")) {
382 return overlay.getData(elem)["focus-allowed"]
383 || elem.frameElement && overlay.getData(elem.frameElement)["focus-allowed"];
385 return overlay.getData(doc, "focus-allowed")
386 || elem.frameElement && overlay.getData(elem.frameElement.ownerDocument)["focus-allowed"];
393 * Focuses the given element. In contrast to a simple
394 * elem.focus() call, this function works for iframes and
397 * @param {Node} elem The element to focus.
399 focusElement: function focusElement(elem) {
400 let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
401 overlay.setData(elem, "focus-allowed", true);
402 overlay.setData(win.document, "focus-allowed", true);
404 if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
405 Ci.nsIDOMHTMLIFrameElement]))
406 elem = elem.contentWindow;
409 overlay.setData(elem.document, "focus-allowed", true);
411 if (elem instanceof Ci.nsIDOMHTMLInputElement && elem.type == "file") {
412 Buffer.openUploadPrompt(elem);
413 this.lastInputField = elem;
416 if (isinstance(elem, [Ci.nsIDOMHTMLInputElement,
417 Ci.nsIDOMXULTextBoxElement]))
418 var flags = services.focus.FLAG_BYMOUSE;
420 flags = services.focus.FLAG_SHOWRING;
422 // Hack to deal with current versions of Firefox misplacing
424 if (!overlay.getData(elem, "had-focus", false) && elem.value &&
425 elem instanceof Ci.nsIDOMHTMLInputElement &&
426 DOM(elem).isEditable &&
427 elem.selectionStart != null &&
428 elem.selectionStart == elem.selectionEnd)
429 elem.selectionStart = elem.selectionEnd = elem.value.length;
431 DOM(elem).focus(flags);
433 if (elem instanceof Ci.nsIDOMWindow) {
434 let sel = elem.getSelection();
435 if (sel && !sel.rangeCount)
436 sel.addRange(RangeFind.endpoint(
437 RangeFind.nodeRange(elem.document.body || elem.document.documentElement),
441 let range = RangeFind.nodeRange(elem);
442 let sel = (elem.ownerDocument || elem).defaultView.getSelection();
443 if (!sel.rangeCount || !RangeFind.intersects(range, sel.getRangeAt(0))) {
444 range.collapse(true);
445 sel.removeAllRanges();
451 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
453 let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);
455 DOM(elem).mouseover({ screenX: x, screenY: y });
463 * Find the *count*th last link on a page matching one of the given
464 * regular expressions, or with a @rel or @rev attribute matching
465 * the given relation. Each frame is searched beginning with the
466 * last link and progressing to the first, once checking for
467 * matching @rel or @rev properties, and then once for each given
468 * regular expression. The first match is returned. All frames of
469 * the page are searched, beginning with the currently focused.
471 * If follow is true, the link is followed.
473 * @param {string} rel The relationship to look for.
474 * @param {[RegExp]} regexps The regular expressions to search for.
475 * @param {number} count The nth matching link to follow.
476 * @param {bool} follow Whether to follow the matching link.
477 * @param {string} path The CSS to use for the search. @optional
479 findLink: function findLink(rel, regexps, count, follow, path) {
480 let { Hints, dactyl, options } = this.modules;
482 let selector = path || options.get("hinttags").stringDefaultValue;
484 function followFrame(frame) {
485 function iter(elems) {
486 for (let i = 0; i < elems.length; i++)
487 if (elems[i].rel.toLowerCase() === rel || elems[i].rev.toLowerCase() === rel)
491 let elems = frame.document.getElementsByTagName("link");
492 for (let elem in iter(elems))
495 elems = frame.document.getElementsByTagName("a");
496 for (let elem in iter(elems))
499 function a(regexp, elem) regexp.test(elem.textContent) === regexp.result ||
500 Array.some(elem.childNodes,
501 child => (regexp.test(child.alt) === regexp.result));
503 function b(regexp, elem) regexp.test(elem.title) === regexp.result;
505 let res = Array.filter(frame.document.querySelectorAll(selector), Hints.isVisible);
506 for (let test in values([a, b]))
507 for (let regexp in values(regexps))
508 for (let i in util.range(res.length, 0, -1))
509 if (test(regexp, res[i]))
513 for (let frame in values(this.allFrames(null, true)))
514 for (let elem in followFrame(frame))
517 this.followLink(elem, dactyl.CURRENT_TAB);
524 followDocumentRelationship: deprecated("buffer.findLink",
525 function followDocumentRelationship(rel) {
526 let { options } = this.modules;
528 this.findLink(rel, options[rel + "pattern"], 0, true);
532 * Fakes a click on a link.
534 * @param {Node} elem The element to click.
535 * @param {number} where Where to open the link. See
536 * {@link dactyl.open}.
538 followLink: function followLink(elem, where) {
539 let { dactyl } = this.modules;
541 let doc = elem.ownerDocument;
542 let win = doc.defaultView;
543 let { left: offsetX, top: offsetY } = elem.getBoundingClientRect();
545 if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
546 Ci.nsIDOMHTMLIFrameElement]))
547 return this.focusElement(elem);
549 if (isinstance(elem, Ci.nsIDOMHTMLLinkElement))
550 return dactyl.open(elem.href, where);
552 if (elem instanceof Ci.nsIDOMHTMLAreaElement) { // for imagemap
553 let coords = elem.getAttribute("coords").split(",");
554 offsetX = Number(coords[0]) + 1;
555 offsetY = Number(coords[1]) + 1;
557 else if (elem instanceof Ci.nsIDOMHTMLInputElement && elem.type == "file") {
558 Buffer.openUploadPrompt(elem);
562 let { dactyl } = this.modules;
564 let ctrlKey = false, shiftKey = false;
566 switch (dactyl.forceTarget || where) {
568 case dactyl.NEW_BACKGROUND_TAB:
570 shiftKey = dactyl.forceBackground != null ? dactyl.forceBackground
571 : where != dactyl.NEW_BACKGROUND_TAB;
573 case dactyl.NEW_WINDOW:
576 case dactyl.CURRENT_TAB:
580 this.focusElement(elem);
582 prefs.withContext(function () {
583 prefs.set("browser.tabs.loadInBackground", true);
585 button: button, screenX: offsetX, screenY: offsetY,
586 ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
589 DOM(elem).mousedown(params).mouseup(params);
591 let sel = util.selectionController(win);
592 sel.getSelection(sel.SELECTION_FOCUS_REGION).collapseToStart();
597 * Resets the caret position so that it resides within the current
600 resetCaret: function resetCaret() {
601 function visible(range) util.intersection(DOM(range).rect, viewport);
603 function getRanges(rect) {
604 let nodes = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
605 .nodesFromRect(rect.x, rect.y, 0, rect.width, rect.height, 0, false, false);
606 return Array.filter(nodes, n => n instanceof Ci.nsIDOMText)
607 .map(RangeFind.nodeContents);
610 let win = this.focusedFrame;
611 let doc = win.document;
612 let sel = win.getSelection();
613 let { viewport } = DOM(win);
615 if (sel.rangeCount) {
616 var range = sel.getRangeAt(0);
617 if (visible(range).height > 0)
620 var { rect } = DOM(range);
621 var reverse = rect.bottom > viewport.bottom;
623 rect = { x: rect.left, y: 0, width: rect.width, height: win.innerHeight };
626 let w = win.innerWidth;
627 rect = { x: w / 3, y: 0, width: w / 3, height: win.innerHeight };
630 var reduce = (a, b) => DOM(a).rect.top < DOM(b).rect.top ? a : b;
634 reduce = (a, b) => DOM(b).rect.bottom > DOM(a).rect.bottom ? b : a;
636 y = win.innerHeight - 1;
639 let ranges = getRanges(rect);
641 ranges = getRanges({ x: 0, y: y, width: win.innerWidth, height: 0 });
644 range = ranges.reduce(reduce);
647 range.collapse(!reverse);
648 sel.removeAllRanges();
651 if (visible(range).height > 0)
654 var { startContainer, startOffset } = range;
655 sel.modify("move", dir, "line");
656 range = sel.getRangeAt(0);
658 while (startContainer != range.startContainer || startOffset != range.startOffset);
660 sel.modify("move", reverse ? "forward" : "backward", "lineboundary");
665 sel.collapse(doc.body || doc.querySelector("body") || doc.documentElement,
670 * @property {nsISelection} The current document's normal selection.
672 get selection() this.win.getSelection(),
675 * @property {nsISelectionController} The current document's selection
678 get selectionController() util.selectionController(this.focusedFrame),
681 * @property {string|null} The canonical short URL for the current
685 let { uri, doc } = this;
687 function hashify(url) {
688 let newURI = util.newURI(url);
690 if (uri.hasRef && !newURI.hasRef)
691 newURI.ref = uri.ref;
696 for (let shortener of Buffer.uriShorteners)
698 let shortened = shortener(uri, doc);
700 return hashify(shortened.spec);
706 let link = DOM("link[href][rev=canonical], \
707 link[href][rel=shortlink]", doc)
710 return hashify(link);
716 * Opens the appropriate context menu for *elem*.
718 * @param {Node} elem The context element.
720 openContextMenu: deprecated("DOM#contextmenu", function openContextMenu(elem) DOM(elem).contextmenu()),
723 * Saves a page link to disk.
725 * @param {HTMLAnchorElement} elem The page link to save.
726 * @param {boolean} overwrite If true, overwrite any existing file.
728 saveLink: function saveLink(elem, overwrite) {
729 let { completion, dactyl, io } = this.modules;
732 let doc = elem.ownerDocument;
733 let uri = util.newURI(elem.href || elem.src, null, util.newURI(elem.baseURI));
734 let referrer = util.newURI(doc.documentURI, doc.characterSet);
737 services.security.checkLoadURIWithPrincipal(doc.nodePrincipal, uri,
738 services.security.STANDARD);
740 io.CommandFileMode(_("buffer.prompt.saveLink") + " ", {
741 onSubmit: function (path) {
742 let file = io.File(path);
743 if (file.exists() && file.isDirectory())
744 file.append(Buffer.getDefaultNames(elem)[0][0]);
746 util.assert(!file.exists() || overwrite, _("io.existsNoOverride", file.path));
750 file.create(File.NORMAL_FILE_TYPE, octal(644));
753 util.assert(false, _("save.invalidDestination", e.name));
756 self.saveURI({ uri: uri, file: file, context: elem });
759 completer: function (context) completion.savePage(context, elem)
768 * Saves the contents of a URI to disk.
770 * @param {nsIURI} uri The URI to save
771 * @param {nsIFile} file The file into which to write the result.
773 saveURI: function saveURI(params) {
774 if (params instanceof Ci.nsIURI)
776 params = { uri: arguments[0], file: arguments[1],
777 callback: arguments[2], self: arguments[3] };
779 var persist = services.Persist();
780 persist.persistFlags = persist.PERSIST_FLAGS_FROM_CACHE
781 | persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
783 let window = this.topWindow;
784 let privacy = sanitizer.getContext(params.context || this.win);
785 let file = File(params.file);
787 file.create(Ci.nsIFile.NORMAL_FILE_TYPE, octal(666));
789 let downloadListener = new window.DownloadListener(window,
790 services.Transfer(params.uri, file.URI, "", null, null, null,
791 persist, privacy && privacy.usePrivateBrowsing));
793 var { callback, self } = params;
795 persist.progressListener = update(Object.create(downloadListener), {
796 onStateChange: util.wrapCallback(function onStateChange(progress, request, flags, status) {
797 if (callback && (flags & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
798 util.trapErrors(callback, self, params.uri, file.file,
799 progress, request, flags, status);
801 return onStateChange.superapply(this, arguments);
805 persist.progressListener = downloadListener;
807 persist.saveURI(params.uri, null, null, null, null,
812 * Scrolls the currently active element horizontally. See
813 * {@link Buffer.scrollHorizontal} for parameters.
815 scrollHorizontal: function scrollHorizontal(increment, number)
816 Buffer.scrollHorizontal(this.findScrollable(number, true), increment, number),
819 * Scrolls the currently active element vertically. See
820 * {@link Buffer.scrollVertical} for parameters.
822 scrollVertical: function scrollVertical(increment, number)
823 Buffer.scrollVertical(this.findScrollable(number, false), increment, number),
826 * Scrolls the currently active element to the given horizontal and
827 * vertical percentages. See {@link Buffer.scrollToPercent} for
830 scrollToPercent: function scrollToPercent(horizontal, vertical, dir)
831 Buffer.scrollToPercent(this.findScrollable(dir || 0, vertical == null), horizontal, vertical),
834 * Scrolls the currently active element to the given horizontal and
835 * vertical positions. See {@link Buffer.scrollToPosition} for
838 scrollToPosition: function scrollToPosition(horizontal, vertical)
839 Buffer.scrollToPosition(this.findScrollable(0, vertical == null), horizontal, vertical),
841 _scrollByScrollSize: function _scrollByScrollSize(count, direction) {
842 let { options } = this.modules;
845 options["scroll"] = count;
846 this.scrollByScrollSize(direction);
850 * Scrolls the buffer vertically 'scroll' lines.
852 * @param {boolean} direction The direction to scroll. If true then
853 * scroll up and if false scroll down.
854 * @param {number} count The multiple of 'scroll' lines to scroll.
857 scrollByScrollSize: function scrollByScrollSize(direction, count=1) {
858 let { options } = this.modules;
860 direction = direction ? 1 : -1;
862 if (options["scroll"] > 0)
863 this.scrollVertical("lines", options["scroll"] * direction);
865 this.scrollVertical("pages", direction / 2);
869 * Find the best candidate scrollable element for the given
870 * direction and orientation.
872 * @param {number} dir The direction in which the element must be
873 * able to scroll. Negative numbers represent up or left, while
874 * positive numbers represent down or right.
875 * @param {boolean} horizontal If true, look for horizontally
876 * scrollable elements, otherwise look for vertically scrollable
879 findScrollable: function findScrollable(dir, horizontal) {
880 function find(elem) {
881 while (elem && !(elem instanceof Ci.nsIDOMElement) && elem.parentNode)
882 elem = elem.parentNode;
883 for (; elem instanceof Ci.nsIDOMElement; elem = elem.parentNode)
884 if (Buffer.isScrollable(elem, dir, horizontal))
891 var elem = this.focusedFrame.document.activeElement;
892 if (elem == elem.ownerDocument.body)
898 var sel = this.focusedFrame.getSelection();
902 if (!elem && sel && sel.rangeCount)
903 elem = sel.getRangeAt(0).startContainer;
907 for (let e in DOM(Buffer.SCROLLABLE_SEARCH_SELECTOR,
908 this.focusedFrame.document)) {
909 if (Buffer.isScrollable(e, dir, horizontal)) {
911 let a = r.width * r.height;
919 util.trapErrors("focus", elem);
924 if (!(elem instanceof Ci.nsIDOMElement)) {
925 let doc = this.findScrollableWindow().document;
926 elem = find(doc.body || doc.getElementsByTagName("body")[0] ||
927 doc.documentElement);
929 let doc = this.focusedFrame.document;
930 return util.assert(elem || doc.body || doc.documentElement);
934 * Find the best candidate scrollable frame in the current buffer.
936 findScrollableWindow: function findScrollableWindow() {
937 let { document } = this.topWindow;
939 let win = document.commandDispatcher.focusedWindow;
940 if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
943 let win = this.focusedFrame;
944 if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
948 if (win.scrollMaxX > 0 || win.scrollMaxY > 0)
951 for (let frame in array.iterValues(win.frames))
952 if (frame.scrollMaxX > 0 || frame.scrollMaxY > 0)
959 * Finds the next visible element for the node path in 'jumptags'
962 * @param {string} arg The element in 'jumptags' to use for the search.
963 * @param {number} count The number of elements to jump.
965 * @param {boolean} reverse If true, search backwards. @optional
966 * @param {boolean} offScreen If true, include only off-screen elements. @optional
968 findJump: function findJump(arg, count, reverse, offScreen) {
969 let { marks, options } = this.modules;
975 let path = options["jumptags"][arg];
976 util.assert(path, _("error.invalidArgument", arg));
978 let distance = reverse ? rect => -rect.top
981 let elems = [[e, distance(e.getBoundingClientRect())]
982 for (e in path.matcher(this.focusedFrame.document))]
983 .filter(e => e[1] > FUDGE)
984 .sort((a, b) => a[1] - b[1]);
986 if (offScreen && !reverse)
987 elems = elems.filter(function (e) e[1] > this, this.topWindow.innerHeight);
989 let idx = Math.min((count || 1) - 1, elems.length);
990 util.assert(idx in elems);
992 let elem = elems[idx][0];
993 elem.scrollIntoView(true);
995 let sel = elem.ownerDocument.defaultView.getSelection();
996 sel.removeAllRanges();
997 sel.addRange(RangeFind.endpoint(RangeFind.nodeRange(elem), true));
1000 // TODO: allow callback for filtering out unwanted frames? User defined?
1002 * Shifts the focus to another frame within the buffer. Each buffer
1003 * contains at least one frame.
1005 * @param {number} count The number of frames to skip through. A negative
1006 * count skips backwards.
1008 shiftFrameFocus: function shiftFrameFocus(count) {
1009 if (!(this.doc instanceof Ci.nsIDOMHTMLDocument))
1012 let frames = this.allFrames();
1014 if (frames.length == 0) // currently top is always included
1017 // remove all hidden frames
1018 frames = frames.filter(frame => !(frame.document.body instanceof Ci.nsIDOMHTMLFrameSetElement))
1019 .filter(frame => !frame.frameElement ||
1020 let (rect = frame.frameElement.getBoundingClientRect())
1021 rect.width && rect.height);
1023 // find the currently focused frame index
1024 let current = Math.max(0, frames.indexOf(this.focusedFrame));
1026 // calculate the next frame to focus
1027 let next = current + count;
1028 if (next < 0 || next >= frames.length)
1030 next = Math.constrain(next, 0, frames.length - 1);
1032 // focus next frame and scroll into view
1033 DOM(frames[next]).focus();
1034 if (frames[next] != this.win)
1035 DOM(frames[next].frameElement).scrollIntoView();
1037 // add the frame indicator
1038 let doc = frames[next].document;
1039 let indicator = DOM(["div", { highlight: "FrameIndicator" }], doc)
1040 .appendTo(doc.body || doc.documentElement || doc);
1042 util.timeout(function () { indicator.remove(); }, 500);
1045 //doc.body.setAttributeNS(NS, "activeframe", "true");
1046 //util.timeout(function () { doc.body.removeAttributeNS(NS, "activeframe"); }, 500);
1049 // similar to pageInfo
1050 // TODO: print more useful information, just like the DOM inspector
1052 * Displays information about the specified element.
1054 * @param {Node} elem The element to query.
1056 showElementInfo: function showElementInfo(elem) {
1057 let { dactyl } = this.modules;
1059 dactyl.echo(["", /*L*/"Element:", ["br"], util.objectToString(elem, true)]);
1063 * Displays information about the current buffer.
1065 * @param {boolean} verbose Display more verbose information.
1066 * @param {string} sections A string limiting the displayed sections.
1067 * @default The value of 'pageinfo'.
1069 showPageInfo: function showPageInfo(verbose, sections) {
1070 let { commandline, dactyl, options } = this.modules;
1072 // Ctrl-g single line output
1074 let file = this.win.location.pathname.split("/").pop() || _("buffer.noName");
1075 let title = this.win.document.title || _("buffer.noTitle");
1077 let info = template.map(
1078 (sections || options["pageinfo"])
1079 .map((opt) => Buffer.pageInfo[opt].action.call(this)),
1080 res => (res && iter(res).join(", ") || undefined),
1083 if (bookmarkcache.isBookmarked(this.URL))
1084 info += ", " + _("buffer.bookmarked");
1086 let pageInfoText = [file.quote(), " [", info, "] ", title].join("");
1087 dactyl.echo(pageInfoText, commandline.FORCE_SINGLELINE);
1091 let list = template.map(sections || options["pageinfo"], (option) => {
1092 let { action, title } = Buffer.pageInfo[option];
1093 return template.table(title, action.call(this, true));
1096 commandline.commandOutput(list);
1100 * Stops loading and animations in the current content.
1102 stop: function stop() {
1103 let { config } = this.modules;
1108 this.docShell.stop(this.docShell.STOP_ALL);
1112 * Opens a viewer to inspect the source of the currently selected
1115 viewSelectionSource: function viewSelectionSource() {
1116 // copied (and tuned somewhat) from browser.jar -> nsContextMenu.js
1117 let { document, window } = this.topWindow;
1119 let win = document.commandDispatcher.focusedWindow;
1120 if (win == this.topWindow)
1121 win = this.focusedFrame;
1123 let charset = win ? "charset=" + win.document.characterSet : null;
1125 window.openDialog("chrome://global/content/viewPartialSource.xul",
1126 "_blank", "scrollbars,resizable,chrome,dialog=no",
1127 null, charset, win.getSelection(), "selection");
1131 * Opens a viewer to inspect the source of the current buffer or the
1132 * specified *url*. Either the default viewer or the configured external
1135 * @param {string|object|null} loc If a string, the URL of the source,
1136 * otherwise an object with some or all of the following properties:
1138 * url: The URL to view.
1139 * doc: The document to view.
1140 * line: The line to select.
1141 * column: The column to select.
1143 * If no URL is provided, the current document is used.
1144 * @default The current buffer.
1145 * @param {boolean} useExternalEditor View the source in the external editor.
1147 viewSource: function viewSource(loc, useExternalEditor) {
1148 let { dactyl, editor, history, options } = this.modules;
1150 let window = this.topWindow;
1152 let doc = this.focusedFrame.document;
1154 if (isObject(loc)) {
1155 if (options.get("editor").has("line") || !loc.url)
1156 this.viewSourceExternally(loc.doc || loc.url || doc, loc);
1158 window.openDialog("chrome://global/content/viewSource.xul",
1159 "_blank", "all,dialog=no",
1160 loc.url, null, null, loc.line);
1163 if (useExternalEditor)
1164 this.viewSourceExternally(loc || doc);
1166 let url = loc || doc.location.href;
1167 const PREFIX = "view-source:";
1168 if (url.startsWith(PREFIX))
1169 url = url.substr(PREFIX.length);
1173 let sh = history.session;
1174 if (sh[sh.index].URI.spec == url)
1175 this.docShell.gotoIndex(sh.index);
1177 dactyl.open(url, { hide: true });
1183 * Launches an editor to view the source of the given document. The
1184 * contents of the document are saved to a temporary local file and
1185 * removed when the editor returns. This function returns
1188 * @param {Document} doc The document to view.
1189 * @param {function|object} callback If a function, the callback to be
1190 * called with two arguments: the nsIFile of the file, and temp, a
1191 * boolean which is true if the file is temporary. Otherwise, an object
1192 * with line and column properties used to determine where to open the
1196 viewSourceExternally: Class("viewSourceExternally",
1197 XPCOM([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), {
1198 init: function init(doc, callback) {
1199 this.callback = callable(callback) ? callback :
1200 function (file, temp) {
1201 let { editor } = overlay.activeModules;
1203 editor.editFileExternally(update({ file: file.path }, callback || {}),
1204 function () { temp && file.remove(false); });
1208 if (isString(doc)) {
1209 var privacyContext = null;
1210 var uri = util.newURI(doc);
1213 privacyContext = sanitizer.getContext(doc);
1214 uri = util.newURI(doc.location.href);
1217 let ext = uri.fileExtension || "txt";
1218 if (doc.contentType)
1220 ext = services.mime.getPrimaryExtension(doc.contentType, ext);
1225 return io.withTempFiles(function (temp) {
1226 let encoder = services.HtmlEncoder();
1227 encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1228 temp.write(encoder.encodeToString(), ">");
1229 return this.callback(temp, true);
1230 }, this, true, ext);
1232 let file = util.getFile(uri);
1234 this.callback(file, false);
1236 this.file = io.createTempFile();
1237 var persist = services.Persist();
1238 persist.persistFlags = persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
1239 persist.progressListener = this;
1240 persist.saveURI(uri, null, null, null, null, this.file,
1246 onStateChange: function onStateChange(progress, request, flags, status) {
1247 if ((flags & this.STATE_STOP) && status == 0) {
1249 var ok = this.callback(this.file, true);
1253 this.file.remove(false);
1261 * Increases the zoom level of the current buffer.
1263 * @param {number} steps The number of zoom levels to jump.
1264 * @param {boolean} fullZoom Whether to use full zoom or text zoom.
1266 zoomIn: function zoomIn(steps, fullZoom) {
1267 this.bumpZoomLevel(steps, fullZoom);
1271 * Decreases the zoom level of the current buffer.
1273 * @param {number} steps The number of zoom levels to jump.
1274 * @param {boolean} fullZoom Whether to use full zoom or text zoom.
1276 zoomOut: function zoomOut(steps, fullZoom) {
1277 this.bumpZoomLevel(-steps, fullZoom);
1281 * Adjusts the page zoom of the current buffer to the given absolute
1284 * @param {number} value The new zoom value as a possibly fractional
1285 * percentage of the page's natural size.
1286 * @param {boolean} fullZoom If true, zoom all content of the page,
1287 * including raster images. If false, zoom only text. If omitted,
1288 * use the current zoom function. @optional
1289 * @throws {FailedAssertion} if the given *value* is not within the
1290 * closed range [Buffer.ZOOM_MIN, Buffer.ZOOM_MAX].
1292 setZoom: function setZoom(value, fullZoom) {
1293 let { dactyl, statusline, storage } = this.modules;
1294 let { ZoomManager } = this;
1296 if (fullZoom === undefined)
1297 fullZoom = ZoomManager.useFullZoom;
1299 ZoomManager.useFullZoom = fullZoom;
1303 this.contentViewer.textZoom = fullZoom ? 1 : value;
1304 this.contentViewer.fullZoom = !fullZoom ? 1 : value;
1306 catch (e if e == Cr.NS_ERROR_ILLEGAL_VALUE) {
1307 return dactyl.echoerr(_("zoom.illegal"));
1310 if (prefs.get("browser.zoom.siteSpecific")) {
1311 var privacy = sanitizer.getContext(this.win);
1313 this.prefs.clear("browser.content.full-zoom");
1314 this.prefs.clear("dactyl.content.full-zoom");
1317 this.prefs.set("browser.content.full-zoom", value);
1318 this.prefs.set("dactyl.content.full-zoom", fullZoom);
1322 statusline.updateZoomLevel();
1326 * Updates the zoom level of this buffer from a content preference.
1328 updateZoom: promises.task(function updateZoom() {
1331 if (prefs.get("browser.zoom.siteSpecific")) {
1332 let val = yield this.prefs.get("dactyl.content.full-zoom");
1334 if (val != null && uri.equals(this.uri) && val != prefs.get("browser.zoom.full"))
1335 [this.contentViewer.textZoom, this.contentViewer.fullZoom] =
1336 [this.contentViewer.fullZoom, this.contentViewer.textZoom];
1341 * Adjusts the page zoom of the current buffer relative to the
1342 * current zoom level.
1344 * @param {number} steps The integral number of natural fractions by which
1345 * to adjust the current page zoom. If positive, the zoom level is
1346 * increased, if negative it is decreased.
1347 * @param {boolean} fullZoom If true, zoom all content of the page,
1348 * including raster images. If false, zoom only text. If omitted, use
1349 * the current zoom function. @optional
1350 * @throws {FailedAssertion} if the buffer's zoom level is already at its
1351 * extreme in the given direction.
1353 bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) {
1354 let { ZoomManager } = this;
1356 if (fullZoom === undefined)
1357 fullZoom = ZoomManager.useFullZoom;
1359 let values = ZoomManager.zoomValues;
1360 let cur = values.indexOf(ZoomManager.snap(this.zoomLevel / 100));
1361 let i = Math.constrain(cur + steps, 0, values.length - 1);
1363 util.assert(i != cur || fullZoom != ZoomManager.useFullZoom);
1365 this.setZoom(Math.round(values[i] * 100), fullZoom);
1368 getAllFrames: deprecated("buffer.allFrames", "allFrames"),
1369 scrollTop: deprecated("buffer.scrollToPercent", function scrollTop() this.scrollToPercent(null, 0)),
1370 scrollBottom: deprecated("buffer.scrollToPercent", function scrollBottom() this.scrollToPercent(null, 100)),
1371 scrollStart: deprecated("buffer.scrollToPercent", function scrollStart() this.scrollToPercent(0, null)),
1372 scrollEnd: deprecated("buffer.scrollToPercent", function scrollEnd() this.scrollToPercent(100, null)),
1373 scrollColumns: deprecated("buffer.scrollHorizontal", function scrollColumns(cols) this.scrollHorizontal("columns", cols)),
1374 scrollPages: deprecated("buffer.scrollHorizontal", function scrollPages(pages) this.scrollVertical("pages", pages)),
1375 scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) this.win.scrollTo(x, y)),
1376 textZoom: deprecated("buffer.zoomValue/buffer.fullZoom", function textZoom() this.contentViewer.markupDocumentViewer.textZoom * 100)
1379 * The pattern used to search for a scrollable element when we have
1380 * no starting point.
1382 SCROLLABLE_SEARCH_SELECTOR: "html, body, div",
1384 PageInfo: Struct("PageInfo", "name", "title", "action")
1390 * Adds a new section to the page information output.
1392 * @param {string} option The section's value in 'pageinfo'.
1393 * @param {string} title The heading for this section's
1395 * @param {function} func The function to generate this
1398 addPageInfoSection: function addPageInfoSection(option, title, func) {
1399 this.pageInfo[option] = Buffer.PageInfo(option, title, func);
1405 * Adds a new URI shortener for documents matching the given filter.
1407 * @param {string|function(URI, Document):boolean} filter A site filter
1408 * string or a function which accepts a URI and a document and
1409 * returns true if it can shorten the document's URI.
1410 * @param {function(URI, Document):URI} shortener Returns a shortened
1411 * URL for the given URI and document.
1413 addURIShortener: function addURIShortener(filter, shortener) {
1414 if (isString(filter))
1415 filter = Group.compileFilter(filter);
1417 this.uriShorteners.push(function uriShortener(uri, doc) {
1418 if (filter(uri, doc))
1419 return shortener(uri, doc);
1423 Scrollable: function Scrollable(elem) {
1424 if (elem instanceof Ci.nsIDOMElement)
1426 if (isinstance(elem, [Ci.nsIDOMWindow, Ci.nsIDOMDocument]))
1428 __proto__: elem.documentElement || elem.ownerDocument.documentElement,
1430 win: elem.defaultView || elem.ownerDocument.defaultView,
1432 get clientWidth() this.win.innerWidth,
1433 get clientHeight() this.win.innerHeight,
1435 get scrollWidth() this.win.scrollMaxX + this.win.innerWidth,
1436 get scrollHeight() this.win.scrollMaxY + this.win.innerHeight,
1438 get scrollLeftMax() this.win.scrollMaxX,
1439 get scrollRightMax() this.win.scrollMaxY,
1441 get scrollLeft() this.win.scrollX,
1442 set scrollLeft(val) { this.win.scrollTo(val, this.win.scrollY); },
1444 get scrollTop() this.win.scrollY,
1445 set scrollTop(val) { this.win.scrollTo(this.win.scrollX, val); }
1450 get ZOOM_MIN() prefs.get("zoom.minPercent"),
1451 get ZOOM_MAX() prefs.get("zoom.maxPercent"),
1453 setZoom: deprecated("buffer.setZoom", function setZoom()
1454 let ({ buffer } = overlay.activeModules) buffer.setZoom.apply(buffer, arguments)),
1455 bumpZoomLevel: deprecated("buffer.bumpZoomLevel", function bumpZoomLevel()
1456 let ({ buffer } = overlay.activeModules) buffer.bumpZoomLevel.apply(buffer, arguments)),
1459 * Returns the currently selected word in *win*. If the selection is
1460 * null, it tries to guess the word that the caret is positioned in.
1464 currentWord: function currentWord(win, select) {
1465 let { Editor, options } = Buffer(win).modules;
1467 let selection = win.getSelection();
1468 if (selection.rangeCount == 0)
1471 let range = selection.getRangeAt(0).cloneRange();
1472 if (range.collapsed) {
1473 let re = options.get("iskeyword").regexp;
1474 Editor.extendRange(range, true, re, true);
1475 Editor.extendRange(range, false, re, true);
1478 selection.removeAllRanges();
1479 selection.addRange(range);
1481 return DOM.stringify(range);
1484 getDefaultNames: function getDefaultNames(node) {
1485 let url = node.href || node.src || node.documentURI;
1486 let currExt = url.replace(/^.*?(?:\.([a-z0-9]+))?$/i, "$1").toLowerCase();
1489 if (isinstance(node, [Ci.nsIDOMDocument,
1490 Ci.nsIDOMHTMLImageElement])) {
1491 let type = node.contentType || node.QueryInterface(Ci.nsIImageLoadingContent)
1492 .getRequest(0).mimeType;
1494 if (type === "text/plain")
1495 ext = "." + (currExt || "txt");
1497 ext = "." + services.mime.getPrimaryExtension(type, currExt);
1500 ext = "." + currExt;
1502 let re = ext ? RegExp("(\\." + currExt + ")?$", "i") : /$/;
1506 names.push([node.title,
1507 _("buffer.save.pageName")]);
1510 names.push([node.alt,
1511 _("buffer.save.altText")]);
1513 if (!isinstance(node, Ci.nsIDOMDocument) && node.textContent)
1514 names.push([node.textContent,
1515 _("buffer.save.linkText")]);
1517 names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")),
1518 _("buffer.save.filename")]);
1520 return names.filter(([leaf, title]) => leaf)
1521 .map(([leaf, title]) => [leaf.replace(config.OS.illegalCharacters, encodeURIComponent)
1522 .replace(re, ext), title]);
1525 findScrollableWindow: deprecated("buffer.findScrollableWindow", function findScrollableWindow()
1526 let ({ buffer } = overlay.activeModules) buffer.findScrollableWindow.apply(buffer, arguments)),
1527 findScrollable: deprecated("buffer.findScrollable", function findScrollable()
1528 let ({ buffer } = overlay.activeModules) buffer.findScrollable.apply(buffer, arguments)),
1530 isScrollable: function isScrollable(elem, dir, horizontal) {
1531 if (!DOM(elem).isScrollable(horizontal ? "horizontal" : "vertical"))
1534 return this.canScroll(elem, dir, horizontal);
1537 canScroll: function canScroll(elem, dir, horizontal) {
1538 let pos = "scrollTop", size = "clientHeight", end = "scrollHeight", layoutSize = "offsetHeight",
1539 overflow = "overflowX", border1 = "borderTopWidth", border2 = "borderBottomWidth";
1541 pos = "scrollLeft", size = "clientWidth", end = "scrollWidth", layoutSize = "offsetWidth",
1542 overflow = "overflowX", border1 = "borderLeftWidth", border2 = "borderRightWidth";
1545 return elem[pos] > 0;
1547 let max = pos + "Max";
1549 if (elem[pos] < elem[max])
1553 return elem[pos] > 0;
1556 let style = DOM(elem).style;
1557 let borderSize = Math.round(parseFloat(style[border1]) + parseFloat(style[border2]));
1558 let realSize = elem[size];
1560 // Stupid Gecko eccentricities. May fail for quirks mode documents.
1561 if (elem[size] + borderSize >= elem[end] || elem[size] == 0) // Stupid, fallible heuristic.
1564 if (style[overflow] == "hidden")
1565 realSize += borderSize;
1566 return dir > 0 && elem[pos] + realSize < elem[end] || !dir && realSize < elem[end];
1570 * Scroll the contents of the given element to the absolute *left*
1571 * and *top* pixel offsets.
1573 * @param {Element} elem The element to scroll.
1574 * @param {number|null} left The left absolute pixel offset. If
1575 * null, to not alter the horizontal scroll offset.
1576 * @param {number|null} top The top absolute pixel offset. If
1577 * null, to not alter the vertical scroll offset.
1578 * @param {string} reason The reason for the scroll event. See
1579 * {@link marks.push}. @optional
1581 scrollTo: function scrollTo(elem, left, top, reason) {
1582 let doc = elem.ownerDocument || elem.document || elem;
1584 let { buffer, marks, options } = util.topWindow(doc.defaultView).dactyl.modules;
1586 if (~[elem, elem.document, elem.ownerDocument].indexOf(buffer.focusedFrame.document))
1589 if (options["scrollsteps"] > 1)
1590 return this.smoothScrollTo(elem, left, top);
1592 elem = Buffer.Scrollable(elem);
1594 elem.scrollLeft = left;
1596 elem.scrollTop = top;
1600 * Like scrollTo, but scrolls more smoothly and does not update
1603 smoothScrollTo: let (timers = WeakMap())
1604 function smoothScrollTo(node, x, y) {
1605 let { options } = overlay.activeModules;
1607 let time = options["scrolltime"];
1608 let steps = options["scrollsteps"];
1610 let elem = Buffer.Scrollable(node);
1612 if (timers.has(node))
1613 timers.get(node).cancel();
1616 x = elem.scrollLeft;
1620 x = node.dactylScrollDestX = Math.min(x, elem.scrollWidth - elem.clientWidth);
1621 y = node.dactylScrollDestY = Math.min(y, elem.scrollHeight - elem.clientHeight);
1622 let [startX, startY] = [elem.scrollLeft, elem.scrollTop];
1625 if (n++ === steps) {
1626 elem.scrollLeft = x;
1628 delete node.dactylScrollDestX;
1629 delete node.dactylScrollDestY;
1632 elem.scrollLeft = startX + (x - startX) / steps * n;
1633 elem.scrollTop = startY + (y - startY) / steps * n;
1634 timers.set(node, util.timeout(next, time / steps));
1640 * Scrolls the currently given element horizontally.
1642 * @param {Element} elem The element to scroll.
1643 * @param {string} unit The increment by which to scroll.
1644 * Possible values are: "columns", "pages"
1645 * @param {number} number The possibly fractional number of
1646 * increments to scroll. Positive values scroll to the right while
1647 * negative values scroll to the left.
1648 * @throws {FailedAssertion} if scrolling is not possible in the
1651 scrollHorizontal: function scrollHorizontal(node, unit, number) {
1652 let fontSize = parseInt(DOM(node).style.fontSize);
1654 let elem = Buffer.Scrollable(node);
1656 if (unit == "columns")
1657 increment = fontSize; // Good enough, I suppose.
1658 else if (unit == "pages")
1659 increment = elem.clientWidth - fontSize;
1663 util.assert(number < 0 ? elem.scrollLeft > 0 : elem.scrollLeft < elem.scrollWidth - elem.clientWidth);
1665 let left = node.dactylScrollDestX !== undefined ? node.dactylScrollDestX : elem.scrollLeft;
1666 node.dactylScrollDestX = undefined;
1668 Buffer.scrollTo(node, left + number * increment, null, "h-" + unit);
1672 * Scrolls the given element vertically.
1674 * @param {Node} node The node to scroll.
1675 * @param {string} unit The increment by which to scroll.
1676 * Possible values are: "lines", "pages"
1677 * @param {number} number The possibly fractional number of
1678 * increments to scroll. Positive values scroll upward while
1679 * negative values scroll downward.
1680 * @throws {FailedAssertion} if scrolling is not possible in the
1683 scrollVertical: function scrollVertical(node, unit, number) {
1684 let fontSize = parseInt(DOM(node).style.lineHeight);
1686 let elem = Buffer.Scrollable(node);
1688 if (unit == "lines")
1689 increment = fontSize;
1690 else if (unit == "pages")
1691 increment = elem.clientHeight - fontSize;
1695 util.assert(number < 0 ? elem.scrollTop > 0 : elem.scrollTop < elem.scrollHeight - elem.clientHeight);
1697 let top = node.dactylScrollDestY !== undefined ? node.dactylScrollDestY : elem.scrollTop;
1698 node.dactylScrollDestY = undefined;
1700 Buffer.scrollTo(node, null, top + number * increment, "v-" + unit);
1704 * Scrolls the currently active element to the given horizontal and
1705 * vertical percentages.
1707 * @param {Element} elem The element to scroll.
1708 * @param {number|null} horizontal The possibly fractional
1709 * percentage of the current viewport width to scroll to. If null,
1710 * do not scroll horizontally.
1711 * @param {number|null} vertical The possibly fractional percentage
1712 * of the current viewport height to scroll to. If null, do not
1713 * scroll vertically.
1715 scrollToPercent: function scrollToPercent(node, horizontal, vertical) {
1716 let elem = Buffer.Scrollable(node);
1717 Buffer.scrollTo(node,
1718 horizontal == null ? null
1719 : (elem.scrollWidth - elem.clientWidth) * (horizontal / 100),
1720 vertical == null ? null
1721 : (elem.scrollHeight - elem.clientHeight) * (vertical / 100));
1725 * Scrolls the currently active element to the given horizontal and
1726 * vertical position.
1728 * @param {Element} elem The element to scroll.
1729 * @param {number|null} horizontal The possibly fractional
1730 * line ordinal to scroll to.
1731 * @param {number|null} vertical The possibly fractional
1732 * column ordinal to scroll to.
1734 scrollToPosition: function scrollToPosition(elem, horizontal, vertical) {
1735 let style = DOM(elem.body || elem).style;
1736 Buffer.scrollTo(elem,
1737 horizontal == null ? null :
1738 horizontal == 0 ? 0 : this._exWidth(elem) * horizontal,
1739 vertical == null ? null : parseFloat(style.lineHeight) * vertical);
1743 * Returns the current scroll position as understood by
1744 * {@link #scrollToPosition}.
1746 * @param {Element} elem The element to scroll.
1748 getScrollPosition: function getPosition(node) {
1749 let style = DOM(node.body || node).style;
1751 let elem = Buffer.Scrollable(node);
1753 x: elem.scrollLeft && elem.scrollLeft / this._exWidth(node),
1754 y: elem.scrollTop / parseFloat(style.lineHeight)
1758 _exWidth: function _exWidth(elem) {
1760 let div = DOM(["elem", { style: "width: 1ex !important; position: absolute !important; padding: 0 !important; display: block;" }],
1761 elem.ownerDocument).appendTo(elem.body || elem);
1763 return parseFloat(div.style.width);
1770 return parseFloat(DOM(elem).fontSize) / 1.618;
1774 openUploadPrompt: function openUploadPrompt(elem) {
1775 let { io } = overlay.activeModules;
1777 io.CommandFileMode(_("buffer.prompt.uploadFile") + " ", {
1778 onSubmit: function onSubmit(path) {
1779 let file = io.File(path);
1780 util.assert(file.exists());
1782 DOM(elem).val(file.path).change();
1784 }).open(elem.value);
1787 init: function init(dactyl, modules, window) {
1788 init.superapply(this, arguments);
1790 dactyl.commands["buffer.viewSource"] = function (event) {
1791 let elem = event.originalTarget;
1792 let obj = { url: elem.getAttribute("href"), line: Number(elem.getAttribute("line")) };
1793 if (elem.hasAttribute("column"))
1794 obj.column = elem.getAttribute("column");
1796 modules.buffer.viewSource(obj);
1799 commands: function initCommands(dactyl, modules, window) {
1800 let { buffer, commands, config, options } = modules;
1802 commands.add(["frameo[nly]"],
1803 "Show only the current frame's page",
1805 dactyl.open(buffer.focusedFrame.location.href);
1809 commands.add(["ha[rdcopy]"],
1810 "Print current document",
1814 // FIXME: arg handling is a bit of a mess, check for filename
1815 dactyl.assert(!arg || arg[0] == ">",
1816 _("error.trailingCharacters"));
1818 const PRINTER = "PostScript/default";
1819 const BRANCH = "printer_" + PRINTER + ".";
1820 const BRANCHES = ["print.", BRANCH, "print." + BRANCH];
1821 function set(pref, value) {
1822 BRANCHES.forEach(function (branch) { prefs.set(branch + pref, value); });
1825 let settings = services.printSettings.newPrintSettings;
1826 settings.printSilent = args.bang;
1828 settings.printToFile = true;
1829 settings.toFileName = io.File(arg.substr(1)).path;
1830 settings.outputFormat = settings.kOutputFormatPDF;
1832 dactyl.echomsg(_("print.toFile", arg.substr(1)));
1835 dactyl.echomsg(_("print.sending"));
1838 prefs.set("print.show_print_progress", !args.bang);
1841 config.browser.contentWindow
1842 .QueryInterface(Ci.nsIInterfaceRequestor)
1843 .getInterface(Ci.nsIWebBrowserPrint).print(settings, null);
1845 dactyl.echomsg(_("print.sent"));
1850 completer: function (context, args) {
1851 if (args.bang && /^>/.test(context.filter))
1852 context.fork("file", 1, modules.completion, "file");
1857 commands.add(["pa[geinfo]"],
1858 "Show various page information",
1861 let opt = options.get("pageinfo");
1863 dactyl.assert(!arg || opt.validator(opt.parse(arg)),
1864 _("error.invalidArgument", arg));
1865 buffer.showPageInfo(true, arg);
1869 completer: function (context) {
1870 modules.completion.optionValue(context, "pageinfo", "+", "");
1871 context.title = ["Page Info"];
1875 commands.add(["pagest[yle]", "pas"],
1876 "Select the author style sheet to apply",
1878 let arg = args[0] || "";
1880 let titles = buffer.alternateStyleSheets.map(sheet => sheet.title);
1882 dactyl.assert(!arg || titles.indexOf(arg) >= 0,
1883 _("error.invalidArgument", arg));
1885 if (options["usermode"])
1886 options["usermode"] = false;
1888 window.stylesheetSwitchAll(buffer.focusedFrame, arg);
1892 completer: function (context) modules.completion.alternateStyleSheet(context),
1896 commands.add(["re[load]"],
1897 "Reload the current web page",
1898 function (args) { modules.tabs.reload(config.browser.mCurrentTab, args.bang); },
1904 // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional?
1905 commands.add(["sav[eas]", "w[rite]"],
1906 "Save current document to disk",
1908 let { commandline, io } = modules;
1909 let { doc, win } = buffer;
1911 let chosenData = null;
1912 let filename = args[0];
1914 let command = commandline.command;
1916 if (filename[0] == "!")
1917 return buffer.viewSourceExternally(buffer.focusedFrame.document,
1919 let output = io.system(filename.substr(1), file);
1920 commandline.command = command;
1921 commandline.commandOutput(["span", { highlight: "CmdOutput" }, output]);
1924 if (/^>>/.test(filename)) {
1925 let file = io.File(filename.replace(/^>>\s*/, ""));
1926 dactyl.assert(args.bang || file.exists() && file.isWritable(),
1927 _("io.notWriteable", file.path.quote()));
1929 return buffer.viewSourceExternally(buffer.focusedFrame.document,
1930 function (tmpFile) {
1932 file.write(tmpFile, ">>");
1935 dactyl.echoerr(_("io.notWriteable", file.path.quote()));
1940 let file = io.File(filename);
1942 if (filename.substr(-1) === File.PATH_SEP || file.exists() && file.isDirectory())
1943 file.append(Buffer.getDefaultNames(doc)[0][0]);
1945 dactyl.assert(args.bang || !file.exists(), _("io.exists"));
1947 chosenData = { file: file.file, uri: util.newURI(doc.location.href) };
1950 // if browser.download.useDownloadDir = false then the "Save As"
1951 // dialog is used with this as the default directory
1952 // TODO: if we're going to do this shouldn't it be done in setCWD or the value restored?
1953 prefs.set("browser.download.lastDir", io.cwd.path);
1956 var contentDisposition = win.QueryInterface(Ci.nsIInterfaceRequestor)
1957 .getInterface(Ci.nsIDOMWindowUtils)
1958 .getDocumentMetadata("content-disposition");
1962 window.internalSave(doc.location.href, doc, null, contentDisposition,
1963 doc.contentType, false, null, chosenData,
1964 doc.referrer ? window.makeURI(doc.referrer) : null,
1970 completer: function (context) {
1971 let { buffer, completion } = modules;
1973 if (context.filter[0] == "!")
1975 if (/^>>/.test(context.filter))
1976 context.advance(/^>>\s*/.exec(context.filter)[0].length);
1978 completion.savePage(context, buffer.doc);
1979 context.fork("file", 0, completion, "file");
1984 commands.add(["st[op]"],
1985 "Stop loading the current web page",
1986 function () { buffer.stop(); },
1989 commands.add(["vie[wsource]"],
1990 "View source code of current document",
1991 function (args) { buffer.viewSource(args[0], args.bang); },
1995 completer: function (context) modules.completion.url(context, "bhf")
1998 commands.add(["zo[om]"],
1999 "Set zoom value of current web page",
2006 else if (/^\d+$/.test(arg))
2007 level = parseInt(arg, 10);
2008 else if (/^[+-]\d+$/.test(arg))
2009 level = Math.round(buffer.zoomLevel + parseInt(arg, 10));
2011 dactyl.assert(false, _("error.trailingCharacters"));
2013 buffer.setZoom(level, args.bang);
2020 completion: function initCompletion(dactyl, modules, window) {
2021 let { CompletionContext, buffer, completion } = modules;
2023 completion.alternateStyleSheet = function alternateStylesheet(context) {
2024 context.title = ["Stylesheet", "Location"];
2026 // unify split style sheets
2027 let styles = iter([s.title, []] for (s in values(buffer.alternateStyleSheets))).toObject();
2029 buffer.alternateStyleSheets.forEach(function (style) {
2030 styles[style.title].push(style.href || _("style.inline"));
2033 context.completions = [[title, href.join(", ")] for ([title, href] in Iterator(styles))];
2036 completion.savePage = function savePage(context, node) {
2037 context.fork("generated", context.filter.replace(/[^/]*$/, "").length,
2038 this, function (context) {
2039 context.generate = function () {
2040 this.incomplete = true;
2041 this.completions = Buffer.getDefaultNames(node);
2042 util.httpGet(node.href || node.src || node.documentURI, {
2044 callback: function callback(xhr) {
2045 context.incomplete = false;
2047 if (/filename="(.*?)"/.test(xhr.getResponseHeader("Content-Disposition")))
2048 context.completions.push([decodeURIComponent(RegExp.$1),
2049 _("buffer.save.suggested")]);
2052 context.completions = context.completions.slice();
2055 notificationCallbacks: Class(XPCOM([Ci.nsIChannelEventSink, Ci.nsIInterfaceRequestor]), {
2056 getInterface: function getInterface(iid) this.QueryInterface(iid),
2058 asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
2059 if (newChannel instanceof Ci.nsIHttpChannel)
2060 newChannel.requestMethod = "HEAD";
2061 callback.onRedirectVerifyCallback(Cr.NS_OK);
2069 events: function initEvents(dactyl, modules, window) {
2070 let { buffer, config, events } = modules;
2072 events.listen(config.browser, "scroll", buffer.bound._updateBufferPosition, false);
2074 mappings: function initMappings(dactyl, modules, window) {
2075 let { Editor, Events, buffer, editor, events, ex, mappings, modes, options, tabs } = modules;
2077 mappings.add([modes.NORMAL],
2078 ["y", "<yank-location>"], "Yank current location to the clipboard",
2080 let { doc, uri } = buffer;
2081 if (uri instanceof Ci.nsIURL)
2082 uri.query = uri.query.replace(/(?:^|&)utm_[^&]+/g, "")
2085 let url = options.get("yankshort").getKey(uri) && buffer.shortURL || uri.spec;
2086 dactyl.clipboardWrite(url, true);
2089 mappings.add([modes.NORMAL],
2090 ["<C-a>", "<increment-url-path>"], "Increment last number in URL",
2091 function ({ count }) { buffer.incrementURL(Math.max(count, 1)); },
2094 mappings.add([modes.NORMAL],
2095 ["<C-x>", "<decrement-url-path>"], "Decrement last number in URL",
2096 function ({ count }) { buffer.incrementURL(-Math.max(count, 1)); },
2099 mappings.add([modes.NORMAL], ["gu", "<open-parent-path>"],
2100 "Go to parent directory",
2101 function ({ count }) { buffer.climbUrlPath(Math.max(count, 1)); },
2104 mappings.add([modes.NORMAL], ["gU", "<open-root-path>"],
2105 "Go to the root of the website",
2106 function () { buffer.climbUrlPath(-1); });
2108 mappings.add([modes.COMMAND], [".", "<repeat-key>"],
2109 "Repeat the last key event",
2110 function ({ count }) {
2111 if (mappings.repeat) {
2112 for (let i in util.interruptibleRange(0, Math.max(count, 1), 100))
2118 mappings.add([modes.NORMAL], ["i", "<Insert>"],
2120 function () { modes.push(modes.CARET); });
2122 mappings.add([modes.NORMAL], ["<C-c>", "<stop-load>"],
2123 "Stop loading the current web page",
2124 function () { ex.stop(); });
2127 mappings.add([modes.NORMAL], ["j", "<Down>", "<C-e>", "<scroll-down-line>"],
2128 "Scroll document down",
2129 function ({ count }) { buffer.scrollVertical("lines", Math.max(count, 1)); },
2132 mappings.add([modes.NORMAL], ["k", "<Up>", "<C-y>", "<scroll-up-line>"],
2133 "Scroll document up",
2134 function ({ count }) { buffer.scrollVertical("lines", -Math.max(count, 1)); },
2137 mappings.add([modes.NORMAL], dactyl.has("mail") ? ["h", "<scroll-left-column>"] : ["h", "<Left>", "<scroll-left-column>"],
2138 "Scroll document to the left",
2139 function ({ count }) { buffer.scrollHorizontal("columns", -Math.max(count, 1)); },
2142 mappings.add([modes.NORMAL], dactyl.has("mail") ? ["l", "<scroll-right-column>"] : ["l", "<Right>", "<scroll-right-column>"],
2143 "Scroll document to the right",
2144 function ({ count }) { buffer.scrollHorizontal("columns", Math.max(count, 1)); },
2147 mappings.add([modes.NORMAL], ["0", "^", "<scroll-begin>"],
2148 "Scroll to the absolute left of the document",
2149 function () { buffer.scrollToPercent(0, null); });
2151 mappings.add([modes.NORMAL], ["$", "<scroll-end>"],
2152 "Scroll to the absolute right of the document",
2153 function () { buffer.scrollToPercent(100, null); });
2155 mappings.add([modes.NORMAL], ["gg", "<Home>", "<scroll-top>"],
2156 "Go to the top of the document",
2157 function ({ count }) { buffer.scrollToPercent(null, count != null ? count : 0,
2158 count != null ? 0 : -1); },
2161 mappings.add([modes.NORMAL], ["G", "<End>", "<scroll-bottom>"],
2162 "Go to the end of the document",
2163 function ({ count }) {
2165 var elem = options.get("linenumbers")
2166 .getLine(buffer.focusedFrame.document,
2169 elem.scrollIntoView(true);
2171 buffer.scrollToPosition(null, count);
2173 buffer.scrollToPercent(null, 100, 1);
2177 mappings.add([modes.NORMAL], ["%", "<scroll-percent>"],
2178 "Scroll to {count} percent of the document",
2179 function ({ count }) {
2180 dactyl.assert(count > 0 && count <= 100);
2181 buffer.scrollToPercent(null, count);
2185 mappings.add([modes.NORMAL], ["<C-d>", "<scroll-down>"],
2186 "Scroll window downwards in the buffer",
2187 function ({ count }) { buffer._scrollByScrollSize(count, true); },
2190 mappings.add([modes.NORMAL], ["<C-u>", "<scroll-up>"],
2191 "Scroll window upwards in the buffer",
2192 function ({ count }) { buffer._scrollByScrollSize(count, false); },
2195 mappings.add([modes.NORMAL], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-up-page>"],
2196 "Scroll up a full page",
2197 function ({ count }) { buffer.scrollVertical("pages", -Math.max(count, 1)); },
2200 mappings.add([modes.NORMAL], ["<Space>"],
2201 "Scroll down a full page",
2202 function ({ count }) {
2203 if (isinstance((services.focus.focusedWindow || buffer.win).document.activeElement,
2204 [Ci.nsIDOMHTMLInputElement,
2205 Ci.nsIDOMHTMLButtonElement,
2206 Ci.nsIDOMXULButtonElement]))
2209 buffer.scrollVertical("pages", Math.max(count, 1));
2213 mappings.add([modes.NORMAL], ["<C-f>", "<PageDown>", "<scroll-down-page>"],
2214 "Scroll down a full page",
2215 function ({ count }) { buffer.scrollVertical("pages", Math.max(count, 1)); },
2218 mappings.add([modes.NORMAL], ["]f", "<previous-frame>"],
2220 function ({ count }) { buffer.shiftFrameFocus(Math.max(count, 1)); },
2223 mappings.add([modes.NORMAL], ["[f", "<next-frame>"],
2224 "Focus previous frame",
2225 function ({ count }) { buffer.shiftFrameFocus(-Math.max(count, 1)); },
2228 mappings.add([modes.NORMAL], ["["],
2229 "Jump to the previous element as defined by 'jumptags'",
2230 function ({ arg, count }) { buffer.findJump(arg, count, true); },
2231 { arg: true, count: true });
2233 mappings.add([modes.NORMAL], ["g]"],
2234 "Jump to the next off-screen element as defined by 'jumptags'",
2235 function ({ arg, count }) { buffer.findJump(arg, count, false, true); },
2236 { arg: true, count: true });
2238 mappings.add([modes.NORMAL], ["]"],
2239 "Jump to the next element as defined by 'jumptags'",
2240 function ({ arg, count }) { buffer.findJump(arg, count, false); },
2241 { arg: true, count: true });
2243 mappings.add([modes.NORMAL], ["{"],
2244 "Jump to the previous paragraph",
2245 function ({ count }) { buffer.findJump("p", count, true); },
2248 mappings.add([modes.NORMAL], ["}"],
2249 "Jump to the next paragraph",
2250 function ({ count }) { buffer.findJump("p", count, false); },
2253 mappings.add([modes.NORMAL], ["]]", "<next-page>"],
2254 "Follow the link labeled 'next' or '>' if it exists",
2255 function ({ count }) {
2256 buffer.findLink("next", options["nextpattern"], (count || 1) - 1, true);
2260 mappings.add([modes.NORMAL], ["[[", "<previous-page>"],
2261 "Follow the link labeled 'prev', 'previous' or '<' if it exists",
2262 function ({ count }) {
2263 buffer.findLink("prev", options["previouspattern"], (count || 1) - 1, true);
2267 mappings.add([modes.NORMAL], ["gf", "<view-source>"],
2268 "Toggle between rendered and source view",
2269 function () { buffer.viewSource(null, false); });
2271 mappings.add([modes.NORMAL], ["gF", "<view-source-externally>"],
2272 "View source with an external editor",
2273 function () { buffer.viewSource(null, true); });
2275 mappings.add([modes.NORMAL], ["gi", "<focus-input>"],
2276 "Focus last used input field",
2277 function ({ count }) {
2278 let elem = buffer.lastInputField;
2280 if (count >= 1 || !elem || !events.isContentNode(elem)) {
2281 let xpath = ["frame", "iframe", "input", "xul:textbox", "textarea[not(@disabled) and not(@readonly)]"];
2283 let frames = buffer.allFrames(null, true);
2285 let elements = array.flatten(frames.map(win => [m for (m in DOM.XPath(xpath, win.document))]))
2286 .filter(function (elem) {
2287 if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
2288 Ci.nsIDOMHTMLIFrameElement]))
2289 return Editor.getEditor(elem.contentWindow);
2293 if (elem[0].readOnly || elem[0].disabled || !DOM(elem).isEditable)
2296 let style = elem.style;
2297 let rect = elem.rect;
2298 return elem.isVisible &&
2299 (elem[0] instanceof Ci.nsIDOMXULTextBoxElement || style.MozUserFocus != "ignore") &&
2300 rect.width && rect.height;
2303 dactyl.assert(elements.length > 0);
2304 elem = elements[Math.constrain(count, 1, elements.length) - 1];
2306 buffer.focusElement(elem);
2307 DOM(elem).scrollIntoView();
2312 let url = dactyl.clipboardRead();
2313 dactyl.assert(url, _("error.clipboardEmpty"));
2315 let proto = /^([-\w]+):/.exec(url);
2316 if (proto && services.PROTOCOL + proto[1] in Cc && !RegExp(options["urlseparator"]).test(url))
2317 return url.replace(/\s+/g, "");
2321 mappings.add([modes.NORMAL], ["gP"],
2322 "Open (put) a URL based on the current clipboard contents in a new background buffer",
2324 dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB, background: true });
2327 mappings.add([modes.NORMAL], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
2328 "Open (put) a URL based on the current clipboard contents in the current buffer",
2333 mappings.add([modes.NORMAL], ["P", "<tab-open-clipboard-url>"],
2334 "Open (put) a URL based on the current clipboard contents in a new buffer",
2336 dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB });
2340 mappings.add([modes.NORMAL], ["r", "<reload>"],
2341 "Reload the current web page",
2342 function () { tabs.reload(tabs.getTab(), false); });
2344 mappings.add([modes.NORMAL], ["R", "<full-reload>"],
2345 "Reload while skipping the cache",
2346 function () { tabs.reload(tabs.getTab(), true); });
2349 mappings.add([modes.NORMAL], ["Y", "<yank-selection>"],
2350 "Copy selected text or current word",
2352 let sel = buffer.currentWord;
2354 editor.setRegister(null, sel, true);
2358 mappings.add([modes.NORMAL], ["zi", "+", "<text-zoom-in>"],
2359 "Enlarge text zoom of current web page",
2360 function ({ count }) { buffer.zoomIn(Math.max(count, 1), false); },
2363 mappings.add([modes.NORMAL], ["zm", "<text-zoom-more>"],
2364 "Enlarge text zoom of current web page by a larger amount",
2365 function ({ count }) { buffer.zoomIn(Math.max(count, 1) * 3, false); },
2368 mappings.add([modes.NORMAL], ["zo", "-", "<text-zoom-out>"],
2369 "Reduce text zoom of current web page",
2370 function ({ count }) { buffer.zoomOut(Math.max(count, 1), false); },
2373 mappings.add([modes.NORMAL], ["zr", "<text-zoom-reduce>"],
2374 "Reduce text zoom of current web page by a larger amount",
2375 function ({ count }) { buffer.zoomOut(Math.max(count, 1) * 3, false); },
2378 mappings.add([modes.NORMAL], ["zz", "<text-zoom>"],
2379 "Set text zoom value of current web page",
2380 function ({ count }) { buffer.setZoom(count > 1 ? count : 100, false); },
2383 mappings.add([modes.NORMAL], ["ZI", "zI", "<full-zoom-in>"],
2384 "Enlarge full zoom of current web page",
2385 function ({ count }) { buffer.zoomIn(Math.max(count, 1), true); },
2388 mappings.add([modes.NORMAL], ["ZM", "zM", "<full-zoom-more>"],
2389 "Enlarge full zoom of current web page by a larger amount",
2390 function ({ count }) { buffer.zoomIn(Math.max(count, 1) * 3, true); },
2393 mappings.add([modes.NORMAL], ["ZO", "zO", "<full-zoom-out>"],
2394 "Reduce full zoom of current web page",
2395 function ({ count }) { buffer.zoomOut(Math.max(count, 1), true); },
2398 mappings.add([modes.NORMAL], ["ZR", "zR", "<full-zoom-reduce>"],
2399 "Reduce full zoom of current web page by a larger amount",
2400 function ({ count }) { buffer.zoomOut(Math.max(count, 1) * 3, true); },
2403 mappings.add([modes.NORMAL], ["zZ", "<full-zoom>"],
2404 "Set full zoom value of current web page",
2405 function ({ count }) { buffer.setZoom(count > 1 ? count : 100, true); },
2409 mappings.add([modes.NORMAL], ["<C-g>", "<page-info>"],
2410 "Print the current file name",
2411 function () { buffer.showPageInfo(false); });
2413 mappings.add([modes.NORMAL], ["g<C-g>", "<more-page-info>"],
2414 "Print file information",
2415 function () { buffer.showPageInfo(true); });
2417 options: function initOptions(dactyl, modules, window) {
2418 let { Option, buffer, completion, config, options } = modules;
2420 options.add(["encoding", "enc"],
2421 "The current buffer's character encoding",
2424 scope: Option.SCOPE_LOCAL,
2425 getter: function () buffer.docShell.QueryInterface(Ci.nsIDocCharset).charset,
2426 setter: function (val) {
2427 if (options["encoding"] == val)
2430 // Stolen from browser.jar/content/browser/browser.js, more or less.
2432 buffer.docShell.QueryInterface(Ci.nsIDocCharset).charset = val;
2433 window.PlacesUtils.history.setCharsetForURI(buffer.uri, val);
2434 buffer.docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
2436 catch (e) { dactyl.reportError(e); }
2439 completer: function (context) completion.charset(context)
2442 options.add(["iskeyword", "isk"],
2443 "Regular expression defining which characters constitute words",
2444 "string", '[^\\s.,!?:;/"\'^$%&?()[\\]{}<>#*+|=~_-]',
2446 setter: function (value) {
2447 this.regexp = util.regexp(value);
2450 validator: function (value) RegExp(value)
2453 options.add(["jumptags", "jt"],
2454 "XPath or CSS selector strings of jumpable elements for extended hint modes",
2456 "p": "p,table,ul,ol,blockquote",
2457 "h": "h1,h2,h3,h4,h5,h6"
2461 setter: function (vals) {
2462 for (let [k, v] in Iterator(vals))
2463 vals[k] = update(new String(v), { matcher: DOM.compileMatcher(Option.splitList(v)) });
2466 validator: function (value) DOM.validateMatcher.call(this, value)
2467 && Object.keys(value).every(v => v.length == 1)
2470 options.add(["linenumbers", "ln"],
2471 "Patterns used to determine line numbers used by G",
2473 // Make sure to update the docs when you change this.
2474 "view-source:*": 'body,[id^=line]',
2475 "code.google.com": '#nums [id^="nums_table"] a[href^="#"]',
2476 "github.com": '.line_numbers>*',
2477 "mxr.mozilla.org": 'a.l',
2478 "pastebin.com": '#code_frame>div>ol>li',
2479 "addons.mozilla.org": '.gutter>.line>a',
2480 "bugzilla.mozilla.org": ".bz_comment:not(.bz_first_comment):not(.ih_history)",
2481 "*": '/* Hgweb/Gitweb */ .completecodeline a.codeline, a.linenr'
2484 getLine: function getLine(doc, line) {
2485 let uri = util.newURI(doc.documentURI);
2486 for (let filter in values(this.value))
2487 if (filter(uri, doc)) {
2488 if (/^func:/.test(filter.result))
2489 var res = dactyl.userEval("(" + Option.dequote(filter.result.substr(5)) + ")")(doc, line);
2491 res = iter.find(filter.matcher(doc),
2492 elem => ((elem.nodeValue || elem.textContent).trim() == line &&
2493 DOM(elem).display != "none"))
2494 || iter.nth(filter.matcher(doc), util.identity, line - 1);
2504 setter: function (vals) {
2505 for (let value in values(vals))
2506 if (!/^func:/.test(value.result))
2507 value.matcher = DOM.compileMatcher(Option.splitList(value.result));
2511 validator: function validate(values) {
2512 return this.testValues(values, function (value) {
2513 if (/^func:/.test(value))
2514 return callable(dactyl.userEval("(" + Option.dequote(value.substr(5)) + ")"));
2516 return DOM.testMatcher(Option.dequote(value));
2521 options.add(["nextpattern"],
2522 "Patterns to use when guessing the next page in a document sequence",
2523 "regexplist", UTF8(/'^Next [>»]','^Next »','\bnext\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\bmore\b'/.source),
2524 { regexpFlags: "i" });
2526 options.add(["previouspattern"],
2527 "Patterns to use when guessing the previous page in a document sequence",
2528 "regexplist", UTF8(/'[<«] Prev$','« Prev$','\bprev(ious)?\b',^<$,^(<<|«)$,^(<|«),(<|«)$/.source),
2529 { regexpFlags: "i" });
2531 options.add(["pageinfo", "pa"],
2532 "Define which sections are shown by the :pageinfo command",
2533 "charlist", "gesfm",
2534 { get values() values(Buffer.pageInfo).toObject() });
2536 options.add(["scroll", "scr"],
2537 "Number of lines to scroll with <C-u> and <C-d> commands",
2539 { validator: function (value) value >= 0 });
2541 options.add(["showstatuslinks", "ssli"],
2542 "Where to show the destination of the link under the cursor",
2546 "": "Don't show link destinations",
2547 "status": "Show link destinations in the status line",
2548 "command": "Show link destinations in the command line"
2552 options.add(["scrolltime", "sct"],
2553 "The time, in milliseconds, in which to smooth scroll to a new position",
2556 options.add(["scrollsteps", "scs"],
2557 "The number of steps in which to smooth scroll to a new position",
2560 PREF: "general.smoothScroll",
2562 initValue: function () {},
2564 getter: function getter(value) !prefs.get(this.PREF) ? 1 : value,
2566 setter: function setter(value) {
2567 prefs.set(this.PREF, value > 1);
2572 validator: function (value) value > 0
2575 options.add(["usermode", "um"],
2576 "Show current website without styling defined by the author",
2579 setter: function (value) buffer.contentViewer.authorStyleDisabled = value,
2580 getter: function () buffer.contentViewer.authorStyleDisabled
2583 options.add(["yankshort", "ys"],
2584 "Yank the canonical short URL of a web page where provided",
2585 "sitelist", ["youtube.com", "bugzilla.mozilla.org"]);
2589 Buffer.addPageInfoSection("e", "Search Engines", function (verbose) {
2593 for (let { document: doc } in values(this.allFrames())) {
2594 let engines = DOM("link[href][rel=search][type='application/opensearchdescription+xml']", doc);
2595 nEngines += engines.length;
2598 for (let link in engines)
2599 yield [link.title || /*L*/ "Engine " + n++,
2600 ["a", { href: link.href, highlight: "URL",
2601 onclick: "if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }" },
2605 if (!verbose && nEngines)
2606 yield nEngines + /*L*/" engine" + (nEngines > 1 ? "s" : "");
2609 Buffer.addPageInfoSection("f", "Feeds", function (verbose) {
2611 "application/rss+xml": "RSS",
2612 "application/atom+xml": "Atom",
2614 "application/xml": "XML",
2615 "application/rdf+xml": "XML"
2618 function isValidFeed(data, principal, isFeed) {
2619 if (!data || !principal)
2623 var type = data.type && data.type.toLowerCase();
2624 type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
2626 isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
2627 // really slimy: general XML types with magic letters in the title
2628 type in feedTypes && /\brss\b/i.test(data.title);
2633 services.security.checkLoadURIStrWithPrincipal(principal, data.href,
2634 services.security.DISALLOW_INHERIT_PRINCIPAL);
2648 for (let [i, win] in Iterator(this.allFrames())) {
2649 let doc = win.document;
2651 for (let link in DOM("link[href][rel=feed], link[href][rel=alternate][type]", doc)) {
2652 let rel = link.rel.toLowerCase();
2653 let feed = { title: link.title, href: link.href, type: link.type || "" };
2654 if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) {
2656 let type = feedTypes[feed.type] || "RSS";
2658 yield [feed.title, [template.highlightURL(feed.href, true),
2659 ["span", { class: "extra-info" }, " (" + type + ")"]]];
2665 if (!verbose && nFeed)
2666 yield nFeed + /*L*/" feed" + (nFeed > 1 ? "s" : "");
2669 Buffer.addPageInfoSection("g", "General Info", function (verbose) {
2670 let doc = this.focusedFrame.document;
2673 const ACCESS_READ = Ci.nsICache.ACCESS_READ;
2674 let cacheKey = doc.documentURI;
2676 for (let proto in array.iterValues(["HTTP", "FTP"])) {
2678 var cacheEntryDescriptor = services.cache.createSession(proto, 0, true)
2679 .openCacheEntry(cacheKey, ACCESS_READ, false);
2685 let pageSize = []; // [0] bytes; [1] kbytes
2686 if (cacheEntryDescriptor) {
2687 pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
2688 pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
2689 if (pageSize[1] == pageSize[0])
2690 pageSize.length = 1; // don't output "xx Bytes" twice
2693 let lastModVerbose = new Date(doc.lastModified).toLocaleString();
2694 let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");
2696 if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
2697 lastModVerbose = lastMod = null;
2701 yield (pageSize[1] || pageSize[0]) + /*L*/" bytes";
2706 yield ["Title", doc.title];
2707 yield ["URL", template.highlightURL(doc.location.href, true)];
2709 let { shortURL } = this;
2711 yield ["Short URL", template.highlightURL(shortURL, true)];
2713 let ref = "referrer" in doc && doc.referrer;
2715 yield ["Referrer", template.highlightURL(ref, true)];
2718 yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
2721 yield ["Mime-Type", doc.contentType];
2722 yield ["Encoding", doc.characterSet];
2723 yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
2725 yield ["Last Modified", lastModVerbose];
2728 Buffer.addPageInfoSection("m", "Meta Tags", function (verbose) {
2732 // get meta tag data, sort and put into pageMeta[]
2733 let metaNodes = this.focusedFrame.document.getElementsByTagName("meta");
2735 return Array.map(metaNodes, node => [(node.name || node.httpEquiv),
2736 template.highlightURL(node.content)])
2737 .sort((a, b) => util.compareIgnoreCase(a[0], b[0]));
2740 Buffer.addPageInfoSection("s", "Security", function (verbose) {
2741 let { statusline } = this.modules;
2743 let identity = this.topWindow.gIdentityHandler;
2745 if (!verbose || !identity)
2748 // Modified from Firefox
2749 function location(data) array.compact([
2750 data.city, data.state, data.country
2753 switch (statusline.security) {
2756 var data = identity.getIdentityData();
2758 yield ["Host", identity.getEffectiveHost()];
2760 if (statusline.security === "extended")
2761 yield ["Owner", data.subjectOrg];
2763 yield ["Owner", _("pageinfo.s.ownerUnverified", data.subjectOrg)];
2765 if (location(data).length)
2766 yield ["Location", location(data)];
2768 yield ["Verified by", data.caOrg];
2770 let { host, port } = identity._lastUri;
2774 if (identity._overrideService.hasMatchingOverride(host, port, data.cert, {}, {}))
2775 yield ["User exception", /*L*/"true"];
2780 // internal navigation doesn't currently update link[rel='shortlink']
2781 Buffer.addURIShortener("youtube.com", (uri, doc) => {
2782 let video = array.toObject(uri.query.split("&")
2783 .map(p => p.split("="))).v;
2784 return video ? util.newURI("http://youtu.be/" + video) : null;
2787 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
2791 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: