point: buffer-outer-container overflow:hidden, buffer-container manually sized
[conkeror.git] / modules / content-buffer.js
blob3e0e685f22d842229f4fc8ee7f8524a4ae2e748d
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2009,2011-2012 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 require("buffer.js");
11 require("load-spec.js");
12 require("history.js");
14 define_variable("homepage", "chrome://conkeror-help/content/help.html",
15                 "The url loaded by default for new content buffers.");
17 define_buffer_local_hook("content_buffer_finished_loading_hook");
18 define_buffer_local_hook("content_buffer_started_loading_hook");
19 define_buffer_local_hook("content_buffer_progress_change_hook");
20 define_buffer_local_hook("content_buffer_location_change_hook");
21 define_buffer_local_hook("content_buffer_status_change_hook");
22 define_buffer_local_hook("content_buffer_focus_change_hook");
23 define_buffer_local_hook("content_buffer_dom_link_added_hook");
24 define_buffer_local_hook("content_buffer_popup_blocked_hook");
26 define_current_buffer_hook("current_content_buffer_finished_loading_hook", "content_buffer_finished_loading_hook");
27 define_current_buffer_hook("current_content_buffer_progress_change_hook", "content_buffer_progress_change_hook");
28 define_current_buffer_hook("current_content_buffer_location_change_hook", "content_buffer_location_change_hook");
29 define_current_buffer_hook("current_content_buffer_status_change_hook", "content_buffer_status_change_hook");
30 define_current_buffer_hook("current_content_buffer_focus_change_hook", "content_buffer_focus_change_hook");
33 function content_buffer_modality (buffer) {
34     var elem = buffer.focused_element;
35     function push_keymaps (tag) {
36         buffer.content_modalities.map(
37             function (m) {
38                 if (m[tag])
39                     buffer.keymaps.push(m[tag]);
40             });
41         return null;
42     }
43     push_keymaps('normal');
44     if (elem) {
45         let p = elem.parentNode;
46         while (p && !(p instanceof Ci.nsIDOMHTMLFormElement))
47             p = p.parentNode;
48         if (p)
49             push_keymaps('form');
50     }
51     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
52         var type = (elem.getAttribute("type") || "").toLowerCase();
53         if ({checkbox:1, radio:1, submit:1, reset:1}[type])
54             return push_keymaps(type);
55         else
56             return push_keymaps('text');
57     }
58     if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
59         return push_keymaps('textarea');
60     if (elem instanceof Ci.nsIDOMHTMLSelectElement)
61         return push_keymaps('select');
62     if (elem instanceof Ci.nsIDOMHTMLAnchorElement)
63         return push_keymaps('anchor');
64     if (elem instanceof Ci.nsIDOMHTMLButtonElement)
65         return push_keymaps('button');
66     if (elem instanceof Ci.nsIDOMHTMLEmbedElement ||
67         elem instanceof Ci.nsIDOMHTMLObjectElement)
68     {
69         return push_keymaps('embed');
70     }
71     var frame = buffer.focused_frame;
72     if (frame && frame.document.designMode &&
73         frame.document.designMode == "on")
74     {
75         return push_keymaps('richedit');
76     }
77     while (elem) {
78         switch (elem.contentEditable) {
79         case "true":
80             return push_keymaps('richedit');
81         case "false":
82             return null;
83         default: // "inherit"
84             elem = elem.parentNode;
85         }
86     }
87     return null;
91 define_keywords("$load");
92 function content_buffer (window) {
93     keywords(arguments);
94     this.constructor_begin();
95     try {
96         conkeror.buffer.call(this, window, forward_keywords(arguments));
98         this.browser.addProgressListener(this);
99         var buffer = this;
100         this.browser.addEventListener("DOMTitleChanged", function (event) {
101             buffer_title_change_hook.run(buffer);
102         }, true /* capture */);
104         this.browser.addEventListener("scroll", function (event) {
105             buffer_scroll_hook.run(buffer);
106         }, true /* capture */);
108         this.browser.addEventListener("focus", function (event) {
109             content_buffer_focus_change_hook.run(buffer, event);
110         }, true /* capture */);
112         this.browser.addEventListener("DOMLinkAdded", function (event) {
113             content_buffer_dom_link_added_hook.run(buffer, event);
114         }, true /* capture */);
116         this.browser.addEventListener("DOMPopupBlocked", function (event) {
117             dumpln("Blocked popup: " + event.popupWindowURI.spec);
118             content_buffer_popup_blocked_hook.run(buffer, event);
119         }, true /* capture */);
121         this.page_modes = [];
123         this.ignore_initial_blank = true;
125         var lspec = arguments.$load;
126         if (lspec) {
127             if (load_spec_uri_string(lspec) == "about:blank")
128                 this.ignore_initial_blank = false;
129             else {
130                 /* Ensure that an existing load of about:blank is stopped */
131                 this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
133                 this.load(lspec);
134             }
135         }
137         this.modalities.push(content_buffer_modality);
138         this.content_modalities = [{
139             normal: content_buffer_normal_keymap,
140             form: content_buffer_form_keymap,
141             checkbox: content_buffer_checkbox_keymap,
142             radio: content_buffer_checkbox_keymap,
143             submit: content_buffer_button_keymap,
144             reset: content_buffer_button_keymap,
145             text: content_buffer_text_keymap,
146             textarea: content_buffer_textarea_keymap,
147             select: content_buffer_select_keymap,
148             anchor: content_buffer_anchor_keymap,
149             button: content_buffer_button_keymap,
150             embed: content_buffer_embed_keymap,
151             richedit: content_buffer_richedit_keymap
152         }];
153     } finally {
154         this.constructor_end();
155     }
157 content_buffer.prototype = {
158     constructor: content_buffer,
159     toString: function () "#<content_buffer>",
160     QueryInterface: generate_QI(Ci.nsISHistoryListener,
161                                 Ci.nsIWebProgressListener,
162                                 Ci.nsISupportsWeakReference),
164     destroy: function () {
165         this.browser.removeProgressListener(this);
166         buffer.prototype.destroy.call(this);
167     },
169     content_modalities: null,
171     get scrollX () { return this.top_frame.scrollX; },
172     get scrollY () { return this.top_frame.scrollY; },
173     get scrollMaxX () { return this.top_frame.scrollMaxX; },
174     get scrollMaxY () { return this.top_frame.scrollMaxY; },
176     /* Used to display the correct URI when the buffer opens initially
177      * even before loading has progressed far enough for currentURI to
178      * contain the correct URI. */
179     _display_uri: null,
181     get display_uri_string () {
182         if (this._display_uri)
183             return this._display_uri;
184         if (this.current_uri)
185             return this.current_uri.spec;
186         return "";
187     },
189     get title () { return this.browser.contentTitle; },
190     get description () { return this.display_uri_string; },
192     load: function (load_spec) {
193         apply_load_spec(this, load_spec);
194     },
196     _request_count: 0,
198     loading: false,
200     // nsIWebProgressListener interface
201     //
203     // This method is called to indicate state changes.
204     onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
205         if (!aRequest)
206             return;
207         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
208             this._request_count++;
209         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
210             const NS_ERROR_UNKNOWN_HOST = 2152398878;
211             if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
212                 // to prevent bug 235825: wait for the request handled
213                 // by the automatic keyword resolver
214                 return;
215             }
216             // since we (try to) only handle STATE_STOP of the last request,
217             // the count of open requests should now be 0
218             this._request_count = 0;
219         }
221         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
222             aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
223             // It's okay to clear what the user typed when we start
224             // loading a document. If the user types, this counter gets
225             // set to zero, if the document load ends without an
226             // onLocationChange, this counter gets decremented
227             // (so we keep it while switching tabs after failed loads)
228             this.loading = true;
229             content_buffer_started_loading_hook.run(this);
230         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
231                    aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
232             if (this.loading == true) {
233                 this.loading = false;
234                 content_buffer_finished_loading_hook.run(this);
235             }
236         }
238         if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
239                            Ci.nsIWebProgressListener.STATE_START)) {
240             if (!this.loading)
241                 this.set_default_message("Done");
242         }
243     },
245     // This method is called to indicate progress changes for the
246     // currently loading page.
247     onProgressChange: function (webProgress, request, curSelf, maxSelf,
248                                 curTotal, maxTotal)
249     {
250         content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
251     },
253     // This method is called to indicate a change to the current location.
254     // The url can be gotten as location.spec.
255     onLocationChange: function (webProgress, request, location) {
256         /* Attempt to ignore onLocationChange calls due to the initial
257          * loading of about:blank by all xul:browser elements. */
258         if (location.spec == "about:blank" && this.ignore_initial_blank)
259             return;
261         this.ignore_initial_blank = false;
263         //dumpln("spec: " + location.spec  +" ;;; " + this.display_uri_string);
264         /* Use the real location URI now */
265         this._display_uri = null;
266         this.set_input_mode();
267         this.page = {
268             local: { __proto__: this.local }
269         };
270         this.default_browser_object_classes = {};
271         content_buffer_location_change_hook.run(this, request, location);
272         buffer_description_change_hook.run(this);
273     },
275     // This method is called to indicate a status changes for the currently
276     // loading page.  The message is already formatted for display.
277     // Status messages could be displayed in the minibuffer output area.
278     onStatusChange: function (webProgress, request, status, msg) {
279         this.set_default_message(msg);
280         content_buffer_status_change_hook.run(this, request, status, msg);
281     },
283     // This method is called when the security state of the browser changes.
284     onSecurityChange: function (webProgress, request, state) {
285         //FIXME: implement this.
286         /*
287         const WPL = Components.interfaces.nsIWebProgressListener;
289         if (state & WPL.STATE_IS_INSECURE) {
290             // update visual indicator
291         } else {
292             var level = "unknown";
293             if (state & WPL.STATE_IS_SECURE) {
294                 if (state & WPL.STATE_SECURE_HIGH)
295                     level = "high";
296                 else if (state & WPL.STATE_SECURE_MED)
297                     level = "medium";
298                 else if (state & WPL.STATE_SECURE_LOW)
299                     level = "low";
300             } else if (state & WPL_STATE_IS_BROKEN) {
301                 level = "mixed";
302             }
303             // provide a visual indicator of the security state here.
304         }
305         */
306     },
308     /* Inherit from buffer */
309     __proto__: buffer.prototype
313 add_hook("current_content_buffer_finished_loading_hook",
314          function (buffer) {
315                  buffer.window.minibuffer.show("Done");
316          });
318 add_hook("current_content_buffer_status_change_hook",
319          function (buffer, request, status, msg) {
320              buffer.set_default_message(msg);
321          });
325 define_variable("read_url_handler_list", [],
326     "A list of handler functions which transform a typed url into a valid " +
327     "url or webjump.  If the typed input is not valid then each function " +
328     "on this list is tried in turn.  The handler function is called with " +
329     "a single string argument and it should return either a string or " +
330     "null.  The result of the first function on the list that returns a " +
331     "string is used in place of the input.");
334  * read_url_make_default_webjump_handler returns a function that
335  * transforms any input into the given webjump.  It should be the last
336  * handler on read_url_handler_list (because any input is
337  * accepted).
338  */
339 function read_url_make_default_webjump_handler (default_webjump) {
340     return function (input) {
341         return default_webjump + " " + input;
342     };
346  * read_url_make_blank_url_handler returns a function that replaces a
347  * blank (empty) input with the given url (or webjump).  The url may
348  * perform some function, eg. "javascript:location.reload()".
349  */
350 function read_url_make_blank_url_handler (url) {
351     return function (input) {
352         if (input.length == 0)
353             return url;
354         return null;
355     };
358 function try_read_url_handlers (input) {
359     var result;
360     for (var i = 0; i < read_url_handler_list.length; ++i) {
361         if ((result = read_url_handler_list[i](input)))
362             return result;
363     }
364     return input;
367 define_variable("url_completion_use_webjumps", true,
368     "Specifies whether URL completion should complete webjumps.");
370 define_variable("url_completion_use_bookmarks", true,
371     "Specifies whether URL completion should complete bookmarks.");
373 define_variable("url_completion_use_history", false,
374     "Specifies whether URL completion should complete using browser "+
375     "history.");
377 define_variable("url_completion_sort_order", "visitcount_descending",
378     "Gives the default sort order for history and bookmark completion.\n"+
379     "The value is given as a string, and the available options include: "+
380     "'none', 'title_ascending', 'date_ascending', 'uri_ascending', "+
381     "'visitcount_ascending', 'keyword_ascending', 'dateadded_ascending', "+
382     "'lastmodified_ascending', 'tags_ascending', and 'annotation_ascending'. "+
383     "For every 'ascending' option, there is a corresponding 'descending' "+
384     "option.  Additionally, with XULRunner 6 and later, the options "+
385     "'frecency_ascending' and 'frecency_descending' are available.  See also "+
386     "<https://developer.mozilla.org/en/NsINavHistoryQueryOptions#Sorting_methods>.");
388 define_variable("minibuffer_read_url_select_initial", true,
389     "Specifies whether a URL  presented in the minibuffer for editing "+
390     "should be selected.  This affects find-alternate-url.");
393 minibuffer_auto_complete_preferences["url"] = true;
394 minibuffer.prototype.read_url = function () {
395     keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
396              $use_webjumps = url_completion_use_webjumps,
397              $use_history = url_completion_use_history,
398              $use_bookmarks = url_completion_use_bookmarks,
399              $sort_order = url_completion_sort_order);
400     var completer = url_completer($use_webjumps = arguments.$use_webjumps,
401         $use_bookmarks = arguments.$use_bookmarks,
402         $use_history = arguments.$use_history,
403         $sort_order = arguments.$sort_order);
404     var result = yield this.read(
405         $prompt = arguments.$prompt,
406         $history = arguments.$history,
407         $completer = completer,
408         $initial_value = arguments.$initial_value,
409         $auto_complete = "url",
410         $select = minibuffer_read_url_select_initial,
411         $match_required = false);
412     if (!possibly_valid_url(result) && !get_webjump(result))
413         result = try_read_url_handlers(result);
414     if (result == "") // well-formedness check. (could be better!)
415         throw ("invalid url or webjump (\""+ result +"\")");
416     yield co_return(load_spec(result));
421  * Overlink Mode
422  */
423 function overlink_update_status (buffer, node) {
424     if (node && node.href.length > 0)
425         buffer.window.minibuffer.show("Link: " + node.href);
426     else
427         buffer.window.minibuffer.clear();
430 function overlink_predicate (node) {
431     while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
432         node = node.parentNode;
433     return node;
436 function overlink_initialize (buffer) {
437     buffer.current_overlink = null;
438     buffer.overlink_mouseover = function (event) {
439         if (buffer != buffer.window.buffers.current ||
440             event.target == buffer.browser)
441         {
442             return;
443         }
444         var node = overlink_predicate(event.target);
445         if (node) {
446             buffer.current_overlink = event.target;
447             overlink_update_status(buffer, node);
448         }
449     };
450     buffer.overlink_mouseout = function (event) {
451         if (buffer != buffer.window.buffers.current)
452             return;
453         if (buffer.current_overlink == event.target) {
454             buffer.current_overlink = null;
455             overlink_update_status(buffer, null);
456         }
457     };
458     buffer.browser.addEventListener("mouseover", buffer.overlink_mouseover, true);
459     buffer.browser.addEventListener("mouseout", buffer.overlink_mouseout, true);
462 define_global_mode("overlink_mode",
463     function enable () {
464         add_hook("create_buffer_hook", overlink_initialize);
465         for_each_buffer(overlink_initialize);
466     },
467     function disable () {
468         remove_hook("create_buffer_hook", overlink_initialize);
469         for_each_buffer(function (b) {
470             b.browser.removeEventListener("mouseover", b.overlink_mouseover, true);
471             b.browser.removeEventListener("mouseout", b.overlink_mouseout, true);
472             delete b.current_overlink;
473             delete b.overlink_mouseover;
474             delete b.overlink_mouseout;
475         });
476     },
477     $doc = "Overlink-mode is a programmable mode for showing information "+
478            "about DOM nodes (such as link URLs) in the minibuffer when "+
479            "hovering with the mouse.");
481 overlink_mode(true);
485  * Navigation Commands
486  */
487 function go_back (b, prefix) {
488     if (prefix < 0)
489         go_forward(b, -prefix);
491     check_buffer(b, content_buffer);
493     if (b.web_navigation.canGoBack) {
494         var hist = b.web_navigation.sessionHistory;
495         var idx = hist.index - prefix;
496         if (idx < 0)
497             idx = 0;
498         b.web_navigation.gotoIndex(idx);
499     } else
500         throw interactive_error("Can't go back");
503 interactive("back",
504     "Go back in the session history for the current buffer.",
505     function (I) {go_back(I.buffer, I.p);});
508 function go_forward (b, prefix) {
509     if (prefix < 0)
510         go_back(b, -prefix);
512     check_buffer(b, content_buffer);
514     if (b.web_navigation.canGoForward) {
515         var hist = b.web_navigation.sessionHistory;
516         var idx = hist.index + prefix;
517         if (idx >= hist.count) idx = hist.count-1;
518         b.web_navigation.gotoIndex(idx);
519     } else
520         throw interactive_error("Can't go forward");
523 interactive("forward",
524             "Go forward in the session history for the current buffer.",
525             function (I) {go_forward(I.buffer, I.p);});
528 function stop_loading (b) {
529     check_buffer(b, content_buffer);
530     b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
533 interactive("stop-loading",
534             "Stop loading the current document.",
535             function (I) {stop_loading(I.buffer);});
538 function reload (b, bypass_cache, element, forced_charset) {
539     check_buffer(b, content_buffer);
540     if (element) {
541         if (element instanceof Ci.nsIDOMHTMLImageElement) {
542             try {
543                 var cache = Cc['@mozilla.org/image/cache;1']
544                     .getService(Ci.imgICache);
545                 cache.removeEntry(make_uri(element.src));
546             } catch (e) {}
547         }
548         element.parentNode.replaceChild(element.cloneNode(true), element);
549     } else if (b.current_uri.spec != b.display_uri_string) {
550         apply_load_spec(b, load_spec({ uri: b.display_uri_string,
551                                        forced_charset: forced_charset }));
552     } else {
553         var flags = bypass_cache == null ?
554             Ci.nsIWebNavigation.LOAD_FLAGS_NONE :
555             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
557         if (! forced_charset && forced_charset_list)
558             forced_charset = predicate_alist_match(forced_charset_list,
559                                                    b.current_uri.spec);
561         if (forced_charset) {
562             try {
563                 var atomservice = Cc['@mozilla.org/atom-service;1']
564                     .getService(Ci.nsIAtomService);
565                 b.web_navigation.documentCharsetInfo.forcedCharset =
566                     atomservice.getAtom(forced_charset);
567             } catch (e) {}
568         }
569         b.web_navigation.reload(flags);
570     }
573 interactive("reload",
574     "Reload the current document.\n" +
575     "If a prefix argument is specified, the cache is bypassed.  If a "+
576     "DOM node is supplied via browser object, that node will be "+
577     "reloaded.",
578     function (I) {
579         check_buffer(I.buffer, content_buffer);
580         var element = yield read_browser_object(I);
581         reload(I.buffer, I.P, element, I.forced_charset);
582     },
583     $browser_object = null);
586  * browserDOMWindow: intercept window opening
587  */
588 function initialize_browser_dom_window (window) {
589     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
590         new browser_dom_window(window);
593 define_variable("browser_default_open_target", OPEN_NEW_BUFFER,
594     "Specifies how new window requests by content pages (e.g. by "+
595     "window.open from JavaScript or by using the target attribute of "+
596     "anchor and form elements) will be handled.  This will generally "+
597     "be `OPEN_NEW_BUFFER', `OPEN_NEW_BUFFER_BACKGROUND', or "+
598     "`OPEN_NEW_WINDOW'.");
601 function browser_dom_window (window) {
602     this.window = window;
603     this.next_target = null;
605 browser_dom_window.prototype = {
606     constructor: browser_dom_window,
607     QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
609     openURI: function (aURI, aOpener, aWhere, aContext) {
610         // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
611         var target = this.next_target;
612         if (target == null || target == FOLLOW_DEFAULT)
613             target = browser_default_open_target;
614         this.next_target = null;
616         /* Determine the opener buffer */
617         var opener = get_buffer_from_frame(this.window, aOpener);
619         switch (browser_default_open_target) {
620         case OPEN_CURRENT_BUFFER:
621             return aOpener.top;
622         case FOLLOW_CURRENT_FRAME:
623             return aOpener;
624         case OPEN_NEW_BUFFER:
625             var buffer = new content_buffer(this.window, $opener = opener);
626             this.window.buffers.current = buffer;
627             return buffer.top_frame;
628         case OPEN_NEW_BUFFER_BACKGROUND:
629             var buffer = new content_buffer(this.window, $opener = opener);
630             return buffer.top_frame;
631         case OPEN_NEW_WINDOW:
632         default: /* shouldn't be needed */
634             /* We don't call make_window here, because that will result
635              * in the URL being loaded as the top-level document,
636              * instead of within a browser buffer.  Instead, we can
637              * rely on Mozilla using browser.chromeURL. */
638             window_set_extra_arguments(
639                 {initial_buffer_creator: buffer_creator(content_buffer, $opener = opener)}
640             );
641             return null;
642         }
643     }
646 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
650  * Page Modes
651  */
653 define_keywords("$test");
654 function page_mode (name, enable, disable) {
655     keywords(arguments);
656     buffer_mode.call(this, name, enable, disable,
657                      forward_keywords(arguments));
658     this.test = make_array(arguments.$test);
660 page_mode.prototype = {
661     constructor: page_mode,
662     __proto__: buffer_mode.prototype,
663     test: null,
664     enable: function (buffer) {
665         buffer_mode.prototype.enable.call(this, buffer);
666         buffer.page_modes.push(this.name);
667         buffer.set_input_mode();
668         return true;
669     },
670     disable: function (buffer) {
671         buffer_mode.prototype.disable.call(this, buffer);
672         var i = buffer.page_modes.indexOf(this.name);
673         if (i > -1)
674             buffer.page_modes.splice(i, 1);
675         buffer.set_input_mode();
676     }
679 function define_page_mode (name, test, enable, disable) {
680     keywords(arguments, $constructor = page_mode);
681     define_buffer_mode(name, enable, disable,
682                        $test = test,
683                        forward_keywords(arguments));
685 ignore_function_for_get_caller_source_code_reference("define_page_mode");
688 define_keywords("$modality");
689 function keymaps_page_mode (name, enable, disable) {
690     keywords(arguments);
691     page_mode.call(this, name, enable, disable,
692                    forward_keywords(arguments));
693     this.modality = arguments.$modality;
695 keymaps_page_mode.prototype = {
696     constructor: keymaps_page_mode,
697     __proto__: page_mode.prototype,
698     modality: null,
699     _enable: function (buffer) {
700         buffer.content_modalities.push(this.modality);
701     },
702     _disable: function (buffer) {
703         var i = buffer.content_modalities.indexOf(this.modality);
704         if (i > -1)
705             buffer.content_modalities.splice(i, 1);
706     }
708 function define_keymaps_page_mode (name, test, modality) {
709     keywords(arguments, $constructor = keymaps_page_mode);
710     define_buffer_mode(name, null, null,
711                        $test = test,
712                        $modality = modality,
713                        forward_keywords(arguments));
715 ignore_function_for_get_caller_source_code_reference("define_keymaps_page_mode");
718 var active_page_modes = [];
720 function page_mode_activate (page_mode) {
721     var i = active_page_modes.indexOf(page_mode.name);
722     if (i == -1)
723         active_page_modes.push(page_mode.name);
726 function page_mode_deactivate (page_mode) {
727     var i = active_page_modes.indexOf(page_mode.name);
728     if (i > -1)
729         active_page_modes.splice(i, 1);
733 function page_mode_update (buffer) {
734     for (var i = buffer.page_modes.length - 1; i >= 0; --i) {
735         var p = buffer.page_modes[i];
736         conkeror[p].disable(buffer);
737     }
738     var uri = buffer.current_uri;
739     for each (var name in active_page_modes) {
740         var m = conkeror[name];
741         m.test.some(
742             function (test) {
743                 if (test instanceof RegExp) {
744                     if (test.exec(uri.spec))
745                         return m.enable(buffer);
746                 } else if (test(uri))
747                     return m.enable(buffer);
748                 return false;
749             });
750     }
753 add_hook("content_buffer_location_change_hook", page_mode_update);
755 provide("content-buffer");