buffer.js: fix typo
[conkeror.git] / modules / buffer.js
blob0d4779456cbb354b961e4bd62e6f35b6efc40b5b
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2010 John J. Foerch
4  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
10 in_module(null);
12 var define_buffer_local_hook = local_hook_definer("window");
14 function define_current_buffer_hook (hook_name, existing_hook) {
15     define_buffer_local_hook(hook_name);
16     add_hook(existing_hook, function (buffer) {
17             if (!buffer.window.buffers || buffer != buffer.window.buffers.current)
18                 return;
19             var hook = conkeror[hook_name];
20             hook.run.apply(hook, Array.prototype.slice.call(arguments));
21         });
24 define_buffer_local_hook("buffer_title_change_hook");
25 define_buffer_local_hook("buffer_description_change_hook");
26 define_buffer_local_hook("buffer_icon_change_hook");
27 define_buffer_local_hook("select_buffer_hook");
28 define_buffer_local_hook("create_buffer_early_hook");
29 define_buffer_local_hook("create_buffer_late_hook");
30 define_buffer_local_hook("create_buffer_hook");
31 define_buffer_local_hook("kill_buffer_hook");
32 define_buffer_local_hook("buffer_scroll_hook");
33 define_buffer_local_hook("buffer_dom_content_loaded_hook");
34 define_buffer_local_hook("buffer_loaded_hook");
35 define_buffer_local_hook("set_input_mode_hook");
37 define_current_buffer_hook("current_buffer_title_change_hook", "buffer_title_change_hook");
38 define_current_buffer_hook("current_buffer_description_change_hook", "buffer_description_change_hook");
39 define_current_buffer_hook("current_buffer_icon_change_hook", "buffer_icon_change_hook");
40 define_current_buffer_hook("current_buffer_scroll_hook", "buffer_scroll_hook");
41 define_current_buffer_hook("current_buffer_dom_content_loaded_hook", "buffer_dom_content_loaded_hook");
44 define_keywords("$opener");
45 function buffer_creator (type) {
46     var args = forward_keywords(arguments);
47     return function (window) {
48         return new type(window, args);
49     };
52 define_variable("allow_browser_window_close", true,
53     "If this is set to true, if a content buffer page calls " +
54     "window.close() from JavaScript and is not prevented by the " +
55     "normal Mozilla mechanism that restricts pages from closing " +
56     "a window that was not opened by a script, the buffer will be " +
57     "killed, deleting the window as well if it is the only buffer.");
59 function buffer_modality (buffer) {
60     buffer.keymaps.push(default_global_keymap);
63 function buffer (window) {
64     this.constructor_begin();
65     keywords(arguments);
66     this.opener = arguments.$opener;
67     this.window = window;
68     var element = create_XUL(window, "vbox");
69     element.setAttribute("flex", "1");
70     var browser = create_XUL(window, "browser");
71     browser.setAttribute("type", "content");
72     browser.setAttribute("flex", "1");
73     browser.setAttribute("autocompletepopup", "popup_autocomplete");
74     element.appendChild(browser);
75     this.window.buffers.container.appendChild(element);
76     this.window.buffers.buffer_list.push(this);
77     this.element = element;
78     this.browser = element.firstChild;
79     this.element.conkeror_buffer_object = this;
81     this.local = { __proto__: conkeror };
82     this.page = null;
83     this.enabled_modes = [];
84     this.default_browser_object_classes = {};
86     var buffer = this;
88     this.browser.addEventListener("scroll", function (event) {
89             buffer_scroll_hook.run(buffer);
90         }, true /* capture */);
92     this.browser.addEventListener("DOMContentLoaded", function (event) {
93             buffer_dom_content_loaded_hook.run(buffer);
94         }, true /* capture */);
96     this.window.setTimeout(function () { create_buffer_late_hook.run(buffer); }, 0);
98     this.browser.addEventListener("load", function (event) {
99             buffer_loaded_hook.run(buffer);
100         }, true /* capture */);
102     this.browser.addEventListener("DOMWindowClose", function (event) {
103             /* This call to preventDefault is very important; without
104              * it, somehow Mozilla does something bad and as a result
105              * the window loses focus, causing keyboard commands to
106              * stop working. */
107             event.preventDefault();
109             if (allow_browser_window_close)
110                 kill_buffer(buffer, true);
111         }, true);
113     this.browser.addEventListener("focus", function (event) {
114         if (buffer.focusblocker &&
115             event.target instanceof Ci.nsIDOMHTMLElement &&
116             buffer.focusblocker(buffer, event))
117         {
118             event.target.blur();
119         } else
120             buffer.set_input_mode();
121     }, true);
123     this.browser.addEventListener("blur", function (event) {
124         buffer.set_input_mode();
125     }, true);
127     this.modalities = [buffer_modality];
129     // When create_buffer_early_hook runs, basic buffer properties
130     // will be available, but not the properties subclasses.
131     create_buffer_early_hook.run(this);
133     this.constructor_end();
135 buffer.prototype = {
136     constructor: buffer,
138     /* Saved focus state */
139     saved_focused_frame: null,
140     saved_focused_element: null,
142     // get title ()   [must be defined by subclasses]
143     // get name ()    [must be defined by subclasses]
144     dead: false, /* This is set when the buffer is killed */
146     keymaps: null,
147     mark_active: false,
149     // The property focusblocker is available for an external module to
150     // put a function on which takes a buffer as its argument and returns
151     // true to block a focus event, or false to let normal processing
152     // occur.  Having this one property explicitly handled by the buffer
153     // class allows for otherwise modular focus-blockers.
154     focusblocker: null,
156     // icon is a string url of an icon to use for this buffer.  Setting it
157     // causes buffer_icon_change_hook to be run.
158     _icon: null,
159     get icon () this._icon,
160     set icon (x) {
161         if (this._icon != x) {
162             this._icon = x;
163             buffer_icon_change_hook.run(this);
164         }
165     },
167     default_message: "",
169     set_default_message: function (str) {
170         this.default_message = str;
171         if (this == this.window.buffers.current)
172             this.window.minibuffer.set_default_message(str);
173     },
175     constructors_running: 0,
177     constructor_begin: function () {
178         this.constructors_running++;
179     },
181     constructor_end: function () {
182         if (--this.constructors_running == 0) {
183             create_buffer_hook.run(this);
184             this.set_input_mode();
185             delete this.opener;
186         }
187     },
189     destroy: function () {
190         this.dead = true;
191         this.browser = null;
192         this.element = null;
193         this.saved_focused_frame = null;
194         this.saved_focused_element = null;
195         // prevent modalities from accessing dead browser
196         this.modalities = [];
197     },
199     set_input_mode: function () {
200         if (this != this.window.buffers.current)
201             return;
202         this.keymaps = [];
203         this.modalities.map(function (m) m(this), this);
204         set_input_mode_hook.run(this);
205     },
207     override_keymaps: function (keymaps) {
208         if (keymaps) {
209             this.keymaps = keymaps;
210             this.set_input_mode = function () {
211                 set_input_mode_hook.run(this);
212             };
213         } else
214             delete this.set_input_mode;
215         this.set_input_mode();
216     },
218     /* Browser accessors */
219     get top_frame () { return this.browser.contentWindow; },
220     get document () { return this.browser.contentDocument; },
221     get web_navigation () { return this.browser.webNavigation; },
222     get doc_shell () { return this.browser.docShell; },
223     get markup_document_viewer () { return this.browser.markupDocumentViewer; },
224     get current_uri () { return this.browser.currentURI; },
226     is_child_element: function (element) {
227         return (element && this.is_child_frame(element.ownerDocument.defaultView));
228     },
230     is_child_frame: function (frame) {
231         return (frame && frame.top == this.top_frame);
232     },
234     // This method is like focused_frame, except that if no content
235     // frame actually has focus, this returns null.
236     get focused_frame_or_null () {
237         var frame = this.window.document.commandDispatcher.focusedWindow;
238         if (this.is_child_frame(frame))
239             return frame;
240         return null;
241     },
243     get focused_frame () {
244         var frame = this.window.document.commandDispatcher.focusedWindow;
245         if (this.is_child_frame(frame))
246             return frame;
247         return this.top_frame;
248     },
250     get focused_element () {
251         var element = this.window.document.commandDispatcher.focusedElement;
252         if (this.is_child_element(element))
253             return element;
254         return null;
255     },
257     get focused_selection_controller () {
258         return this.focused_frame
259             .QueryInterface(Ci.nsIInterfaceRequestor)
260             .getInterface(Ci.nsIWebNavigation)
261             .QueryInterface(Ci.nsIInterfaceRequestor)
262             .getInterface(Ci.nsISelectionDisplay)
263             .QueryInterface(Ci.nsISelectionController);
264     },
266     do_command: function (command) {
267         function attempt_command (element, command) {
268             var controller;
269             if (element.controllers
270                 && (controller = element.controllers.getControllerForCommand(command)) != null
271                 && controller.isCommandEnabled(command))
272             {
273                 controller.doCommand(command);
274                 return true;
275             }
276             return false;
277         }
279         var element = this.focused_element;
280         if (element && attempt_command(element, command))
281             return;
282         var win = this.focused_frame;
283         while (true) {
284             if (attempt_command(win, command))
285                 return;
286             if (!win.parent || win == win.parent)
287                 break;
288             win = win.parent;
289         }
290     }
293 function with_current_buffer (buffer, callback) {
294     return callback(new interactive_context(buffer));
297 function check_buffer (obj, type) {
298     if (!(obj instanceof type))
299         throw interactive_error("Buffer has invalid type.");
300     if (obj.dead)
301         throw interactive_error("Buffer has already been killed.");
302     return obj;
305 function caret_enabled (buffer) {
306     return buffer.browser.getAttribute('showcaret');
309 function clear_selection (buffer) {
310     let sel_ctrl = buffer.focused_selection_controller;
311     if (sel_ctrl) {
312         let sel = sel_ctrl.getSelection(sel_ctrl.SELECTION_NORMAL);
313         if (caret_enabled(buffer)) {
314             if (sel.anchorNode)
315                 sel.collapseToStart();
316         } else {
317             sel.removeAllRanges();
318         }
319     }
323 function buffer_container (window, create_initial_buffer) {
324     this.window = window;
325     this.container = window.document.getElementById("buffer-container");
326     this.buffer_list = [];
327     window.buffers = this;
328     create_initial_buffer(window);
330 buffer_container.prototype = {
331     constructor: buffer_container,
333     get current () {
334         return this.container.selectedPanel.conkeror_buffer_object;
335     },
337     set current (buffer) {
338         var old_value = this.current;
339         if (old_value == buffer)
340             return;
342         this.buffer_list.splice(this.buffer_list.indexOf(buffer), 1);
343         this.buffer_list.unshift(buffer);
345         this._switch_away_from(this.current);
346         this._switch_to(buffer);
348         // Run hooks
349         select_buffer_hook.run(buffer);
350     },
352     _switch_away_from: function (old_value) {
353         // Save focus state
354         old_value.saved_focused_frame = old_value.focused_frame;
355         old_value.saved_focused_element = old_value.focused_element;
357         old_value.browser.setAttribute("type", "content");
358     },
360     _switch_to: function (buffer) {
361         // Select new buffer in the XUL deck
362         this.container.selectedPanel = buffer.element;
364         buffer.browser.setAttribute("type", "content-primary");
366         /**
367          * This next focus call seems to be needed to avoid focus
368          * somehow getting lost (and the keypress handler therefore
369          * not getting called at all) when killing buffers.
370          */
371         this.window.focus();
373         // Restore focus state
374         buffer.browser.focus();
375         if (buffer.saved_focused_element)
376             set_focus_no_scroll(this.window, buffer.saved_focused_element);
377         else if (buffer.saved_focused_frame)
378             set_focus_no_scroll(this.window, buffer.saved_focused_frame);
380         buffer.saved_focused_element = null;
381         buffer.saved_focused_frame = null;
383         buffer.set_input_mode();
385         this.window.minibuffer.set_default_message(buffer.default_message);
386     },
388     get count () {
389         return this.container.childNodes.length;
390     },
392     get_buffer: function (index) {
393         if (index >= 0 && index < this.count)
394             return this.container.childNodes.item(index).conkeror_buffer_object;
395         return null;
396     },
398     get selected_index () {
399         var nodes = this.container.childNodes;
400         var count = nodes.length;
401         for (var i = 0; i < count; ++i)
402             if (nodes.item(i) == this.container.selectedPanel)
403                 return i;
404         return null;
405     },
407     index_of: function (b) {
408         var nodes = this.container.childNodes;
409         var count = nodes.length;
410         for (var i = 0; i < count; ++i)
411             if (nodes.item(i) == b.element)
412                 return i;
413         return null;
414     },
416     get unique_name_list () {
417         var existing_names = new string_hashset();
418         var bufs = [];
419         this.for_each(function(b) {
420                 var base_name = b.name;
421                 var name = base_name;
422                 var index = 1;
423                 while (existing_names.contains(name)) {
424                     ++index;
425                     name = base_name + "<" + index + ">";
426                 }
427                 existing_names.add(name);
428                 bufs.push([name, b]);
429             });
430         return bufs;
431     },
433     kill_buffer: function (b) {
434         if (b.dead)
435             return true;
436         var count = this.count;
437         if (count <= 1)
438             return false;
439         var new_buffer = this.buffer_list[0];
440         var changed = false;
441         if (b == new_buffer) {
442             new_buffer = this.buffer_list[1];
443             changed = true;
444         }
445         this._switch_away_from(this.current);
446         // The removeChild call below may trigger events in progress
447         // listeners.  This call to `destroy' gives buffer subclasses a
448         // chance to remove such listeners, so that they cannot try to
449         // perform UI actions based upon a xul:browser that no longer
450         // exists.
451         var element = b.element;
452         b.destroy();
453         this.container.removeChild(element);
454         this.buffer_list.splice(this.buffer_list.indexOf(b), 1);
455         this._switch_to(new_buffer);
456         if (changed) {
457             select_buffer_hook.run(new_buffer);
458             this.buffer_list.splice(this.buffer_list.indexOf(new_buffer), 1);
459             this.buffer_list.unshift(new_buffer);
460         }
461         kill_buffer_hook.run(b);
462         return true;
463     },
465     bury_buffer: function (b) {
466         var new_buffer = this.buffer_list[0];
467         if (b == new_buffer)
468             new_buffer = this.buffer_list[1];
469         if (! new_buffer)
470             throw interactive_error("No other buffer");
471         this.buffer_list.splice(this.buffer_list.indexOf(b), 1);
472         this.buffer_list.push(b);
473         this.current = new_buffer;
474         return true;
475     },
477     for_each: function (f) {
478         var count = this.count;
479         for (var i = 0; i < count; ++i)
480             f(this.get_buffer(i));
481     }
484 function buffer_initialize_window_early (window) {
485     /**
486      * Use content_buffer by default to handle an unusual case where
487      * browser.chromeURI is used perhaps.  In general this default
488      * should not be needed.
489      */
490     var create_initial_buffer =
491         window.args.initial_buffer_creator || buffer_creator(content_buffer);
492     new buffer_container(window, create_initial_buffer);
495 add_hook("window_initialize_early_hook", buffer_initialize_window_early);
499  * initialize_first_buffer_type is a workaround for a XULRunner bug that
500  * first appeared in version 2.0, manifested as missing scrollbars in the
501  * first buffer of any window.  It only affects content-primary browsers,
502  * and the workaround is to initialize the browser as type "content" then
503  * change it to content-primary after a delay.
504  */
505 function initialize_first_buffer_type (window) {
506     window.buffers.current.browser.setAttribute("type", "content-primary");
509 add_hook("window_initialize_late_hook", initialize_first_buffer_type);
512 define_buffer_local_hook("buffer_kill_before_hook", RUN_HOOK_UNTIL_FAILURE);
513 function buffer_before_window_close (window) {
514     var bs = window.buffers;
515     var count = bs.count;
516     for (let i = 0; i < count; ++i) {
517         if (!buffer_kill_before_hook.run(bs.get_buffer(i)))
518             return false;
519     }
520     return true;
522 add_hook("window_before_close_hook", buffer_before_window_close);
524 function buffer_window_close_handler (window) {
525     var bs = window.buffers;
526     var count = bs.count;
527     for (let i = 0; i < count; ++i) {
528         let b = bs.get_buffer(i);
529         b.destroy();
530     }
532 add_hook("window_close_hook", buffer_window_close_handler);
534 /* open/follow targets */
535 const OPEN_CURRENT_BUFFER = 0; // only valid for open if the current
536                                // buffer is a content_buffer.
537 const OPEN_NEW_BUFFER = 1;
538 const OPEN_NEW_BUFFER_BACKGROUND = 2;
539 const OPEN_NEW_WINDOW = 3;
541 const FOLLOW_DEFAULT = 4; // for open, implies OPEN_CURRENT_BUFFER
542 const FOLLOW_CURRENT_FRAME = 5; // for open, implies OPEN_CURRENT_BUFFER
544 var TARGET_PROMPTS = [" in current buffer",
545                       " in new buffer",
546                       " in new buffer (background)",
547                       " in new window",
548                       "",
549                       " in current frame"];
551 var TARGET_NAMES = ["current buffer",
552                     "new buffer",
553                     "new buffer (background)",
554                     "new window",
555                     "default",
556                     "current frame"];
559 function create_buffer (window, creator, target) {
560     switch (target) {
561     case OPEN_NEW_BUFFER:
562         window.buffers.current = creator(window, null);
563         break;
564     case OPEN_NEW_BUFFER_BACKGROUND:
565         creator(window, null);
566         break;
567     case OPEN_NEW_WINDOW:
568         make_window(creator);
569         break;
570     default:
571         throw new Error("invalid target");
572     }
575 let (queued_buffer_creators = null) {
576     function create_buffer_in_current_window (creator, target, focus_existing) {
577         function process_queued_buffer_creators (window) {
578             for (var i = 0; i < queued_buffer_creators.length; ++i) {
579                 var x = queued_buffer_creators[i];
580                 create_buffer(window, x[0], x[1]);
581             }
582             queued_buffer_creators = null;
583         }
585         if (target == OPEN_NEW_WINDOW)
586             throw new Error("invalid target");
587         var window = get_recent_conkeror_window();
588         if (window) {
589             if (focus_existing)
590                 window.focus();
591             create_buffer(window, creator, target);
592         } else if (queued_buffer_creators != null) {
593             queued_buffer_creators.push([creator,target]);
594         } else {
595             queued_buffer_creators = [];
596             window = make_window(creator);
597             add_hook.call(window, "window_initialize_late_hook", process_queued_buffer_creators);
598         }
599     }
604  * Read Buffer
605  */
606 define_variable("read_buffer_show_icons", false,
607     "Boolean which says whether read_buffer should show buffer "+
608     "icons in the completions list.\nNote, setting this variable "+
609     "alone does not cause favicons or other kinds of icons to be "+
610     "fetched.  For that, load the `favicon' (or similar other) "+
611     "library.");
613 minibuffer_auto_complete_preferences["buffer"] = true;
614 define_keywords("$default");
615 minibuffer.prototype.read_buffer = function () {
616     var window = this.window;
617     var buffer = this.window.buffers.current;
618     keywords(arguments, $prompt = "Buffer:",
619              $default = buffer,
620              $history = "buffer");
621     var completer = all_word_completer(
622         $completions = function (visitor) window.buffers.for_each(visitor),
623         $get_string = function (x) x.description,
624         $get_description = function (x) x.title,
625         $get_icon = (read_buffer_show_icons ?
626                      function (x) x.icon : null));
627     var result = yield this.read(
628         $keymap = read_buffer_keymap,
629         $prompt = arguments.$prompt,
630         $history = arguments.$history,
631         $completer = completer,
632         $enable_icons = read_buffer_show_icons,
633         $match_required = true,
634         $auto_complete = "buffer",
635         $auto_complete_initial = true,
636         $auto_complete_delay = 0,
637         $default_completion = arguments.$default);
638     yield co_return(result);
642 function buffer_next (window, count) {
643     var index = window.buffers.selected_index;
644     var total = window.buffers.count;
645     if (total == 1)
646         throw new interactive_error("No other buffer");
647     index = (index + count) % total;
648     if (index < 0)
649         index += total;
650     window.buffers.current = window.buffers.get_buffer(index);
652 interactive("buffer-next",
653     "Switch to the next buffer.",
654     function (I) { buffer_next(I.window, I.p); });
655 interactive("buffer-previous",
656     "Switch to the previous buffer.",
657     function (I) { buffer_next(I.window, -I.p); });
659 function switch_to_buffer (window, buffer) {
660     if (buffer && !buffer.dead)
661         window.buffers.current = buffer;
663 interactive("switch-to-buffer",
664     "Switch to a buffer specified in the minibuffer.",
665     function (I) {
666         switch_to_buffer(
667             I.window,
668             (yield I.minibuffer.read_buffer(
669                 $prompt = "Switch to buffer:",
670                 $default = (I.window.buffers.count > 1 ?
671                             I.window.buffers.buffer_list[1] :
672                             I.buffer))));
673     });
675 define_variable("can_kill_last_buffer", true,
676     "If this is set to true, kill-buffer can kill the last "+
677     "remaining buffer, and close the window.");
679 function kill_other_buffers (buffer) {
680     if (!buffer)
681         return;
682     var bs = buffer.window.buffers;
683     var b;
684     while ((b = bs.get_buffer(0)) != buffer)
685         bs.kill_buffer(b);
686     var count = bs.count;
687     while (--count)
688         bs.kill_buffer(bs.get_buffer(1));
690 interactive("kill-other-buffers",
691     "Kill all buffers except current one.\n",
692     function (I) { kill_other_buffers(I.buffer); });
695 function kill_buffer (buffer, force) {
696     if (!buffer)
697         return;
698     var buffers = buffer.window.buffers;
699     if (buffers.count == 1 && buffer == buffers.current) {
700         if (can_kill_last_buffer || force) {
701             delete_window(buffer.window);
702             return;
703         } else
704             throw interactive_error("Can't kill last buffer.");
705     }
706     buffers.kill_buffer(buffer);
708 interactive("kill-buffer",
709     "Kill a buffer specified in the minibuffer.\n" +
710     "If `can_kill_last_buffer' is set to true, an attempt to kill the "+
711     "last remaining buffer in a window will cause the window to be closed.",
712     function (I) {
713         kill_buffer((yield I.minibuffer.read_buffer($prompt = "Kill buffer:")));
714     });
716 interactive("kill-current-buffer",
717     "Kill the current buffer.\n" +
718     "If `can_kill_last_buffer' is set to true, an attempt to kill the "+
719     "last remaining buffer in a window will cause the window to be closed.",
720     function (I) { kill_buffer(I.buffer); });
722 interactive("read-buffer-kill-buffer",
723     "Kill the current selected buffer in the completions list "+
724     "in a read buffer minibuffer interaction.",
725     function (I) {
726         var s = I.window.minibuffer.current_state;
727         var i = s.selected_completion_index;
728         var c = s.completions;
729         if (i == -1)
730             return;
731         kill_buffer(c.get_value(i));
732         s.completer.refresh();
733         s.handle_input(I.window.minibuffer);
734     });
736 interactive("bury-buffer",
737     "Bury the current buffer.\n Put the current buffer at the end of " +
738     "the buffer list, so that it is the least likely buffer to be " +
739     "selected by `switch-to-buffer'.",
740     function (I) { I.window.buffers.bury_buffer(I.buffer); });
742 function change_directory (buffer, dir) {
743     if (buffer.page != null)
744         delete buffer.page.local.cwd;
745     buffer.local.cwd = make_file(dir);
747 interactive("change-directory",
748     "Change the current directory of the selected buffer.",
749     function (I) {
750         change_directory(
751             I.buffer,
752             (yield I.minibuffer.read_existing_directory_path(
753                 $prompt = "Change to directory:",
754                 $initial_value = make_file(I.local.cwd).path)));
755     });
757 interactive("shell-command", null,
758     function (I) {
759         var cwd = I.local.cwd;
760         var cmd = (yield I.minibuffer.read_shell_command($cwd = cwd));
761         yield shell_command(cmd, $cwd = cwd);
762     });
766  * selection_is_embed_p is used to test whether the unfocus command can
767  * unfocus an element, even though there is a selection.  This happens
768  * when the focused element is an html:embed.
769  */
770 function selection_is_embed_p (sel, focused_element) {
771     if (sel.rangeCount == 1) {
772         try {
773             var r = sel.getRangeAt(0);
774             var a = r.startContainer.childNodes[r.startOffset];
775             if ((a instanceof Ci.nsIDOMHTMLEmbedElement ||
776                  a instanceof Ci.nsIDOMHTMLObjectElement) &&
777                 a == focused_element)
778             {
779                 return true;
780             }
781         } catch (e) {}
782     }
783     return false;
787  * unfocus is a high-level command for unfocusing hyperlinks, inputs,
788  * frames, iframes, plugins, and also clearing the selection.
789  */
790 define_buffer_local_hook("unfocus_hook");
791 function unfocus (window, buffer) {
792     // 1. if there is a selection, clear it.
793     var selc = buffer.focused_selection_controller;
794     if (selc) {
795         var sel = selc.getSelection(selc.SELECTION_NORMAL);
796         var active = ! sel.isCollapsed;
797         var embed_p = selection_is_embed_p(sel, buffer.focused_element);
798         clear_selection(buffer);
799         if (active && !embed_p) {
800             window.minibuffer.message("cleared selection");
801             return;
802         }
803     }
804     // 2. if there is a focused element, unfocus it.
805     if (buffer.focused_element) {
806         buffer.focused_element.blur();
807         // if an element in a detached fragment has focus, blur() will
808         // not work, and we need to take more drastic measures.  the
809         // action taken was found through experiment, so it is possibly
810         // not the most concise way to unfocus such an element.
811         if (buffer.focused_element) {
812             buffer.element.focus();
813             buffer.top_frame.focus();
814         }
815         window.minibuffer.message("unfocused element");
816         return;
817     }
818     // 3. if an iframe has focus, we must blur it.
819     if (buffer.focused_frame_or_null &&
820         buffer.focused_frame_or_null.frameElement)
821     {
822         buffer.focused_frame_or_null.frameElement.blur();
823     }
824     // 4. return focus to top-frame from subframes and plugins.
825     buffer.top_frame.focus();
826     buffer.top_frame.focus(); // needed to get focus back from plugins
827     window.minibuffer.message("refocused top frame");
828     // give page-modes an opportunity to set focus specially
829     unfocus_hook.run(buffer);
831 interactive("unfocus",
832     "Unfocus is a high-level command for unfocusing hyperlinks, inputs, "+
833     "frames, iframes, plugins, and also for clearing the selection.\n"+
834     "The action that it takes is based on precedence.  If there is a "+
835     "focused hyperlink or input, it will unfocus that.  Otherwise, if "+
836     "there is a selection, it will clear the selection.  Otherwise, it "+
837     "will return focus to the top frame from a focused frame, iframe, "+
838     "or plugin.  In the case of plugins, since they steal keyboard "+
839     "control away from Conkeror, the normal way to unfocus them is "+
840     "to use command-line remoting externally: conkeror -batch -f "+
841     "unfocus.  Page-modes also have an opportunity to alter the default"+
842     "focus via the hook, `focus_hook'.",
843     function (I) {
844         unfocus(I.window, I.buffer);
845     });
848 function for_each_buffer (f) {
849     for_each_window(function (w) { w.buffers.for_each(f); });
854  * BUFFER MODES
855  */
857 var mode_functions = {};
858 var mode_display_names = {};
860 define_buffer_local_hook("buffer_mode_change_hook");
861 define_current_buffer_hook("current_buffer_mode_change_hook", "buffer_mode_change_hook");
863 define_keywords("$display_name", "$class", "$enable", "$disable", "$doc");
864 function define_buffer_mode (name) {
865     keywords(arguments);
867     var hyphen_name = name.replace("_","-","g");
868     var display_name = arguments.$display_name;
869     var mode_class = arguments.$class;
870     var enable = arguments.$enable;
871     var disable = arguments.$disable;
873     mode_display_names[name] = display_name;
875     var can_disable;
877     if (disable == false) {
878         can_disable = false;
879         disable = null;
880     } else
881         can_disable = true;
883     var state = (mode_class != null) ? mode_class : (name + "_enabled");
884     var enable_hook_name = name + "_enable_hook";
885     var disable_hook_name = name + "_disable_hook";
886     define_buffer_local_hook(enable_hook_name);
887     define_buffer_local_hook(disable_hook_name);
889     var change_hook_name = null;
891     if (mode_class) {
892         mode_functions[name] = { enable: enable,
893                                  disable: disable,
894                                  mode_class: mode_class,
895                                  disable_hook_name: disable_hook_name };
896         change_hook_name = mode_class + "_change_hook";
897         define_buffer_local_hook(change_hook_name);
898     }
900     function func (buffer, arg) {
901         var old_state = buffer[state];
902         var cur_state = (old_state == name);
903         var new_state = (arg == null) ? !cur_state : (arg > 0);
904         if ((new_state == cur_state) || (!can_disable && !new_state))
905             // perhaps show a message if (!can_disable && !new_state)
906             // to tell the user that this mode cannot be disabled.  do
907             // we have any existing modes that would benefit by it?
908             return null;
909         if (new_state) {
910             if (mode_class && old_state != null)  {
911                 // Another buffer-mode of our same mode-class is
912                 // enabled.  Buffer-modes within a mode-class are
913                 // mutually exclusive, so turn the old one off.
914                 buffer.enabled_modes.splice(buffer.enabled_modes.indexOf(old_state), 1);
915                 let x = mode_functions[old_state];
916                 let y = x.disable;
917                 if (y) y(buffer);
918                 conkeror[x.disable_hook_name].run(buffer);
919             }
920             buffer[state] = name;
921             if (enable)
922                 enable(buffer);
923             conkeror[enable_hook_name].run(buffer);
924             buffer.enabled_modes.push(name);
925         } else {
926             buffer.enabled_modes.splice(buffer.enabled_modes.indexOf(name), 1);
927             disable(buffer);
928             conkeror[disable_hook_name].run(buffer);
929             buffer[state] = null;
930         }
931         if (change_hook_name)
932             conkeror[change_hook_name].run(buffer, buffer[state]);
933         buffer_mode_change_hook.run(buffer);
934         return new_state;
935     }
937     conkeror[name] = func;
938     interactive(hyphen_name, arguments.$doc, function (I) {
939         var arg = I.P;
940         var new_state = func(I.buffer, arg && univ_arg_to_number(arg));
941         I.minibuffer.message(hyphen_name + (new_state ? " enabled" : " disabled"));
942     });
944 ignore_function_for_get_caller_source_code_reference("define_buffer_mode");
947 function minibuffer_mode_indicator (window) {
948     this.window = window;
949     var element = create_XUL(window, "label");
950     element.setAttribute("id", "minibuffer-mode-indicator");
951     element.setAttribute("class", "mode-text-widget");
952     window.document.getElementById("minibuffer").appendChild(element);
953     this.element = element;
954     this.hook_func = method_caller(this, this.update);
955     add_hook.call(window, "select_buffer_hook", this.hook_func);
956     add_hook.call(window, "current_buffer_mode_change_hook", this.hook_func);
957     this.update();
959 minibuffer_mode_indicator.prototype = {
960     constructor: minibuffer_mode_indicator,
961     update: function () {
962         var buf = this.window.buffers.current;
963         var modes = buf.enabled_modes;
964         var str = modes.map(
965             function (x) {
966                 let y = mode_display_names[x];
967                 if (y)
968                     return y;
969                 else
970                     return null;
971             }).filter(function (x) x != null).join(" ");
972         this.element.value = str;
973     },
974     uninstall: function () {
975         remove_hook.call(this.window, "select_buffer_hook", this.hook_fun);
976         remove_hook.call(this.window, "current_buffer_mode_change_hook", this.hook_fun);
977         this.element.parentNode.removeChild(this.element);
978     }
980 define_global_window_mode("minibuffer_mode_indicator", "window_initialize_hook");
981 minibuffer_mode_indicator_mode(true);
985  * minibuffer-keymaps-display
986  */
987 function minibuffer_keymaps_display_update (buffer) {
988     var element = buffer.window.document
989         .getElementById("keymaps-display");
990     if (element) {
991         var str = buffer.keymaps.reduce(
992             function (acc, kmap) {
993                 if (kmap.display_name)
994                     acc.push(kmap.display_name);
995                 return acc;
996             }, []).join("/");
997         if (element.value != str)
998             element.value = str;
999     }
1002 function minibuffer_keymaps_display_initialize (window) {
1003     var element = create_XUL(window, "label");
1004     element.setAttribute("id", "keymaps-display");
1005     element.setAttribute("class", "mode-text-widget");
1006     element.setAttribute("value", "");
1007     var mb = window.document.getElementById("minibuffer");
1008     mb.appendChild(element);
1011 define_global_mode("minibuffer_keymaps_display_mode",
1012     function enable () {
1013         add_hook("window_initialize_hook", minibuffer_keymaps_display_initialize);
1014         add_hook("set_input_mode_hook", minibuffer_keymaps_display_update);
1015         for_each_window(minibuffer_keymaps_display_initialize);
1016     },
1017     function disable () {
1018         remove_hook("window_initialize_hook", minibuffer_keymaps_display_initialize);
1019         remove_hook("set_input_mode_hook", minibuffer_keymaps_display_update);
1020         for_each_window(function (w) {
1021             var element = w.document
1022                 .getElementById("keymaps-display");
1023             if (element)
1024                 element.parentNode.removeChild(element);
1025         });
1026     });
1028 minibuffer_keymaps_display_mode(true);
1032  * minibuffer-keymaps-highlight
1033  */
1034 function minibuffer_keymaps_highlight_update (buffer) {
1035     var mb = buffer.window.document.getElementById("minibuffer");
1036     if (buffer.keymaps.some(function (k) k.notify))
1037         dom_add_class(mb, "highlight");
1038     else
1039         dom_remove_class(mb, "highlight");
1042 define_global_mode("minibuffer_keymaps_highlight_mode",
1043     function enable () {
1044         add_hook("set_input_mode_hook", minibuffer_keymaps_highlight_update);
1045     },
1046     function disable () {
1047         remove_hook("set_input_mode_hook", minibuffer_keymaps_highlight_update);
1048         for_each_window(function (w) {
1049             var mb = w.document.getElementById("minibuffer");
1050             if (mb)
1051                 dom_remove_class("highlight");
1052         });
1053     });
1055 minibuffer_keymaps_highlight_mode(true);
1058 provide("buffer");