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-2011 by 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.
12 * A class to manage the primary web content buffer. The name comes
13 * from Vim's term, 'buffer', which signifies instances of open
17 var Buffer = Module("buffer", {
18 init: function init() {
19 this.evaluateXPath = util.evaluateXPath;
22 this.addPageInfoSection("f", "Feeds", function (verbose) {
24 "application/rss+xml": "RSS",
25 "application/atom+xml": "Atom",
27 "application/xml": "XML",
28 "application/rdf+xml": "XML"
31 function isValidFeed(data, principal, isFeed) {
32 if (!data || !principal)
36 var type = data.type && data.type.toLowerCase();
37 type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
39 isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
40 // really slimy: general XML types with magic letters in the title
41 type in feedTypes && /\brss\b/i.test(data.title);
46 window.urlSecurityCheck(data.href, principal,
47 Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
61 for (let [i, win] in Iterator(buffer.allFrames())) {
62 let doc = win.document;
64 for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc)) {
65 let rel = link.rel.toLowerCase();
66 let feed = { title: link.title, href: link.href, type: link.type || "" };
67 if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) {
69 let type = feedTypes[feed.type] || "RSS";
71 yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info"> ({type})</span>];
77 if (!verbose && nFeed)
78 yield nFeed + " feed" + (nFeed > 1 ? "s" : "");
81 this.addPageInfoSection("g", "General Info", function (verbose) {
82 let doc = buffer.focusedFrame.document;
85 const ACCESS_READ = Ci.nsICache.ACCESS_READ;
86 let cacheKey = doc.documentURI;
88 for (let proto in array.iterValues(["HTTP", "FTP"])) {
90 var cacheEntryDescriptor = services.cache.createSession(proto, 0, true)
91 .openCacheEntry(cacheKey, ACCESS_READ, false);
97 let pageSize = []; // [0] bytes; [1] kbytes
98 if (cacheEntryDescriptor) {
99 pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
100 pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
101 if (pageSize[1] == pageSize[0])
102 pageSize.length = 1; // don't output "xx Bytes" twice
105 let lastModVerbose = new Date(doc.lastModified).toLocaleString();
106 let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");
108 if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
109 lastModVerbose = lastMod = null;
113 yield (pageSize[1] || pageSize[0]) + " bytes";
118 yield ["Title", doc.title];
119 yield ["URL", template.highlightURL(doc.location.href, true)];
121 let ref = "referrer" in doc && doc.referrer;
123 yield ["Referrer", template.highlightURL(ref, true)];
126 yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
129 yield ["Mime-Type", doc.contentType];
130 yield ["Encoding", doc.characterSet];
131 yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
133 yield ["Last Modified", lastModVerbose];
136 this.addPageInfoSection("m", "Meta Tags", function (verbose) {
137 // get meta tag data, sort and put into pageMeta[]
138 let metaNodes = buffer.focusedFrame.document.getElementsByTagName("meta");
140 return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
141 .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
144 dactyl.commands["buffer.viewSource"] = function (event) {
145 let elem = event.originalTarget;
146 buffer.viewSource([elem.getAttribute("href"), Number(elem.getAttribute("line"))]);
150 // called when the active document is scrolled
151 _updateBufferPosition: function _updateBufferPosition() {
152 statusline.updateBufferPosition();
153 commandline.clear(true);
157 * @property {Array} The alternative style sheets for the current
158 * buffer. Only returns style sheets for the 'screen' media type.
160 get alternateStyleSheets() {
161 let stylesheets = window.getAllStyleSheets(this.focusedFrame);
163 return stylesheets.filter(
164 function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title)
168 climbUrlPath: function climbUrlPath(count) {
169 let url = buffer.documentURI.clone();
170 dactyl.assert(url instanceof Ci.nsIURL);
172 while (count-- && url.path != "/")
173 url.path = url.path.replace(/[^\/]+\/*$/, "");
175 dactyl.assert(!url.equals(buffer.documentURI));
176 dactyl.open(url.spec);
179 incrementURL: function incrementURL(count) {
180 let matches = buffer.uri.spec.match(/(.*?)(\d+)(\D*)$/);
181 dactyl.assert(matches);
182 let oldNum = matches[2];
184 // disallow negative numbers as trailing numbers are often proceeded by hyphens
185 let newNum = String(Math.max(parseInt(oldNum, 10) + count, 0));
186 if (/^0/.test(oldNum))
187 while (newNum.length < oldNum.length)
188 newNum = "0" + newNum;
191 dactyl.open(matches.slice(1).join(""));
195 * @property {Object} A map of page info sections to their
196 * content generating functions.
201 * @property {number} True when the buffer is fully loaded.
203 get loaded() Math.min.apply(null,
205 .map(function (frame) ["loading", "interactive", "complete"]
206 .indexOf(frame.document.readyState))),
209 * @property {Object} The local state store for the currently selected
213 if (!content.document.dactylStore)
214 content.document.dactylStore = {};
215 return content.document.dactylStore;
219 * @property {Node} The last focused input field in the buffer. Used
220 * by the "gi" key binding.
222 get lastInputField() {
223 let field = this.localStore.lastInputField && this.localStore.lastInputField.get();
224 let doc = field && field.ownerDocument;
225 let win = doc && doc.defaultView;
226 return win && doc === win.document ? field : null;
228 set lastInputField(value) { this.localStore.lastInputField = value && Cu.getWeakReference(value); },
231 * @property {nsIURI} The current top-level document.
233 get doc() window.content.document,
236 * @property {nsIURI} The current top-level document's URI.
238 get uri() util.newURI(content.location.href),
241 * @property {nsIURI} The current top-level document's URI, sans any
242 * fragment identifier.
244 get documentURI() let (doc = content.document) doc.documentURIObject || util.newURI(doc.documentURI),
247 * @property {string} The current top-level document's URL.
249 get URL() update(new String(content.location.href), util.newURI(content.location.href)),
252 * @property {number} The buffer's height in pixels.
254 get pageHeight() content.innerHeight,
257 * @property {number} The current browser's zoom level, as a
258 * percentage with 100 as 'normal'.
260 get zoomLevel() config.browser.markupDocumentViewer[this.fullZoom ? "fullZoom" : "textZoom"] * 100,
261 set zoomLevel(value) { this.setZoom(value, this.fullZoom); },
264 * @property {boolean} Whether the current browser is using full
265 * zoom, as opposed to text zoom.
267 get fullZoom() ZoomManager.useFullZoom,
268 set fullZoom(value) { this.setZoom(this.zoomLevel, value); },
271 * @property {string} The current document's title.
273 get title() content.document.title,
276 * @property {number} The buffer's horizontal scroll percentile.
278 get scrollXPercent() {
279 let elem = this.findScrollable(0, true);
280 if (elem.scrollWidth - elem.clientWidth === 0)
282 return elem.scrollLeft * 100 / (elem.scrollWidth - elem.clientWidth);
286 * @property {number} The buffer's vertical scroll percentile.
288 get scrollYPercent() {
289 let elem = this.findScrollable(0, false);
290 if (elem.scrollHeight - elem.clientHeight === 0)
292 return elem.scrollTop * 100 / (elem.scrollHeight - elem.clientHeight);
296 * Adds a new section to the page information output.
298 * @param {string} option The section's value in 'pageinfo'.
299 * @param {string} title The heading for this section's
301 * @param {function} func The function to generate this
304 addPageInfoSection: function addPageInfoSection(option, title, func) {
305 this.pageInfo[option] = Buffer.PageInfo(option, title, func);
309 * Returns a list of all frames in the given window or current buffer.
311 allFrames: function allFrames(win, focusedFirst) {
313 (function rec(frame) {
314 if (frame.document.body instanceof HTMLBodyElement)
316 Array.forEach(frame.frames, rec);
319 return frames.filter(function (f) f === buffer.focusedFrame).concat(
320 frames.filter(function (f) f !== buffer.focusedFrame));
325 * @property {Window} Returns the currently focused frame.
328 let frame = this.localStore.focusedFrame;
329 return frame && frame.get() || content;
331 set focusedFrame(frame) {
332 this.localStore.focusedFrame = Cu.getWeakReference(frame);
336 * Returns the currently selected word. If the selection is
337 * null, it tries to guess the word that the caret is
342 get currentWord() Buffer.currentWord(this.focusedFrame),
343 getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() this.currentWord),
346 * Returns true if a scripts are allowed to focus the given input
347 * element or input elements in the given window.
349 * @param {Node|Window}
352 focusAllowed: function focusAllowed(elem) {
353 if (elem instanceof Window && !Editor.getEditor(elem))
355 let doc = elem.ownerDocument || elem.document || elem;
356 return !options["strictfocus"] || doc.dactylFocusAllowed;
360 * Focuses the given element. In contrast to a simple
361 * elem.focus() call, this function works for iframes and
364 * @param {Node} elem The element to focus.
366 focusElement: function focusElement(elem) {
367 let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
368 win.document.dactylFocusAllowed = true;
370 if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
371 elem = elem.contentWindow;
373 elem.document.dactylFocusAllowed = true;
375 if (elem instanceof HTMLInputElement && elem.type == "file") {
376 Buffer.openUploadPrompt(elem);
377 this.lastInputField = elem;
380 if (isinstance(elem, [HTMLInputElement, XULTextBoxElement]))
381 var flags = services.focus.FLAG_BYMOUSE;
383 flags = services.focus.FLAG_SHOWRING;
384 dactyl.focus(elem, flags);
386 if (elem instanceof Window) {
387 let sel = elem.getSelection();
388 if (sel && !sel.rangeCount)
389 sel.addRange(RangeFind.endpoint(
390 RangeFind.nodeRange(elem.document.body || elem.document.documentElement),
394 let range = RangeFind.nodeRange(elem);
395 let sel = (elem.ownerDocument || elem).defaultView.getSelection();
396 if (!sel.rangeCount || !RangeFind.intersects(range, sel.getRangeAt(0))) {
397 range.collapse(true);
398 sel.removeAllRanges();
404 if (elem instanceof HTMLAreaElement) {
406 let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);
408 events.dispatch(elem, events.create(elem.ownerDocument, "mouseover", { screenX: x, screenY: y }));
416 * Find the counth last link on a page matching one of the given
417 * regular expressions, or with a @rel or @rev attribute matching
418 * the given relation. Each frame is searched beginning with the
419 * last link and progressing to the first, once checking for
420 * matching @rel or @rev properties, and then once for each given
421 * regular expression. The first match is returned. All frames of
422 * the page are searched, beginning with the currently focused.
424 * If follow is true, the link is followed.
426 * @param {string} rel The relationship to look for.
427 * @param {[RegExp]} regexps The regular expressions to search for.
428 * @param {number} count The nth matching link to follow.
429 * @param {bool} follow Whether to follow the matching link.
430 * @param {string} path The CSS to use for the search. @optional
432 followDocumentRelationship: deprecated("buffer.findLink",
433 function followDocumentRelationship(rel) {
434 this.findLink(rel, options[rel + "pattern"], 0, true);
436 findLink: function findLink(rel, regexps, count, follow, path) {
437 let selector = path || options.get("hinttags").stringDefaultValue;
439 function followFrame(frame) {
440 function iter(elems) {
441 for (let i = 0; i < elems.length; i++)
442 if (elems[i].rel.toLowerCase() === rel || elems[i].rev.toLowerCase() === rel)
446 let elems = frame.document.getElementsByTagName("link");
447 for (let elem in iter(elems))
450 elems = frame.document.getElementsByTagName("a");
451 for (let elem in iter(elems))
454 let res = frame.document.querySelectorAll(selector);
455 for (let regexp in values(regexps)) {
456 for (let i in util.range(res.length, 0, -1)) {
458 if (regexp.test(elem.textContent) === regexp.result || regexp.test(elem.title) === regexp.result ||
459 Array.some(elem.childNodes, function (child) regexp.test(child.alt) === regexp.result))
465 for (let frame in values(this.allFrames(null, true)))
466 for (let elem in followFrame(frame))
469 this.followLink(elem, dactyl.CURRENT_TAB);
478 * Fakes a click on a link.
480 * @param {Node} elem The element to click.
481 * @param {number} where Where to open the link. See
482 * {@link dactyl.open}.
484 followLink: function followLink(elem, where) {
485 let doc = elem.ownerDocument;
486 let view = doc.defaultView;
487 let { left: offsetX, top: offsetY } = elem.getBoundingClientRect();
489 if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
490 return this.focusElement(elem);
491 if (isinstance(elem, HTMLLinkElement))
492 return dactyl.open(elem.href, where);
494 if (elem instanceof HTMLAreaElement) { // for imagemap
495 let coords = elem.getAttribute("coords").split(",");
496 offsetX = Number(coords[0]) + 1;
497 offsetY = Number(coords[1]) + 1;
499 else if (elem instanceof HTMLInputElement && elem.type == "file") {
500 Buffer.openUploadPrompt(elem);
504 let ctrlKey = false, shiftKey = false;
507 case dactyl.NEW_BACKGROUND_TAB:
509 shiftKey = (where != dactyl.NEW_BACKGROUND_TAB);
511 case dactyl.NEW_WINDOW:
514 case dactyl.CURRENT_TAB:
518 this.focusElement(elem);
520 prefs.withContext(function () {
521 prefs.set("browser.tabs.loadInBackground", true);
522 ["mousedown", "mouseup", "click"].slice(0, util.haveGecko("2b") ? 2 : 3)
523 .forEach(function (event) {
524 events.dispatch(elem, events.create(doc, event, {
525 screenX: offsetX, screenY: offsetY,
526 ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
533 * @property {nsISelectionController} The current document's selection
536 get selectionController() config.browser.docShell
537 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
538 .QueryInterface(Ci.nsISelectionController),
541 * Opens the appropriate context menu for *elem*.
543 * @param {Node} elem The context element.
545 openContextMenu: function openContextMenu(elem) {
546 document.popupNode = elem;
547 let menu = document.getElementById("contentAreaContextMenu");
548 menu.showPopup(elem, -1, -1, "context", "bottomleft", "topleft");
552 * Saves a page link to disk.
554 * @param {HTMLAnchorElement} elem The page link to save.
556 saveLink: function saveLink(elem) {
557 let doc = elem.ownerDocument;
558 let uri = util.newURI(elem.href || elem.src, null, util.newURI(elem.baseURI));
559 let referrer = util.newURI(doc.documentURI, doc.characterSet);
562 window.urlSecurityCheck(uri.spec, doc.nodePrincipal);
564 io.CommandFileMode("Save link: ", {
565 onSubmit: function (path) {
566 let file = io.File(path);
567 if (file.exists() && file.isDirectory())
568 file.append(Buffer.getDefaultNames(elem)[0][0]);
572 file.create(File.NORMAL_FILE_TYPE, octal(644));
575 util.assert(false, _("save.invalidDestination", e.name));
578 buffer.saveURI(uri, file);
581 completer: function (context) completion.savePage(context, elem)
590 * Saves the contents of a URI to disk.
592 * @param {nsIURI} uri The URI to save
593 * @param {nsIFile} file The file into which to write the result.
595 saveURI: function saveURI(uri, file, callback, self) {
596 var persist = services.Persist();
597 persist.persistFlags = persist.PERSIST_FLAGS_FROM_CACHE
598 | persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
600 let downloadListener = new window.DownloadListener(window,
601 services.Transfer(uri, services.io.newFileURI(file), "",
602 null, null, null, persist));
604 persist.progressListener = update(Object.create(downloadListener), {
605 onStateChange: function onStateChange(progress, request, flag, status) {
606 if (callback && (flag & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
607 dactyl.trapErrors(callback, self, uri, file, progress, request, flag, status);
609 return onStateChange.superapply(this, arguments);
613 persist.saveURI(uri, null, null, null, null, file);
617 * Scrolls the currently active element horizontally. See
618 * {@link Buffer.scrollHorizontal} for parameters.
620 scrollHorizontal: function scrollHorizontal(increment, number)
621 Buffer.scrollHorizontal(this.findScrollable(number, true), increment, number),
624 * Scrolls the currently active element vertically. See
625 * {@link Buffer.scrollVertical} for parameters.
627 scrollVertical: function scrollVertical(increment, number)
628 Buffer.scrollVertical(this.findScrollable(number, false), increment, number),
631 * Scrolls the currently active element to the given horizontal and
632 * vertical percentages. See {@link Buffer.scrollToPercent} for
635 scrollToPercent: function scrollToPercent(horizontal, vertical)
636 Buffer.scrollToPercent(this.findScrollable(0, vertical == null), horizontal, vertical),
638 _scrollByScrollSize: function _scrollByScrollSize(count, direction) {
640 options["scroll"] = count;
641 this.scrollByScrollSize(direction);
645 * Scrolls the buffer vertically 'scroll' lines.
647 * @param {boolean} direction The direction to scroll. If true then
648 * scroll up and if false scroll down.
649 * @param {number} count The multiple of 'scroll' lines to scroll.
652 scrollByScrollSize: function scrollByScrollSize(direction, count) {
653 direction = direction ? 1 : -1;
656 if (options["scroll"] > 0)
657 this.scrollVertical("lines", options["scroll"] * direction);
659 this.scrollVertical("pages", direction / 2);
663 * Find the best candidate scrollable element for the given
664 * direction and orientation.
666 * @param {number} dir The direction in which the element must be
667 * able to scroll. Negative numbers represent up or left, while
668 * positive numbers represent down or right.
669 * @param {boolean} horizontal If true, look for horizontally
670 * scrollable elements, otherwise look for vertically scrollable
673 findScrollable: function findScrollable(dir, horizontal) {
674 function find(elem) {
675 while (!(elem instanceof Element) && elem.parentNode)
676 elem = elem.parentNode;
677 for (; elem && elem.parentNode instanceof Element; elem = elem.parentNode)
678 if (Buffer.isScrollable(elem, dir, horizontal))
684 var elem = this.focusedFrame.document.activeElement;
685 if (elem == elem.ownerDocument.body)
691 var sel = this.focusedFrame.getSelection();
694 if (!elem && sel && sel.rangeCount)
695 elem = sel.getRangeAt(0).startContainer;
699 if (!(elem instanceof Element)) {
700 let doc = this.findScrollableWindow().document;
701 elem = find(doc.body || doc.getElementsByTagName("body")[0] ||
702 doc.documentElement);
704 let doc = this.focusedFrame.document;
705 return elem || doc.body || doc.documentElement;
709 * Find the best candidate scrollable frame in the current buffer.
711 findScrollableWindow: function findScrollableWindow() {
712 win = window.document.commandDispatcher.focusedWindow;
713 if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
716 let win = this.focusedFrame;
717 if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
721 if (win.scrollMaxX > 0 || win.scrollMaxY > 0)
724 for (let frame in array.iterValues(win.frames))
725 if (frame.scrollMaxX > 0 || frame.scrollMaxY > 0)
731 // TODO: allow callback for filtering out unwanted frames? User defined?
733 * Shifts the focus to another frame within the buffer. Each buffer
734 * contains at least one frame.
736 * @param {number} count The number of frames to skip through. A negative
737 * count skips backwards.
739 shiftFrameFocus: function shiftFrameFocus(count) {
740 if (!(content.document instanceof HTMLDocument))
743 let frames = this.allFrames();
745 if (frames.length == 0) // currently top is always included
748 // remove all hidden frames
749 frames = frames.filter(function (frame) !(frame.document.body instanceof HTMLFrameSetElement))
750 .filter(function (frame) !frame.frameElement ||
751 let (rect = frame.frameElement.getBoundingClientRect())
752 rect.width && rect.height);
754 // find the currently focused frame index
755 let current = Math.max(0, frames.indexOf(this.focusedFrame));
757 // calculate the next frame to focus
758 let next = current + count;
759 if (next < 0 || next >= frames.length)
761 next = Math.constrain(next, 0, frames.length - 1);
763 // focus next frame and scroll into view
764 dactyl.focus(frames[next]);
765 if (frames[next] != content)
766 frames[next].frameElement.scrollIntoView(false);
768 // add the frame indicator
769 let doc = frames[next].document;
770 let indicator = util.xmlToDom(<div highlight="FrameIndicator"/>, doc);
771 (doc.body || doc.documentElement || doc).appendChild(indicator);
773 util.timeout(function () { doc.body.removeChild(indicator); }, 500);
776 //doc.body.setAttributeNS(NS.uri, "activeframe", "true");
777 //util.timeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500);
780 // similar to pageInfo
781 // TODO: print more useful information, just like the DOM inspector
783 * Displays information about the specified element.
785 * @param {Node} elem The element to query.
787 showElementInfo: function showElementInfo(elem) {
788 dactyl.echo(<>Element:<br/>{util.objectToString(elem, true)}</>, commandline.FORCE_MULTILINE);
792 * Displays information about the current buffer.
794 * @param {boolean} verbose Display more verbose information.
795 * @param {string} sections A string limiting the displayed sections.
796 * @default The value of 'pageinfo'.
798 showPageInfo: function showPageInfo(verbose, sections) {
799 // Ctrl-g single line output
801 let file = content.location.pathname.split("/").pop() || "[No Name]";
802 let title = content.document.title || "[No Title]";
804 let info = template.map("gf",
805 function (opt) template.map(buffer.pageInfo[opt].action(), util.identity, ", "),
808 if (bookmarkcache.isBookmarked(this.URL))
809 info += ", bookmarked";
811 let pageInfoText = <>{file.quote()} [{info}] {title}</>;
812 dactyl.echo(pageInfoText, commandline.FORCE_SINGLELINE);
816 let list = template.map(sections || options["pageinfo"], function (option) {
817 let { action, title } = buffer.pageInfo[option];
818 return template.table(title, action(true));
820 dactyl.echo(list, commandline.FORCE_MULTILINE);
824 * Stops loading and animations in the current content.
826 stop: function stop() {
830 config.browser.mCurrentBrowser.stop();
834 * Opens a viewer to inspect the source of the currently selected
837 viewSelectionSource: function viewSelectionSource() {
838 // copied (and tuned somewhat) from browser.jar -> nsContextMenu.js
839 let win = document.commandDispatcher.focusedWindow;
841 win = this.focusedFrame;
843 let charset = win ? "charset=" + win.document.characterSet : null;
845 window.openDialog("chrome://global/content/viewPartialSource.xul",
846 "_blank", "scrollbars,resizable,chrome,dialog=no",
847 null, charset, win.getSelection(), "selection");
851 * Opens a viewer to inspect the source of the current buffer or the
852 * specified *url*. Either the default viewer or the configured external
855 * @param {string} url The URL of the source.
856 * @default The current buffer.
857 * @param {boolean} useExternalEditor View the source in the external editor.
859 viewSource: function viewSource(url, useExternalEditor) {
860 let doc = this.focusedFrame.document;
863 if (options.get("editor").has("line"))
864 this.viewSourceExternally(url[0] || doc, url[1]);
866 window.openDialog("chrome://global/content/viewSource.xul",
867 "_blank", "all,dialog=no",
868 url[0], null, null, url[1]);
871 if (useExternalEditor)
872 this.viewSourceExternally(url || doc);
874 url = url || doc.location.href;
875 const PREFIX = "view-source:";
876 if (url.indexOf(PREFIX) == 0)
877 url = url.substr(PREFIX.length);
881 let sh = history.session;
882 if (sh[sh.index].URI.spec == url)
883 window.getWebNavigation().gotoIndex(sh.index);
885 dactyl.open(url, { hide: true });
891 * Launches an editor to view the source of the given document. The
892 * contents of the document are saved to a temporary local file and
893 * removed when the editor returns. This function returns
896 * @param {Document} doc The document to view.
898 viewSourceExternally: Class("viewSourceExternally",
899 XPCOM([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), {
900 init: function init(doc, callback) {
901 this.callback = callable(callback) ? callback :
902 function (file, temp) {
903 editor.editFileExternally({ file: file.path, line: callback },
904 function () { temp && file.remove(false); });
908 let uri = isString(doc) ? util.newURI(doc) : util.newURI(doc.location.href);
911 return io.withTempFiles(function (temp) {
912 let encoder = services.HtmlEncoder();
913 encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
914 temp.write(encoder.encodeToString(), ">");
915 return this.callback(temp, true);
918 let file = util.getFile(uri);
920 this.callback(file, false);
922 this.file = io.createTempFile();
923 var persist = services.Persist();
924 persist.persistFlags = persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
925 persist.progressListener = this;
926 persist.saveURI(uri, null, null, null, null, this.file);
931 onStateChange: function onStateChange(progress, request, flag, status) {
932 if ((flag & this.STATE_STOP) && status == 0) {
934 var ok = this.callback(this.file, true);
938 this.file.remove(false);
946 * Increases the zoom level of the current buffer.
948 * @param {number} steps The number of zoom levels to jump.
949 * @param {boolean} fullZoom Whether to use full zoom or text zoom.
951 zoomIn: function zoomIn(steps, fullZoom) {
952 this.bumpZoomLevel(steps, fullZoom);
956 * Decreases the zoom level of the current buffer.
958 * @param {number} steps The number of zoom levels to jump.
959 * @param {boolean} fullZoom Whether to use full zoom or text zoom.
961 zoomOut: function zoomOut(steps, fullZoom) {
962 this.bumpZoomLevel(-steps, fullZoom);
966 * Adjusts the page zoom of the current buffer to the given absolute
969 * @param {number} value The new zoom value as a possibly fractional
970 * percentage of the page's natural size.
971 * @param {boolean} fullZoom If true, zoom all content of the page,
972 * including raster images. If false, zoom only text. If omitted,
973 * use the current zoom function. @optional
974 * @throws {FailedAssertion} if the given *value* is not within the
975 * closed range [Buffer.ZOOM_MIN, Buffer.ZOOM_MAX].
977 setZoom: function setZoom(value, fullZoom) {
978 dactyl.assert(value >= Buffer.ZOOM_MIN || value <= Buffer.ZOOM_MAX,
979 _("zoom.outOfRange", Buffer.ZOOM_MIN, Buffer.ZOOM_MAX));
981 if (fullZoom !== undefined)
982 ZoomManager.useFullZoom = fullZoom;
984 ZoomManager.zoom = value / 100;
986 catch (e if e == Cr.NS_ERROR_ILLEGAL_VALUE) {
987 return dactyl.echoerr(_("zoom.illegal"));
990 if ("FullZoom" in window)
991 FullZoom._applySettingToPref();
993 statusline.updateZoomLevel(value, ZoomManager.useFullZoom);
997 * Adjusts the page zoom of the current buffer relative to the
998 * current zoom level.
1000 * @param {number} steps The integral number of natural fractions by
1001 * which to adjust the current page zoom. If positive, the zoom
1002 * level is increased, if negative it is decreased.
1003 * @param {boolean} fullZoom If true, zoom all content of the page,
1004 * including raster images. If false, zoom only text. If omitted,
1005 * use the current zoom function. @optional
1006 * @throws {FailedAssertion} if the buffer's zoom level is already
1007 * at its extreme in the given direction.
1009 bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) {
1010 if (fullZoom === undefined)
1011 fullZoom = ZoomManager.useFullZoom;
1013 let values = ZoomManager.zoomValues;
1014 let cur = values.indexOf(ZoomManager.snap(ZoomManager.zoom));
1015 let i = Math.constrain(cur + steps, 0, values.length - 1);
1017 dactyl.assert(i != cur || fullZoom != ZoomManager.useFullZoom);
1019 this.setZoom(Math.round(values[i] * 100), fullZoom);
1022 getAllFrames: deprecated("buffer.allFrames", function getAllFrames() buffer.getAllFrames.apply(buffer, arguments)),
1023 scrollTop: deprecated("buffer.scrollToPercent", function scrollTop() buffer.scrollToPercent(null, 0)),
1024 scrollBottom: deprecated("buffer.scrollToPercent", function scrollBottom() buffer.scrollToPercent(null, 100)),
1025 scrollStart: deprecated("buffer.scrollToPercent", function scrollStart() buffer.scrollToPercent(0, null)),
1026 scrollEnd: deprecated("buffer.scrollToPercent", function scrollEnd() buffer.scrollToPercent(100, null)),
1027 scrollColumns: deprecated("buffer.scrollHorizontal", function scrollColumns(cols) buffer.scrollHorizontal("columns", cols)),
1028 scrollPages: deprecated("buffer.scrollHorizontal", function scrollPages(pages) buffer.scrollVertical("pages", pages)),
1029 scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) content.scrollTo(x, y)),
1030 textZoom: deprecated("buffer.zoomValue and buffer.fullZoom", function textZoom() config.browser.markupDocumentViewer.textZoom * 100)
1032 PageInfo: Struct("PageInfo", "name", "title", "action")
1035 ZOOM_MIN: Class.memoize(function () prefs.get("zoom.minPercent")),
1036 ZOOM_MAX: Class.memoize(function () prefs.get("zoom.maxPercent")),
1038 setZoom: deprecated("buffer.setZoom", function setZoom() buffer.setZoom.apply(buffer, arguments)),
1039 bumpZoomLevel: deprecated("buffer.bumpZoomLevel", function bumpZoomLevel() buffer.bumpZoomLevel.apply(buffer, arguments)),
1042 * Returns the currently selected word in *win*. If the selection is
1043 * null, it tries to guess the word that the caret is positioned in.
1047 currentWord: function currentWord(win) {
1048 let selection = win.getSelection();
1049 if (selection.rangeCount == 0)
1052 let range = selection.getRangeAt(0).cloneRange();
1053 if (range.collapsed) {
1054 let re = options.get("iskeyword").regexp;
1055 Editor.extendRange(range, true, re, true);
1056 Editor.extendRange(range, false, re, true);
1058 return util.domToString(range);
1061 getDefaultNames: function getDefaultNames(node) {
1062 let url = node.href || node.src || node.documentURI;
1063 let currExt = url.replace(/^.*?(?:\.([a-z0-9]+))?$/i, "$1").toLowerCase();
1065 if (isinstance(node, [Document, HTMLImageElement])) {
1066 let type = node.contentType || node.QueryInterface(Ci.nsIImageLoadingContent)
1067 .getRequest(0).mimeType;
1069 if (type === "text/plain")
1070 var ext = "." + (currExt || "txt");
1072 ext = "." + services.mime.getPrimaryExtension(type, currExt);
1075 ext = "." + currExt;
1078 let re = ext ? RegExp("(\\." + currExt + ")?$", "i") : /$/;
1082 names.push([node.title, "Page Name"]);
1085 names.push([node.alt, "Alternate Text"]);
1087 if (!isinstance(node, Document) && node.textContent)
1088 names.push([node.textContent, "Link Text"]);
1090 names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")), "File Name"]);
1092 return names.filter(function ([leaf, title]) leaf)
1093 .map(function ([leaf, title]) [leaf.replace(util.OS.illegalCharacters, encodeURIComponent)
1094 .replace(re, ext), title]);
1097 findScrollableWindow: deprecated("buffer.findScrollableWindow", function findScrollableWindow() buffer.findScrollableWindow.apply(buffer, arguments)),
1098 findScrollable: deprecated("buffer.findScrollable", function findScrollable() buffer.findScrollable.apply(buffer, arguments)),
1100 isScrollable: function isScrollable(elem, dir, horizontal) {
1101 let pos = "scrollTop", size = "clientHeight", max = "scrollHeight", layoutSize = "offsetHeight",
1102 overflow = "overflowX", border1 = "borderTopWidth", border2 = "borderBottomWidth";
1104 pos = "scrollLeft", size = "clientWidth", max = "scrollWidth", layoutSize = "offsetWidth",
1105 overflow = "overflowX", border1 = "borderLeftWidth", border2 = "borderRightWidth";
1107 let style = util.computedStyle(elem);
1108 let borderSize = Math.round(parseFloat(style[border1]) + parseFloat(style[border2]));
1109 let realSize = elem[size];
1110 // Stupid Gecko eccentricities. May fail for quirks mode documents.
1111 if (elem[size] + borderSize == elem[max] || elem[size] == 0) // Stupid, fallible heuristic.
1113 if (style[overflow] == "hidden")
1114 realSize += borderSize;
1115 return dir < 0 && elem[pos] > 0 || dir > 0 && elem[pos] + realSize < elem[max] || !dir && realSize < elem[max];
1119 * Scroll the contents of the given element to the absolute *left*
1120 * and *top* pixel offsets.
1122 * @param {Element} elem The element to scroll.
1123 * @param {number|null} left The left absolute pixel offset. If
1124 * null, to not alter the horizontal scroll offset.
1125 * @param {number|null} top The top absolute pixel offset. If
1126 * null, to not alter the vertical scroll offset.
1128 scrollTo: function scrollTo(elem, left, top) {
1129 // Temporary hack. Should be done better.
1130 if (elem.ownerDocument == buffer.focusedFrame.document)
1133 elem.scrollLeft = left;
1135 elem.scrollTop = top;
1139 * Scrolls the currently given element horizontally.
1141 * @param {Element} elem The element to scroll.
1142 * @param {string} increment The increment by which to scroll.
1143 * Possible values are: "columns", "pages"
1144 * @param {number} number The possibly fractional number of
1145 * increments to scroll. Positive values scroll to the right while
1146 * negative values scroll to the left.
1147 * @throws {FailedAssertion} if scrolling is not possible in the
1150 scrollHorizontal: function scrollHorizontal(elem, increment, number) {
1151 let fontSize = parseInt(util.computedStyle(elem).fontSize);
1152 if (increment == "columns")
1153 increment = fontSize; // Good enough, I suppose.
1154 else if (increment == "pages")
1155 increment = elem.clientWidth - fontSize;
1159 let left = elem.dactylScrollDestX !== undefined ? elem.dactylScrollDestX : elem.scrollLeft;
1160 elem.dactylScrollDestX = undefined;
1162 dactyl.assert(number < 0 ? left > 0 : left < elem.scrollWidth - elem.clientWidth);
1163 Buffer.scrollTo(elem, left + number * increment, null);
1167 * Scrolls the currently given element vertically.
1169 * @param {Element} elem The element to scroll.
1170 * @param {string} increment The increment by which to scroll.
1171 * Possible values are: "lines", "pages"
1172 * @param {number} number The possibly fractional number of
1173 * increments to scroll. Positive values scroll upward while
1174 * negative values scroll downward.
1175 * @throws {FailedAssertion} if scrolling is not possible in the
1178 scrollVertical: function scrollVertical(elem, increment, number) {
1179 let fontSize = parseInt(util.computedStyle(elem).fontSize);
1180 if (increment == "lines")
1181 increment = fontSize;
1182 else if (increment == "pages")
1183 increment = elem.clientHeight - fontSize;
1187 let top = elem.dactylScrollDestY !== undefined ? elem.dactylScrollDestY : elem.scrollTop;
1188 elem.dactylScrollDestY = undefined;
1190 dactyl.assert(number < 0 ? top > 0 : top < elem.scrollHeight - elem.clientHeight);
1191 Buffer.scrollTo(elem, null, top + number * increment);
1195 * Scrolls the currently active element to the given horizontal and
1196 * vertical percentages.
1198 * @param {Element} elem The element to scroll.
1199 * @param {number|null} horizontal The possibly fractional
1200 * percentage of the current viewport width to scroll to. If null,
1201 * do not scroll horizontally.
1202 * @param {number|null} vertical The possibly fractional percentage
1203 * of the current viewport height to scroll to. If null, do not
1204 * scroll vertically.
1206 scrollToPercent: function scrollToPercent(elem, horizontal, vertical) {
1207 Buffer.scrollTo(elem,
1208 horizontal == null ? null
1209 : (elem.scrollWidth - elem.clientWidth) * (horizontal / 100),
1210 vertical == null ? null
1211 : (elem.scrollHeight - elem.clientHeight) * (vertical / 100));
1214 openUploadPrompt: function openUploadPrompt(elem) {
1215 io.CommandFileMode("Upload file: ", {
1216 onSubmit: function onSubmit(path) {
1217 let file = io.File(path);
1218 dactyl.assert(file.exists());
1220 elem.value = file.path;
1221 events.dispatch(elem, events.create(elem.ownerDocument, "change", {}));
1223 }).open(elem.value);
1226 commands: function initCommands(dactyl, modules, window) {
1227 commands.add(["frameo[nly]"],
1228 "Show only the current frame's page",
1230 dactyl.open(buffer.focusedFrame.location.href);
1234 commands.add(["ha[rdcopy]"],
1235 "Print current document",
1239 // FIXME: arg handling is a bit of a mess, check for filename
1240 dactyl.assert(!arg || arg[0] == ">" && !util.OS.isWindows,
1241 _("error.trailing"));
1243 prefs.withContext(function () {
1245 prefs.set("print.print_to_file", "true");
1246 prefs.set("print.print_to_filename", io.File(arg.substr(1)).path);
1247 dactyl.echomsg(_("print.toFile", arg.substr(1)));
1250 dactyl.echomsg(_("print.sending"));
1252 prefs.set("print.always_print_silent", args.bang);
1253 prefs.set("print.show_print_progress", !args.bang);
1255 config.browser.contentWindow.print();
1259 dactyl.echomsg(_("print.printed", arg.substr(1)));
1261 dactyl.echomsg(_("print.sent"));
1269 commands.add(["pa[geinfo]"],
1270 "Show various page information",
1273 let opt = options.get("pageinfo");
1275 dactyl.assert(!arg || opt.validator(opt.parse(arg)),
1276 _("error.invalidArgument", arg));
1277 buffer.showPageInfo(true, arg);
1281 completer: function (context) {
1282 completion.optionValue(context, "pageinfo", "+", "");
1283 context.title = ["Page Info"];
1287 commands.add(["pagest[yle]", "pas"],
1288 "Select the author style sheet to apply",
1290 let arg = args[0] || "";
1292 let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title);
1294 dactyl.assert(!arg || titles.indexOf(arg) >= 0,
1295 _("error.invalidArgument", arg));
1297 if (options["usermode"])
1298 options["usermode"] = false;
1300 window.stylesheetSwitchAll(buffer.focusedFrame, arg);
1304 completer: function (context) completion.alternateStyleSheet(context),
1308 commands.add(["re[load]"],
1309 "Reload the current web page",
1310 function (args) { tabs.reload(config.browser.mCurrentTab, args.bang); },
1316 // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional?
1317 commands.add(["sav[eas]", "w[rite]"],
1318 "Save current document to disk",
1320 let doc = content.document;
1321 let chosenData = null;
1322 let filename = args[0];
1324 let command = commandline.command;
1326 if (filename[0] == "!")
1327 return buffer.viewSourceExternally(buffer.focusedFrame.document,
1329 let output = io.system(filename.substr(1), file);
1330 commandline.command = command;
1331 commandline.commandOutput(<span highlight="CmdOutput">{output}</span>);
1334 if (/^>>/.test(filename)) {
1335 let file = io.File(filename.replace(/^>>\s*/, ""));
1336 dactyl.assert(args.bang || file.exists() && file.isWritable(),
1337 _("io.notWriteable", file.path.quote()));
1338 return buffer.viewSourceExternally(buffer.focusedFrame.document,
1339 function (tmpFile) {
1341 file.write(tmpFile, ">>");
1344 dactyl.echoerr(_("io.notWriteable", file.path.quote()));
1349 let file = io.File(filename.replace(RegExp(File.PATH_SEP + "*$"), ""));
1351 if (filename.substr(-1) === File.PATH_SEP || file.exists() && file.isDirectory())
1352 file.append(Buffer.getDefaultNames(doc)[0][0]);
1354 dactyl.assert(args.bang || !file.exists(), _("io.exists"));
1356 chosenData = { file: file, uri: util.newURI(doc.location.href) };
1359 // if browser.download.useDownloadDir = false then the "Save As"
1360 // dialog is used with this as the default directory
1361 // TODO: if we're going to do this shouldn't it be done in setCWD or the value restored?
1362 prefs.set("browser.download.lastDir", io.cwd.path);
1365 var contentDisposition = content.QueryInterface(Ci.nsIInterfaceRequestor)
1366 .getInterface(Ci.nsIDOMWindowUtils)
1367 .getDocumentMetadata("content-disposition");
1371 window.internalSave(doc.location.href, doc, null, contentDisposition,
1372 doc.contentType, false, null, chosenData,
1373 doc.referrer ? window.makeURI(doc.referrer) : null,
1379 completer: function (context) {
1380 if (context.filter[0] == "!")
1382 if (/^>>/.test(context.filter))
1383 context.advance(/^>>\s*/.exec(context.filter)[0].length);
1385 completion.savePage(context, content.document);
1386 context.fork("file", 0, completion, "file");
1391 commands.add(["st[op]"],
1392 "Stop loading the current web page",
1393 function () { buffer.stop(); },
1396 commands.add(["vie[wsource]"],
1397 "View source code of current document",
1398 function (args) { buffer.viewSource(args[0], args.bang); },
1402 completer: function (context) completion.url(context, "bhf")
1405 commands.add(["zo[om]"],
1406 "Set zoom value of current web page",
1413 else if (/^\d+$/.test(arg))
1414 level = parseInt(arg, 10);
1415 else if (/^[+-]\d+$/.test(arg)) {
1416 level = Math.round(buffer.zoomLevel + parseInt(arg, 10));
1417 level = Math.constrain(level, Buffer.ZOOM_MIN, Buffer.ZOOM_MAX);
1420 dactyl.assert(false, _("error.trailing"));
1422 buffer.setZoom(level, args.bang);
1429 completion: function initCompletion(dactyl, modules, window) {
1430 completion.alternateStyleSheet = function alternateStylesheet(context) {
1431 context.title = ["Stylesheet", "Location"];
1433 // unify split style sheets
1434 let styles = iter([s.title, []] for (s in values(buffer.alternateStyleSheets))).toObject();
1436 buffer.alternateStyleSheets.forEach(function (style) {
1437 styles[style.title].push(style.href || "inline");
1440 context.completions = [[title, href.join(", ")] for ([title, href] in Iterator(styles))];
1443 completion.buffer = function buffer(context) {
1444 let filter = context.filter.toLowerCase();
1445 let defItem = { parent: { getTitle: function () "" } };
1448 tabs.allTabs.forEach(function (tab, i) {
1449 let group = (tab.tabItem || tab._tabViewTabItem || defItem).parent || defItem.parent;
1450 if (!set.has(tabGroups, group.id))
1451 tabGroups[group.id] = [group.getTitle(), []];
1452 group = tabGroups[group.id];
1453 group[1].push([i, tab.linkedBrowser]);
1456 context.pushProcessor(0, function (item, text, next) <>
1457 <span highlight="Indicator" style="display: inline-block;">{item.indicator}</span>
1458 { next.call(this, item, text) }
1460 context.process[1] = function (item, text) template.bookmarkDescription(item, template.highlightFilter(text, this.filter));
1462 context.anchored = false;
1466 indicator: function (item) item.tab === tabs.getTab() ? "%" :
1467 item.tab === tabs.alternate ? "#" : " ",
1470 command: function () "tabs.select"
1472 context.compare = CompletionContext.Sort.number;
1473 context.filters = [CompletionContext.Filter.textDescription];
1475 for (let [id, vals] in Iterator(tabGroups))
1476 context.fork(id, 0, this, function (context, [name, browsers]) {
1477 context.title = [name || "Buffers"];
1478 context.generate = function ()
1479 Array.map(browsers, function ([i, browser]) {
1480 let indicator = " ";
1481 if (i == tabs.index())
1483 else if (i == tabs.index(tabs.alternate))
1486 let tab = tabs.getTab(i);
1487 let url = browser.contentDocument.location.href;
1491 text: [i + ": " + (tab.label || "(Untitled)"), i + ": " + url],
1495 icon: tab.image || DEFAULT_FAVICON
1501 completion.savePage = function savePage(context, node) {
1502 context.fork("generated", context.filter.replace(/[^/]*$/, "").length,
1503 this, function (context) {
1504 context.completions = Buffer.getDefaultNames(node);
1508 events: function initEvents(dactyl, modules, window) {
1509 events.listen(config.browser, "scroll", buffer.closure._updateBufferPosition, false);
1511 mappings: function initMappings(dactyl, modules, window) {
1512 mappings.add([modes.NORMAL],
1513 ["y", "<yank-location>"], "Yank current location to the clipboard",
1514 function () { dactyl.clipboardWrite(buffer.uri.spec, true); });
1516 mappings.add([modes.NORMAL],
1517 ["<C-a>"], "Increment last number in URL",
1518 function (args) { buffer.incrementURL(Math.max(args.count, 1)); },
1521 mappings.add([modes.NORMAL],
1522 ["<C-x>"], "Decrement last number in URL",
1523 function (args) { buffer.incrementURL(-Math.max(args.count, 1)); },
1526 mappings.add([modes.NORMAL], ["gu"],
1527 "Go to parent directory",
1528 function (args) { buffer.climbUrlPath(Math.max(args.count, 1)); },
1531 mappings.add([modes.NORMAL], ["gU"],
1532 "Go to the root of the website",
1533 function () { buffer.climbUrlPath(-1); });
1535 mappings.add([modes.COMMAND], [".", "<repeat-key>"],
1536 "Repeat the last key event",
1538 if (mappings.repeat) {
1539 for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100))
1545 mappings.add([modes.COMMAND], ["i", "<Insert>"],
1547 function () { modes.push(modes.CARET); });
1549 mappings.add([modes.COMMAND], ["<C-c>"],
1550 "Stop loading the current web page",
1551 function () { ex.stop(); });
1554 mappings.add([modes.COMMAND], ["j", "<Down>", "<C-e>", "<scroll-down-line>"],
1555 "Scroll document down",
1556 function (args) { buffer.scrollVertical("lines", Math.max(args.count, 1)); },
1559 mappings.add([modes.COMMAND], ["k", "<Up>", "<C-y>", "<scroll-up-line>"],
1560 "Scroll document up",
1561 function (args) { buffer.scrollVertical("lines", -Math.max(args.count, 1)); },
1564 mappings.add([modes.COMMAND], dactyl.has("mail") ? ["h", "<scroll-left-column>"] : ["h", "<Left>", "<scroll-left-column>"],
1565 "Scroll document to the left",
1566 function (args) { buffer.scrollHorizontal("columns", -Math.max(args.count, 1)); },
1569 mappings.add([modes.COMMAND], dactyl.has("mail") ? ["l", "<scroll-right-column>"] : ["l", "<Right>", "<scroll-right-column>"],
1570 "Scroll document to the right",
1571 function (args) { buffer.scrollHorizontal("columns", Math.max(args.count, 1)); },
1574 mappings.add([modes.COMMAND], ["0", "^", "<scroll-begin>"],
1575 "Scroll to the absolute left of the document",
1576 function () { buffer.scrollToPercent(0, null); });
1578 mappings.add([modes.COMMAND], ["$", "<scroll-end>"],
1579 "Scroll to the absolute right of the document",
1580 function () { buffer.scrollToPercent(100, null); });
1582 mappings.add([modes.COMMAND], ["gg", "<Home>"],
1583 "Go to the top of the document",
1584 function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 0); },
1587 mappings.add([modes.COMMAND], ["G", "<End>"],
1588 "Go to the end of the document",
1589 function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 100); },
1592 mappings.add([modes.COMMAND], ["%", "<scroll-percent>"],
1593 "Scroll to {count} percent of the document",
1595 dactyl.assert(args.count > 0 && args.count <= 100);
1596 buffer.scrollToPercent(null, args.count);
1600 mappings.add([modes.COMMAND], ["<C-d>", "<scroll-down>"],
1601 "Scroll window downwards in the buffer",
1602 function (args) { buffer._scrollByScrollSize(args.count, true); },
1605 mappings.add([modes.COMMAND], ["<C-u>", "<scroll-up>"],
1606 "Scroll window upwards in the buffer",
1607 function (args) { buffer._scrollByScrollSize(args.count, false); },
1610 mappings.add([modes.COMMAND], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-page-up>"],
1611 "Scroll up a full page",
1612 function (args) { buffer.scrollVertical("pages", -Math.max(args.count, 1)); },
1615 mappings.add([modes.COMMAND], ["<C-f>", "<PageDown>", "<Space>", "<scroll-page-down>"],
1616 "Scroll down a full page",
1617 function (args) { buffer.scrollVertical("pages", Math.max(args.count, 1)); },
1620 mappings.add([modes.COMMAND], ["]f", "<previous-frame>"],
1622 function (args) { buffer.shiftFrameFocus(Math.max(args.count, 1)); },
1625 mappings.add([modes.COMMAND], ["[f", "<next-frame>"],
1626 "Focus previous frame",
1627 function (args) { buffer.shiftFrameFocus(-Math.max(args.count, 1)); },
1630 mappings.add([modes.COMMAND], ["]]", "<next-page>"],
1631 "Follow the link labeled 'next' or '>' if it exists",
1633 buffer.findLink("next", options["nextpattern"], (args.count || 1) - 1, true);
1637 mappings.add([modes.COMMAND], ["[[", "<previous-page>"],
1638 "Follow the link labeled 'prev', 'previous' or '<' if it exists",
1640 buffer.findLink("previous", options["previouspattern"], (args.count || 1) - 1, true);
1644 mappings.add([modes.COMMAND], ["gf", "<view-source>"],
1645 "Toggle between rendered and source view",
1646 function () { buffer.viewSource(null, false); });
1648 mappings.add([modes.COMMAND], ["gF", "<view-source-externally>"],
1649 "View source with an external editor",
1650 function () { buffer.viewSource(null, true); });
1652 mappings.add([modes.COMMAND], ["gi", "<focus-input>"],
1653 "Focus last used input field",
1655 let elem = buffer.lastInputField;
1657 if (args.count >= 1 || !elem || !events.isContentNode(elem)) {
1658 let xpath = ["frame", "iframe", "input", "textarea[not(@disabled) and not(@readonly)]"];
1660 let frames = buffer.allFrames(null, true);
1662 let elements = array.flatten(frames.map(function (win) [m for (m in util.evaluateXPath(xpath, win.document))]))
1663 .filter(function (elem) {
1664 if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
1665 return Editor.getEditor(elem.contentWindow);
1667 if (elem.readOnly || elem instanceof HTMLInputElement && !set.has(util.editableInputs, elem.type))
1670 let computedStyle = util.computedStyle(elem);
1671 let rect = elem.getBoundingClientRect();
1672 return computedStyle.visibility != "hidden" && computedStyle.display != "none" &&
1673 computedStyle.MozUserFocus != "ignore" && rect.width && rect.height;
1676 dactyl.assert(elements.length > 0);
1677 elem = elements[Math.constrain(args.count, 1, elements.length) - 1];
1679 buffer.focusElement(elem);
1680 util.scrollIntoView(elem);
1684 mappings.add([modes.COMMAND], ["gP"],
1685 "Open (]put) a URL based on the current clipboard contents in a new buffer",
1687 let url = dactyl.clipboardRead();
1688 dactyl.assert(url, _("error.clipboardEmpty"));
1689 dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB, background: true });
1692 mappings.add([modes.COMMAND], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
1693 "Open (put) a URL based on the current clipboard contents in the current buffer",
1695 let url = dactyl.clipboardRead();
1696 dactyl.assert(url, _("error.clipboardEmpty"));
1700 mappings.add([modes.COMMAND], ["P", "<tab-open-clipboard-url>"],
1701 "Open (put) a URL based on the current clipboard contents in a new buffer",
1703 let url = dactyl.clipboardRead();
1704 dactyl.assert(url, _("error.clipboardEmpty"));
1705 dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB });
1709 mappings.add([modes.COMMAND], ["r", "<reload>"],
1710 "Reload the current web page",
1711 function () { tabs.reload(tabs.getTab(), false); });
1713 mappings.add([modes.COMMAND], ["R", "<full-reload>"],
1714 "Reload while skipping the cache",
1715 function () { tabs.reload(tabs.getTab(), true); });
1718 mappings.add([modes.COMMAND], ["Y", "<yank-word>"],
1719 "Copy selected text or current word",
1721 let sel = buffer.currentWord;
1723 dactyl.clipboardWrite(sel, true);
1727 mappings.add([modes.COMMAND], ["zi", "+", "<text-zoom-in>"],
1728 "Enlarge text zoom of current web page",
1729 function (args) { buffer.zoomIn(Math.max(args.count, 1), false); },
1732 mappings.add([modes.COMMAND], ["zm", "<text-zoom-more>"],
1733 "Enlarge text zoom of current web page by a larger amount",
1734 function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, false); },
1737 mappings.add([modes.COMMAND], ["zo", "-", "<text-zoom-out>"],
1738 "Reduce text zoom of current web page",
1739 function (args) { buffer.zoomOut(Math.max(args.count, 1), false); },
1742 mappings.add([modes.COMMAND], ["zr", "<text-zoom-reduce>"],
1743 "Reduce text zoom of current web page by a larger amount",
1744 function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, false); },
1747 mappings.add([modes.COMMAND], ["zz", "<text-zoom>"],
1748 "Set text zoom value of current web page",
1749 function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, false); },
1752 mappings.add([modes.COMMAND], ["ZI", "zI", "<full-zoom-in>"],
1753 "Enlarge full zoom of current web page",
1754 function (args) { buffer.zoomIn(Math.max(args.count, 1), true); },
1757 mappings.add([modes.COMMAND], ["ZM", "zM", "<full-zoom-more>"],
1758 "Enlarge full zoom of current web page by a larger amount",
1759 function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, true); },
1762 mappings.add([modes.COMMAND], ["ZO", "zO", "<full-zoom-out>"],
1763 "Reduce full zoom of current web page",
1764 function (args) { buffer.zoomOut(Math.max(args.count, 1), true); },
1767 mappings.add([modes.COMMAND], ["ZR", "zR", "<full-zoom-reduce>"],
1768 "Reduce full zoom of current web page by a larger amount",
1769 function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, true); },
1772 mappings.add([modes.COMMAND], ["zZ", "<full-zoom>"],
1773 "Set full zoom value of current web page",
1774 function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, true); },
1778 mappings.add([modes.COMMAND], ["<C-g>", "<page-info>"],
1779 "Print the current file name",
1780 function () { buffer.showPageInfo(false); });
1782 mappings.add([modes.COMMAND], ["g<C-g>", "<more-page-info>"],
1783 "Print file information",
1784 function () { buffer.showPageInfo(true); });
1786 options: function initOptions(dactyl, modules, window) {
1787 options.add(["encoding", "enc"],
1788 "The current buffer's character encoding",
1791 scope: Option.SCOPE_LOCAL,
1792 getter: function () config.browser.docShell.QueryInterface(Ci.nsIDocCharset).charset,
1793 setter: function (val) {
1794 if (options["encoding"] == val)
1797 // Stolen from browser.jar/content/browser/browser.js, more or less.
1799 config.browser.docShell.QueryInterface(Ci.nsIDocCharset).charset = val;
1800 PlacesUtils.history.setCharsetForURI(getWebNavigation().currentURI, val);
1801 getWebNavigation().reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
1803 catch (e) { dactyl.reportError(e); }
1806 completer: function (context) completion.charset(context)
1809 options.add(["iskeyword", "isk"],
1810 "Regular expression defining which characters constitute word characters",
1811 "string", '[^\\s.,!?:;/"\'^$%&?()[\\]{}<>#*+|=~_-]',
1813 setter: function (value) {
1814 this.regexp = util.regexp(value);
1817 validator: function (value) RegExp(value)
1820 options.add(["nextpattern"],
1821 "Patterns to use when guessing the next page in a document sequence",
1822 "regexplist", UTF8("'\\bnext\\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\\bmore\\b'"),
1823 { regexpFlags: "i" });
1825 options.add(["previouspattern"],
1826 "Patterns to use when guessing the previous page in a document sequence",
1827 "regexplist", UTF8("'\\bprev|previous\\b',^<$,^(<<|«)$,^(<|«),(<|«)$"),
1828 { regexpFlags: "i" });
1830 options.add(["pageinfo", "pa"],
1831 "Define which sections are shown by the :pageinfo command",
1833 { get values() values(buffer.pageInfo).toObject() });
1835 options.add(["scroll", "scr"],
1836 "Number of lines to scroll with <C-u> and <C-d> commands",
1838 { validator: function (value) value >= 0 });
1840 options.add(["showstatuslinks", "ssli"],
1841 "Where to show the destination of the link under the cursor",
1845 "": "Don't show link destinations",
1846 "status": "Show link destinations in the status line",
1847 "command": "Show link destinations in the command line"
1851 options.add(["usermode", "um"],
1852 "Show current website without styling defined by the author",
1855 setter: function (value) config.browser.markupDocumentViewer.authorStyleDisabled = value,
1856 getter: function () config.browser.markupDocumentViewer.authorStyleDisabled
1861 // vim: set fdm=marker sw=4 ts=4 et: