new-tabs: fix bug in tab_bar_kill_buffer
[conkeror.git] / modules / buffer.js
blobf4b59829270d20c0f4e62f6357bb48a8d855ba11
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_hook");
30 define_buffer_local_hook("kill_buffer_hook");
31 define_buffer_local_hook("buffer_scroll_hook");
32 define_buffer_local_hook("buffer_dom_content_loaded_hook");
33 define_buffer_local_hook("buffer_loaded_hook");
34 define_buffer_local_hook("set_input_mode_hook");
36 define_current_buffer_hook("current_buffer_title_change_hook", "buffer_title_change_hook");
37 define_current_buffer_hook("current_buffer_description_change_hook", "buffer_description_change_hook");
38 define_current_buffer_hook("current_buffer_icon_change_hook", "buffer_icon_change_hook");
39 define_current_buffer_hook("current_buffer_scroll_hook", "buffer_scroll_hook");
40 define_current_buffer_hook("current_buffer_dom_content_loaded_hook", "buffer_dom_content_loaded_hook");
43 define_keywords("$opener");
44 function buffer_creator (type) {
45     var args = forward_keywords(arguments);
46     return function (window) {
47         return new type(window, args);
48     };
51 define_variable("allow_browser_window_close", true,
52     "If this is set to true, if a content buffer page calls " +
53     "window.close() from JavaScript and is not prevented by the " +
54     "normal Mozilla mechanism that restricts pages from closing " +
55     "a window that was not opened by a script, the buffer will be " +
56     "killed, deleting the window as well if it is the only buffer.");
58 function buffer_modality (buffer) {
59     buffer.keymaps.push(default_global_keymap);
62 function buffer (window) {
63     this.constructor_begin();
64     keywords(arguments);
65     this.opener = arguments.$opener;
66     this.window = window;
67     var element = create_XUL(window, "vbox");
68     element.setAttribute("flex", "1");
69     var browser = create_XUL(window, "browser");
70     if (window.buffers.count == 0)
71         browser.setAttribute("type", "content-primary");
72     else
73         browser.setAttribute("type", "content");
74     browser.setAttribute("flex", "1");
75     browser.setAttribute("autocompletepopup", "popup_autocomplete");
76     element.appendChild(browser);
77     this.window.buffers.container.appendChild(element);
78     this.window.buffers.buffer_list.push(this);
79     this.element = element;
80     this.browser = element.firstChild;
81     this.element.conkeror_buffer_object = this;
83     this.local = { __proto__: conkeror };
84     this.page = null;
85     this.enabled_modes = [];
86     this.default_browser_object_classes = {};
88     var buffer = this;
90     this.browser.addEventListener("scroll", function (event) {
91             buffer_scroll_hook.run(buffer);
92         }, true /* capture */);
94     this.browser.addEventListener("DOMContentLoaded", function (event) {
95             buffer_dom_content_loaded_hook.run(buffer);
96         }, true /* capture */);
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_hook_early 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         this.buffer_list.splice(this.buffer_list.indexOf(b), 1);
470         this.buffer_list.push(b);
471         this.current = new_buffer;
472         return true;
473     },
475     for_each: function (f) {
476         var count = this.count;
477         for (var i = 0; i < count; ++i)
478             f(this.get_buffer(i));
479     }
482 function buffer_initialize_window_early (window) {
483     /**
484      * Use content_buffer by default to handle an unusual case where
485      * browser.chromeURI is used perhaps.  In general this default
486      * should not be needed.
487      */
488     var create_initial_buffer =
489         window.args.initial_buffer_creator || buffer_creator(content_buffer);
490     new buffer_container(window, create_initial_buffer);
493 add_hook("window_initialize_early_hook", buffer_initialize_window_early);
496 define_buffer_local_hook("buffer_kill_before_hook", RUN_HOOK_UNTIL_FAILURE);
497 function buffer_before_window_close (window) {
498     var bs = window.buffers;
499     var count = bs.count;
500     for (let i = 0; i < count; ++i) {
501         if (!buffer_kill_before_hook.run(bs.get_buffer(i)))
502             return false;
503     }
504     return true;
506 add_hook("window_before_close_hook", buffer_before_window_close);
508 function buffer_window_close_handler (window) {
509     var bs = window.buffers;
510     var count = bs.count;
511     for (let i = 0; i < count; ++i) {
512         let b = bs.get_buffer(i);
513         b.destroy();
514     }
516 add_hook("window_close_hook", buffer_window_close_handler);
518 /* open/follow targets */
519 const OPEN_CURRENT_BUFFER = 0; // only valid for open if the current
520                                // buffer is a content_buffer.
521 const OPEN_NEW_BUFFER = 1;
522 const OPEN_NEW_BUFFER_BACKGROUND = 2;
523 const OPEN_NEW_WINDOW = 3;
525 const FOLLOW_DEFAULT = 4; // for open, implies OPEN_CURRENT_BUFFER
526 const FOLLOW_CURRENT_FRAME = 5; // for open, implies OPEN_CURRENT_BUFFER
528 var TARGET_PROMPTS = [" in current buffer",
529                       " in new buffer",
530                       " in new buffer (background)",
531                       " in new window",
532                       "",
533                       " in current frame"];
535 var TARGET_NAMES = ["current buffer",
536                     "new buffer",
537                     "new buffer (background)",
538                     "new window",
539                     "default",
540                     "current frame"];
543 function create_buffer (window, creator, target) {
544     switch (target) {
545     case OPEN_NEW_BUFFER:
546         window.buffers.current = creator(window, null);
547         break;
548     case OPEN_NEW_BUFFER_BACKGROUND:
549         creator(window, null);
550         break;
551     case OPEN_NEW_WINDOW:
552         make_window(creator);
553         break;
554     default:
555         throw new Error("invalid target");
556     }
559 let (queued_buffer_creators = null) {
560     function create_buffer_in_current_window (creator, target, focus_existing) {
561         function process_queued_buffer_creators (window) {
562             for (var i = 0; i < queued_buffer_creators.length; ++i) {
563                 var x = queued_buffer_creators[i];
564                 create_buffer(window, x[0], x[1]);
565             }
566             queued_buffer_creators = null;
567         }
569         if (target == OPEN_NEW_WINDOW)
570             throw new Error("invalid target");
571         var window = get_recent_conkeror_window();
572         if (window) {
573             if (focus_existing)
574                 window.focus();
575             create_buffer(window, creator, target);
576         } else if (queued_buffer_creators != null) {
577             queued_buffer_creators.push([creator,target]);
578         } else {
579             queued_buffer_creators = [];
580             window = make_window(creator);
581             add_hook.call(window, "window_initialize_late_hook", process_queued_buffer_creators);
582         }
583     }
588  * Read Buffer
589  */
590 define_variable("read_buffer_show_icons", false,
591     "Boolean which says whether read_buffer should show buffer "+
592     "icons in the completions list.\nNote, setting this variable "+
593     "alone does not cause favicons or other kinds of icons to be "+
594     "fetched.  For that, load the `favicon' (or similar other) "+
595     "library.");
597 minibuffer_auto_complete_preferences["buffer"] = true;
598 define_keywords("$default");
599 minibuffer.prototype.read_buffer = function () {
600     var window = this.window;
601     var buffer = this.window.buffers.current;
602     keywords(arguments, $prompt = "Buffer:",
603              $default = buffer,
604              $history = "buffer");
605     var completer = all_word_completer(
606         $completions = function (visitor) window.buffers.for_each(visitor),
607         $get_string = function (x) x.description,
608         $get_description = function (x) x.title,
609         $get_icon = (read_buffer_show_icons ?
610                      function (x) x.icon : null));
611     var result = yield this.read(
612         $keymap = read_buffer_keymap,
613         $prompt = arguments.$prompt,
614         $history = arguments.$history,
615         $completer = completer,
616         $enable_icons = read_buffer_show_icons,
617         $match_required = true,
618         $auto_complete = "buffer",
619         $auto_complete_initial = true,
620         $auto_complete_delay = 0,
621         $default_completion = arguments.$default);
622     yield co_return(result);
626 function buffer_next (window, count) {
627     var index = window.buffers.selected_index;
628     var total = window.buffers.count;
629     if (total == 1)
630         throw new interactive_error("No other buffer");
631     index = (index + count) % total;
632     if (index < 0)
633         index += total;
634     window.buffers.current = window.buffers.get_buffer(index);
636 interactive("buffer-next",
637     "Switch to the next buffer.",
638     function (I) { buffer_next(I.window, I.p); });
639 interactive("buffer-previous",
640     "Switch to the previous buffer.",
641     function (I) { buffer_next(I.window, -I.p); });
643 function switch_to_buffer (window, buffer) {
644     if (buffer && !buffer.dead)
645         window.buffers.current = buffer;
647 interactive("switch-to-buffer",
648     "Switch to a buffer specified in the minibuffer.",
649     function (I) {
650         switch_to_buffer(
651             I.window,
652             (yield I.minibuffer.read_buffer(
653                 $prompt = "Switch to buffer:",
654                 $default = (I.window.buffers.count > 1 ?
655                             I.window.buffers.buffer_list[1] :
656                             I.buffer))));
657     });
659 define_variable("can_kill_last_buffer", true,
660     "If this is set to true, kill-buffer can kill the last "+
661     "remaining buffer, and close the window.");
663 function kill_other_buffers (buffer) {
664     if (!buffer)
665         return;
666     var bs = buffer.window.buffers;
667     var b;
668     while ((b = bs.get_buffer(0)) != buffer)
669         bs.kill_buffer(b);
670     var count = bs.count;
671     while (--count)
672         bs.kill_buffer(bs.get_buffer(1));
674 interactive("kill-other-buffers",
675     "Kill all buffers except current one.\n",
676     function (I) { kill_other_buffers(I.buffer); });
679 function kill_buffer (buffer, force) {
680     if (!buffer)
681         return;
682     var buffers = buffer.window.buffers;
683     if (buffers.count == 1 && buffer == buffers.current) {
684         if (can_kill_last_buffer || force) {
685             delete_window(buffer.window);
686             return;
687         } else
688             throw interactive_error("Can't kill last buffer.");
689     }
690     buffers.kill_buffer(buffer);
692 interactive("kill-buffer",
693     "Kill a buffer specified in the minibuffer.\n" +
694     "If `can_kill_last_buffer' is set to true, an attempt to kill the "+
695     "last remaining buffer in a window will cause the window to be closed.",
696     function (I) {
697         kill_buffer((yield I.minibuffer.read_buffer($prompt = "Kill buffer:")));
698     });
700 interactive("kill-current-buffer",
701     "Kill the current buffer.\n" +
702     "If `can_kill_last_buffer' is set to true, an attempt to kill the "+
703     "last remaining buffer in a window will cause the window to be closed.",
704     function (I) { kill_buffer(I.buffer); });
706 interactive("read-buffer-kill-buffer",
707     "Kill the current selected buffer in the completions list "+
708     "in a read buffer minibuffer interaction.",
709     function (I) {
710         var s = I.window.minibuffer.current_state;
711         var i = s.selected_completion_index;
712         var c = s.completions;
713         if (i == -1)
714             return;
715         kill_buffer(c.get_value(i));
716         s.completer.refresh();
717         s.handle_input(I.window.minibuffer);
718     });
720 interactive("bury-buffer",
721     "Bury the current buffer.\n Put the current buffer at the end of " +
722     "the buffer list, so that it is the least likely buffer to be " +
723     "selected by `switch-to-buffer'.",
724     function (I) { I.window.buffers.bury_buffer(I.buffer); });
726 function change_directory (buffer, dir) {
727     if (buffer.page != null)
728         delete buffer.page.local.cwd;
729     buffer.local.cwd = make_file(dir);
731 interactive("change-directory",
732     "Change the current directory of the selected buffer.",
733     function (I) {
734         change_directory(
735             I.buffer,
736             (yield I.minibuffer.read_existing_directory_path(
737                 $prompt = "Change to directory:",
738                 $initial_value = make_file(I.local.cwd).path)));
739     });
741 interactive("shell-command", null,
742     function (I) {
743         var cwd = I.local.cwd;
744         var cmd = (yield I.minibuffer.read_shell_command($cwd = cwd));
745         yield shell_command(cmd, $cwd = cwd);
746     });
750  * selection_is_embed_p is used to test whether the unfocus command can
751  * unfocus an element, even though there is a selection.  This happens
752  * when the focused element is an html:embed.
753  */
754 function selection_is_embed_p (sel, focused_element) {
755     if (sel.rangeCount == 1) {
756         try {
757             var r = sel.getRangeAt(0);
758             var a = r.startContainer.childNodes[r.startOffset];
759             if ((a instanceof Ci.nsIDOMHTMLEmbedElement ||
760                  a instanceof Ci.nsIDOMHTMLObjectElement) &&
761                 a == focused_element)
762             {
763                 return true;
764             }
765         } catch (e) {}
766     }
767     return false;
771  * unfocus is a high-level command for unfocusing hyperlinks, inputs,
772  * frames, iframes, plugins, and also clearing the selection.
773  */
774 define_buffer_local_hook("unfocus_hook");
775 function unfocus (window, buffer) {
776     // 1. if there is a selection, clear it.
777     var selc = buffer.focused_selection_controller;
778     if (selc) {
779         var sel = selc.getSelection(selc.SELECTION_NORMAL);
780         var active = ! sel.isCollapsed;
781         var embed_p = selection_is_embed_p(sel, buffer.focused_element);
782         clear_selection(buffer);
783         if (active && !embed_p) {
784             window.minibuffer.message("cleared selection");
785             return;
786         }
787     }
788     // 2. if there is a focused element, unfocus it.
789     if (buffer.focused_element) {
790         buffer.focused_element.blur();
791         // if an element in a detached fragment has focus, blur() will
792         // not work, and we need to take more drastic measures.  the
793         // action taken was found through experiment, so it is possibly
794         // not the most concise way to unfocus such an element.
795         if (buffer.focused_element) {
796             buffer.element.focus();
797             buffer.top_frame.focus();
798         }
799         window.minibuffer.message("unfocused element");
800         return;
801     }
802     // 3. if an iframe has focus, we must blur it.
803     if (buffer.focused_frame_or_null &&
804         buffer.focused_frame_or_null.frameElement)
805     {
806         buffer.focused_frame_or_null.frameElement.blur();
807     }
808     // 4. return focus to top-frame from subframes and plugins.
809     buffer.top_frame.focus();
810     buffer.top_frame.focus(); // needed to get focus back from plugins
811     window.minibuffer.message("refocused top frame");
812     // give page-modes an opportunity to set focus specially
813     unfocus_hook.run(buffer);
815 interactive("unfocus",
816     "Unfocus is a high-level command for unfocusing hyperlinks, inputs, "+
817     "frames, iframes, plugins, and also for clearing the selection.\n"+
818     "The action that it takes is based on precedence.  If there is a "+
819     "focused hyperlink or input, it will unfocus that.  Otherwise, if "+
820     "there is a selection, it will clear the selection.  Otherwise, it "+
821     "will return focus to the top frame from a focused frame, iframe, "+
822     "or plugin.  In the case of plugins, since they steal keyboard "+
823     "control away from Conkeror, the normal way to unfocus them is "+
824     "to use command-line remoting externally: conkeror -batch -f "+
825     "unfocus.  Page-modes also have an opportunity to alter the default"+
826     "focus via the hook, `focus_hook'.",
827     function (I) {
828         unfocus(I.window, I.buffer);
829     });
832 function for_each_buffer (f) {
833     for_each_window(function (w) { w.buffers.for_each(f); });
838  * BUFFER MODES
839  */
841 var mode_functions = {};
842 var mode_display_names = {};
844 define_buffer_local_hook("buffer_mode_change_hook");
845 define_current_buffer_hook("current_buffer_mode_change_hook", "buffer_mode_change_hook");
847 define_keywords("$display_name", "$class", "$enable", "$disable", "$doc");
848 function define_buffer_mode (name) {
849     keywords(arguments);
851     var hyphen_name = name.replace("_","-","g");
852     var display_name = arguments.$display_name;
853     var mode_class = arguments.$class;
854     var enable = arguments.$enable;
855     var disable = arguments.$disable;
857     mode_display_names[name] = display_name;
859     var can_disable;
861     if (disable == false) {
862         can_disable = false;
863         disable = null;
864     } else
865         can_disable = true;
867     var state = (mode_class != null) ? mode_class : (name + "_enabled");
868     var enable_hook_name = name + "_enable_hook";
869     var disable_hook_name = name + "_disable_hook";
870     define_buffer_local_hook(enable_hook_name);
871     define_buffer_local_hook(disable_hook_name);
873     var change_hook_name = null;
875     if (mode_class) {
876         mode_functions[name] = { enable: enable,
877                                  disable: disable,
878                                  mode_class: mode_class,
879                                  disable_hook_name: disable_hook_name };
880         change_hook_name = mode_class + "_change_hook";
881         define_buffer_local_hook(change_hook_name);
882     }
884     function func (buffer, arg) {
885         var old_state = buffer[state];
886         var cur_state = (old_state == name);
887         var new_state = (arg == null) ? !cur_state : (arg > 0);
888         if ((new_state == cur_state) || (!can_disable && !new_state))
889             // perhaps show a message if (!can_disable && !new_state)
890             // to tell the user that this mode cannot be disabled.  do
891             // we have any existing modes that would benefit by it?
892             return null;
893         if (new_state) {
894             if (mode_class && old_state != null)  {
895                 // Another buffer-mode of our same mode-class is
896                 // enabled.  Buffer-modes within a mode-class are
897                 // mutually exclusive, so turn the old one off.
898                 buffer.enabled_modes.splice(buffer.enabled_modes.indexOf(old_state), 1);
899                 let x = mode_functions[old_state];
900                 let y = x.disable;
901                 if (y) y(buffer);
902                 conkeror[x.disable_hook_name].run(buffer);
903             }
904             buffer[state] = name;
905             if (enable)
906                 enable(buffer);
907             conkeror[enable_hook_name].run(buffer);
908             buffer.enabled_modes.push(name);
909         } else {
910             buffer.enabled_modes.splice(buffer.enabled_modes.indexOf(name), 1);
911             disable(buffer);
912             conkeror[disable_hook_name].run(buffer);
913             buffer[state] = null;
914         }
915         if (change_hook_name)
916             conkeror[change_hook_name].run(buffer, buffer[state]);
917         buffer_mode_change_hook.run(buffer);
918         return new_state;
919     }
921     conkeror[name] = func;
922     interactive(hyphen_name, arguments.$doc, function (I) {
923         var arg = I.P;
924         var new_state = func(I.buffer, arg && univ_arg_to_number(arg));
925         I.minibuffer.message(hyphen_name + (new_state ? " enabled" : " disabled"));
926     });
928 ignore_function_for_get_caller_source_code_reference("define_buffer_mode");
931 function minibuffer_mode_indicator (window) {
932     this.window = window;
933     var element = create_XUL(window, "label");
934     element.setAttribute("id", "minibuffer-mode-indicator");
935     element.setAttribute("class", "mode-text-widget");
936     window.document.getElementById("minibuffer").appendChild(element);
937     this.element = element;
938     this.hook_func = method_caller(this, this.update);
939     add_hook.call(window, "select_buffer_hook", this.hook_func);
940     add_hook.call(window, "current_buffer_mode_change_hook", this.hook_func);
941     this.update();
943 minibuffer_mode_indicator.prototype = {
944     constructor: minibuffer_mode_indicator,
945     update: function () {
946         var buf = this.window.buffers.current;
947         var modes = buf.enabled_modes;
948         var str = modes.map(
949             function (x) {
950                 let y = mode_display_names[x];
951                 if (y)
952                     return y;
953                 else
954                     return null;
955             }).filter(function (x) x != null).join(" ");
956         this.element.value = str;
957     },
958     uninstall: function () {
959         remove_hook.call(this.window, "select_buffer_hook", this.hook_fun);
960         remove_hook.call(this.window, "current_buffer_mode_change_hook", this.hook_fun);
961         this.element.parentNode.removeChild(this.element);
962     }
964 define_global_window_mode("minibuffer_mode_indicator", "window_initialize_hook");
965 minibuffer_mode_indicator_mode(true);
969  * minibuffer-keymaps-display
970  */
971 function minibuffer_keymaps_display_update (buffer) {
972     var element = buffer.window.document
973         .getElementById("keymaps-display");
974     if (element) {
975         var str = buffer.keymaps.reduce(
976             function (acc, kmap) {
977                 if (kmap.display_name)
978                     acc.push(kmap.display_name);
979                 return acc;
980             }, []).join("/");
981         if (element.value != str)
982             element.value = str;
983     }
986 function minibuffer_keymaps_display_initialize (window) {
987     var element = create_XUL(window, "label");
988     element.setAttribute("id", "keymaps-display");
989     element.setAttribute("class", "mode-text-widget");
990     element.setAttribute("value", "");
991     var mb = window.document.getElementById("minibuffer");
992     mb.appendChild(element);
995 define_global_mode("minibuffer_keymaps_display_mode",
996     function enable () {
997         add_hook("window_initialize_hook", minibuffer_keymaps_display_initialize);
998         add_hook("set_input_mode_hook", minibuffer_keymaps_display_update);
999         for_each_window(minibuffer_keymaps_display_initialize);
1000     },
1001     function disable () {
1002         remove_hook("window_initialize_hook", minibuffer_keymaps_display_initialize);
1003         remove_hook("set_input_mode_hook", minibuffer_keymaps_display_update);
1004         for_each_window(function (w) {
1005             var element = w.document
1006                 .getElementById("keymaps-display");
1007             if (element)
1008                 element.parentNode.removeChild(element);
1009         });
1010     });
1012 minibuffer_keymaps_display_mode(true);
1016  * minibuffer-keymaps-highlight
1017  */
1018 function minibuffer_keymaps_highlight_update (buffer) {
1019     var mb = buffer.window.document.getElementById("minibuffer");
1020     if (buffer.keymaps.some(function (k) k.notify))
1021         dom_add_class(mb, "highlight");
1022     else
1023         dom_remove_class(mb, "highlight");
1026 define_global_mode("minibuffer_keymaps_highlight_mode",
1027     function enable () {
1028         add_hook("set_input_mode_hook", minibuffer_keymaps_highlight_update);
1029     },
1030     function disable () {
1031         remove_hook("set_input_mode_hook", minibuffer_keymaps_highlight_update);
1032         for_each_window(function (w) {
1033             var mb = w.document.getElementById("minibuffer");
1034             if (mb)
1035                 dom_remove_class("highlight");
1036         });
1037     });
1039 minibuffer_keymaps_highlight_mode(true);
1042 provide("buffer");