new-tabs: fix bug in tab_bar_kill_buffer
[conkeror.git] / modules / content-buffer.js
blob46ece7b6af0f0bf3b5a5b1dd07d00ce98701ba07
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2009 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 require("buffer.js");
13 require("load-spec.js");
15 define_variable("homepage", "chrome://conkeror-help/content/help.html",
16                 "The url loaded by default for new content buffers.");
18 define_buffer_local_hook("content_buffer_finished_loading_hook");
19 define_buffer_local_hook("content_buffer_started_loading_hook");
20 define_buffer_local_hook("content_buffer_progress_change_hook");
21 define_buffer_local_hook("content_buffer_location_change_hook");
22 define_buffer_local_hook("content_buffer_status_change_hook");
23 define_buffer_local_hook("content_buffer_focus_change_hook");
24 define_buffer_local_hook("content_buffer_dom_link_added_hook");
25 define_buffer_local_hook("content_buffer_popup_blocked_hook");
27 define_current_buffer_hook("current_content_buffer_finished_loading_hook", "content_buffer_finished_loading_hook");
28 define_current_buffer_hook("current_content_buffer_progress_change_hook", "content_buffer_progress_change_hook");
29 define_current_buffer_hook("current_content_buffer_location_change_hook", "content_buffer_location_change_hook");
30 define_current_buffer_hook("current_content_buffer_status_change_hook", "content_buffer_status_change_hook");
31 define_current_buffer_hook("current_content_buffer_focus_change_hook", "content_buffer_focus_change_hook");
34 function content_buffer_modality (buffer) {
35     var elem = buffer.focused_element;
36     function push_keymaps (tag) {
37         buffer.content_modalities.map(
38             function (m) {
39                 if (m[tag])
40                     buffer.keymaps.push(m[tag]);
41             });
42         return null;
43     }
44     push_keymaps('normal');
45     if (elem) {
46         let p = elem.parentNode;
47         while (p && !(p instanceof Ci.nsIDOMHTMLFormElement))
48             p = p.parentNode;
49         if (p)
50             push_keymaps('form');
51     }
52     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
53         var type = (elem.getAttribute("type") || "").toLowerCase();
54         if ({checkbox:1, radio:1, submit:1, reset:1}[type])
55             return push_keymaps(type);
56         else
57             return push_keymaps('text');
58     }
59     if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
60         return push_keymaps('textarea');
61     if (elem instanceof Ci.nsIDOMHTMLSelectElement)
62         return push_keymaps('select');
63     if (elem instanceof Ci.nsIDOMHTMLAnchorElement)
64         return push_keymaps('anchor');
65     if (elem instanceof Ci.nsIDOMHTMLButtonElement)
66         return push_keymaps('button');
67     if (elem instanceof Ci.nsIDOMHTMLEmbedElement ||
68         elem instanceof Ci.nsIDOMHTMLObjectElement)
69     {
70         return push_keymaps('embed');
71     }
72     var frame = buffer.focused_frame;
73     if (frame && frame.document.designMode &&
74         frame.document.designMode == "on")
75     {
76         return push_keymaps('richedit');
77     }
78     while (elem) {
79         switch (elem.contentEditable) {
80         case "true":
81             return push_keymaps('richedit');
82         case "false":
83             return null;
84         default: // "inherit"
85             elem = elem.parentNode;
86         }
87     }
88     return null;
92 define_keywords("$load");
93 function content_buffer (window) {
94     keywords(arguments);
95     this.constructor_begin();
96     try {
97         conkeror.buffer.call(this, window, forward_keywords(arguments));
99         this.browser.addProgressListener(this);
100         var buffer = this;
101         this.browser.addEventListener("DOMTitleChanged", function (event) {
102             buffer_title_change_hook.run(buffer);
103         }, true /* capture */);
105         this.browser.addEventListener("scroll", function (event) {
106             buffer_scroll_hook.run(buffer);
107         }, true /* capture */);
109         this.browser.addEventListener("focus", function (event) {
110             content_buffer_focus_change_hook.run(buffer, event);
111         }, true /* capture */);
113         this.browser.addEventListener("DOMLinkAdded", function (event) {
114             content_buffer_dom_link_added_hook.run(buffer, event);
115         }, true /* capture */);
117         this.browser.addEventListener("DOMPopupBlocked", function (event) {
118             dumpln("Blocked popup: " + event.popupWindowURI.spec);
119             content_buffer_popup_blocked_hook.run(buffer, event);
120         }, true /* capture */);
122         this.ignore_initial_blank = true;
124         var lspec = arguments.$load;
125         if (lspec) {
126             if (lspec.url == "about:blank")
127                 this.ignore_initial_blank = false;
128             else {
129                 /* Ensure that an existing load of about:blank is stopped */
130                 this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
132                 this.load(lspec);
133             }
134         }
136         this.modalities.push(content_buffer_modality);
137         this.content_modalities = [{
138             normal: content_buffer_normal_keymap,
139             form: content_buffer_form_keymap,
140             checkbox: content_buffer_checkbox_keymap,
141             radio: content_buffer_checkbox_keymap,
142             submit: content_buffer_button_keymap,
143             reset: content_buffer_button_keymap,
144             text: content_buffer_text_keymap,
145             textarea: content_buffer_textarea_keymap,
146             select: content_buffer_select_keymap,
147             anchor: content_buffer_anchor_keymap,
148             button: content_buffer_button_keymap,
149             embed: content_buffer_embed_keymap,
150             richedit: content_buffer_richedit_keymap
151         }];
152     } finally {
153         this.constructor_end();
154     }
156 content_buffer.prototype = {
157     constructor: content_buffer,
159     destroy: function () {
160         this.browser.removeProgressListener(this);
161         buffer.prototype.destroy.call(this);
162     },
164     content_modalities: null,
166     get scrollX () { return this.top_frame.scrollX; },
167     get scrollY () { return this.top_frame.scrollY; },
168     get scrollMaxX () { return this.top_frame.scrollMaxX; },
169     get scrollMaxY () { return this.top_frame.scrollMaxY; },
171     /* Used to display the correct URI when the buffer opens initially
172      * even before loading has progressed far enough for currentURI to
173      * contain the correct URI. */
174     _display_uri: null,
176     get display_uri_string () {
177         if (this._display_uri)
178             return this._display_uri;
179         if (this.current_uri)
180             return this.current_uri.spec;
181         return "";
182     },
184     get title () { return this.browser.contentTitle; },
185     get description () { return this.display_uri_string; },
187     load: function (load_spec) {
188         apply_load_spec(this, load_spec);
189     },
191     _request_count: 0,
193     loading: false,
195     /* nsIWebProgressListener */
196     QueryInterface: generate_QI(Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference),
198     // This method is called to indicate state changes.
199     onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
201         const WPL = Components.interfaces.nsIWebProgressListener;
203         var flagstr = "";
204         if (aStateFlags & WPL.STATE_START)
205             flagstr += ",start";
206         if (aStateFlags & WPL.STATE_STOP)
207             flagstr += ",stop";
208         if (aStateFlags & WPL.STATE_IS_REQUEST)
209             flagstr += ",request";
210         if (aStateFlags & WPL.STATE_IS_DOCUMENT)
211             flagstr += ",document";
212         if (aStateFlags & WPL.STATE_IS_NETWORK)
213             flagstr += ",network";
214         if (aStateFlags & WPL.STATE_IS_WINDOW)
215             flagstr += ",window";
216         dumpln("onStateChange: " + flagstr + ", status: " + aStatus);
218         if (!aRequest)
219             return;
221         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
222             this._request_count++;
223         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
224             const NS_ERROR_UNKNOWN_HOST = 2152398878;
225             if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
226                 // to prevent bug 235825: wait for the request handled
227                 // by the automatic keyword resolver
228                 return;
229             }
230             // since we (try to) only handle STATE_STOP of the last request,
231             // the count of open requests should now be 0
232             this._request_count = 0;
233         }
235         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
236             aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
237             // It's okay to clear what the user typed when we start
238             // loading a document. If the user types, this counter gets
239             // set to zero, if the document load ends without an
240             // onLocationChange, this counter gets decremented
241             // (so we keep it while switching tabs after failed loads)
242             //dumpln("*** started loading");
243             this.loading = true;
244             content_buffer_started_loading_hook.run(this);
245         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
246                    aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
247             if (this.loading == true) {
248                 //dumpln("*** finished loading");
249                 this.loading = false;
250                 content_buffer_finished_loading_hook.run(this);
251             }
252         }
254         if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
255                            Ci.nsIWebProgressListener.STATE_START)) {
256             if (!this.loading)
257                 this.set_default_message("Done");
258         }
259     },
261     /* This method is called to indicate progress changes for the currently
262        loading page. */
263     onProgressChange: function (webProgress, request, curSelf, maxSelf,
264                                 curTotal, maxTotal) {
265         content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
266     },
268     /* This method is called to indicate a change to the current location.
269        The url can be gotten as location.spec. */
270     onLocationChange: function (webProgress, request, location) {
271         /* Attempt to ignore onLocationChange calls due to the initial
272          * loading of about:blank by all xul:browser elements. */
273         if (location.spec == "about:blank" && this.ignore_initial_blank)
274             return;
276         this.ignore_initial_blank = false;
278         //dumpln("spec: " + location.spec  +" ;;; " + this.display_uri_string);
279         /* Use the real location URI now */
280         this._display_uri = null;
281         this.set_input_mode();
282         content_buffer_location_change_hook.run(this, request, location);
283         buffer_description_change_hook.run(this);
284     },
286     // This method is called to indicate a status changes for the currently
287     // loading page.  The message is already formatted for display.
288     // Status messages could be displayed in the minibuffer output area.
289     onStatusChange: function (webProgress, request, status, msg) {
290         this.set_default_message(msg);
291         content_buffer_status_change_hook.run(this, request, status, msg);
292     },
294     // This method is called when the security state of the browser changes.
295     onSecurityChange: function (webProgress, request, state) {
296         /* FIXME: currently this isn't used */
298         /*
299         const WPL = Components.interfaces.nsIWebProgressListener;
301         if (state & WPL.STATE_IS_INSECURE) {
302             // update visual indicator
303         } else {
304             var level = "unknown";
305             if (state & WPL.STATE_IS_SECURE) {
306                 if (state & WPL.STATE_SECURE_HIGH)
307                     level = "high";
308                 else if (state & WPL.STATE_SECURE_MED)
309                     level = "medium";
310                 else if (state & WPL.STATE_SECURE_LOW)
311                     level = "low";
312             } else if (state & WPL_STATE_IS_BROKEN) {
313                 level = "mixed";
314             }
315             // provide a visual indicator of the security state here.
316         }
317         */
318     },
320     /* Inherit from buffer */
321     __proto__: buffer.prototype
325 add_hook("current_content_buffer_finished_loading_hook",
326          function (buffer) {
327                  buffer.window.minibuffer.show("Done");
328          });
330 add_hook("current_content_buffer_status_change_hook",
331          function (buffer, request, status, msg) {
332              buffer.set_default_message(msg);
333          });
337 define_variable("read_url_handler_list", [],
338     "A list of handler functions which transform a typed url into a valid " +
339     "url or webjump.  If the typed input is not valid then each function " +
340     "on this list is tried in turn.  The handler function is called with " +
341     "a single string argument and it should return either a string or " +
342     "null.  The result of the first function on the list that returns a " +
343     "string is used in place of the input.");
346  * read_url_make_default_webjump_handler returns a function that
347  * transforms any input into the given webjump.  It should be the last
348  * handler on read_url_handler_list (because any input is
349  * accepted).
350  */
351 function read_url_make_default_webjump_handler (default_webjump) {
352     return function (input) {
353         return default_webjump + " " + input;
354     };
358  * read_url_make_blank_url_handler returns a function that replaces a
359  * blank (empty) input with the given url (or webjump).  The url may
360  * perform some function, eg. "javascript:location.reload()".
361  */
362 function read_url_make_blank_url_handler (url) {
363     return function (input) {
364         if (input.length == 0)
365             return url;
366         return null;
367     };
370 minibuffer.prototype.try_read_url_handlers = function (input) {
371     var result;
372     for (var i = 0; i < read_url_handler_list.length; ++i) {
373         if ((result = read_url_handler_list[i](input)))
374             return result;
375     }
376     return input;
379 define_variable("url_completion_use_webjumps", true,
380     "Specifies whether URL completion should complete webjumps.");
382 define_variable("url_completion_use_bookmarks", true,
383     "Specifies whether URL completion should complete bookmarks.");
385 define_variable("url_completion_use_history", false,
386     "Specifies whether URL completion should complete using browser "+
387     "history.");
389 define_variable("minibuffer_read_url_select_initial", true,
390     "Specifies whether a URL  presented in the minibuffer for editing "+
391     "should be selected.  This affects find-alternate-url.");
394 minibuffer_auto_complete_preferences["url"] = true;
395 minibuffer.prototype.read_url = function () {
396     keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
397              $use_webjumps = url_completion_use_webjumps,
398              $use_history = url_completion_use_history,
399              $use_bookmarks = url_completion_use_bookmarks);
400     var completer = url_completer($use_webjumps = arguments.$use_webjumps,
401         $use_bookmarks = arguments.$use_bookmarks,
402         $use_history = arguments.$use_history);
403     var result = yield this.read(
404         $prompt = arguments.$prompt,
405         $history = arguments.$history,
406         $completer = completer,
407         $initial_value = arguments.$initial_value,
408         $auto_complete = "url",
409         $select = minibuffer_read_url_select_initial,
410         $match_required = false);
411     if (!possibly_valid_url(result) && !get_webjump(result))
412         result = this.try_read_url_handlers(result);
413     if (result == "") // well-formedness check. (could be better!)
414         throw ("invalid url or webjump (\""+ result +"\")");
415     yield co_return(load_spec(result));
420  * Overlink Mode
421  */
422 function overlink_update_status (buffer, node) {
423     if (node && node.href.length > 0)
424         buffer.window.minibuffer.show("Link: " + node.href);
425     else
426         buffer.window.minibuffer.show("");
429 function overlink_predicate (node) {
430     while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
431         node = node.parentNode;
432     return node;
435 function overlink_initialize (buffer) {
436     buffer.current_overlink = null;
437     buffer.overlink_mouseover = function (event) {
438         if (buffer != buffer.window.buffers.current)
439             return;
440         var node = overlink_predicate(event.target);
441         if (node) {
442             buffer.current_overlink = event.target;
443             overlink_update_status(buffer, node);
444         }
445     }
446     buffer.overlink_mouseout = function (event) {
447         if (buffer != buffer.window.buffers.current)
448             return;
449         if (buffer.current_overlink == event.target) {
450             buffer.current_overlink = null;
451             overlink_update_status(buffer, null);
452         }
453     }
454     buffer.browser.addEventListener("mouseover", buffer.overlink_mouseover, true);
455     buffer.browser.addEventListener("mouseout", buffer.overlink_mouseout, true);
458 define_global_mode("overlink_mode",
459     function enable () {
460         add_hook("create_buffer_hook", overlink_initialize);
461         for_each_buffer(overlink_initialize);
462     },
463     function disable () {
464         remove_hook("create_buffer_hook", overlink_initialize);
465         for_each_buffer(function (b) {
466             b.browser.removeEventListener("mouseover", b.overlink_mouseover, true);
467             b.browser.removeEventListener("mouseout", b.overlink_mouseout, true);
468             delete b.current_overlink;
469             delete b.overlink_mouseover;
470             delete b.overlink_mouseout;
471         });
472     })
474 overlink_mode(true);
478  * Navigation Commands
479  */
480 function go_back (b, prefix) {
481     if (prefix < 0)
482         go_forward(b, -prefix);
484     check_buffer(b, content_buffer);
486     if (b.web_navigation.canGoBack) {
487         var hist = b.web_navigation.sessionHistory;
488         var idx = hist.index - prefix;
489         if (idx < 0)
490             idx = 0;
491         b.web_navigation.gotoIndex(idx);
492     } else
493         throw interactive_error("Can't go back");
496 interactive("back",
497     "Go back in the session history for the current buffer.",
498     function (I) {go_back(I.buffer, I.p);});
501 function go_forward (b, prefix) {
502     if (prefix < 0)
503         go_back(b, -prefix);
505     check_buffer(b, content_buffer);
507     if (b.web_navigation.canGoForward) {
508         var hist = b.web_navigation.sessionHistory;
509         var idx = hist.index + prefix;
510         if (idx >= hist.count) idx = hist.count-1;
511         b.web_navigation.gotoIndex(idx);
512     } else
513         throw interactive_error("Can't go forward");
516 interactive("forward",
517             "Go forward in the session history for the current buffer.",
518             function (I) {go_forward(I.buffer, I.p);});
521 function stop_loading (b) {
522     check_buffer(b, content_buffer);
523     b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
526 interactive("stop-loading",
527             "Stop loading the current document.",
528             function (I) {stop_loading(I.buffer);});
531 function reload (b, bypass_cache, element, forced_charset) {
532     check_buffer(b, content_buffer);
533     if (element) {
534         if (element instanceof Ci.nsIDOMHTMLImageElement) {
535             try {
536                 var cache = Cc['@mozilla.org/image/cache;1']
537                     .getService(Ci.imgICache);
538                 cache.removeEntry(make_uri(element.src));
539             } catch (e) {}
540         }
541         element.parentNode.replaceChild(element.cloneNode(true), element);
542     } else if (b.current_uri.spec != b.display_uri_string) {
543         apply_load_spec(b, load_spec({ uri: b.display_uri_string,
544                                        forced_charset: forced_charset }));
545     } else {
546         var flags = bypass_cache == null ?
547             Ci.nsIWebNavigation.LOAD_FLAGS_NONE :
548             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
550         if (! forced_charset && forced_charset_list)
551             forced_charset = predicate_alist_match(forced_charset_list,
552                                                    b.current_uri.spec);
554         if (forced_charset) {
555             try {
556                 var atomservice = Cc['@mozilla.org/atom-service;1']
557                     .getService(Ci.nsIAtomService);
558                 b.web_navigation.documentCharsetInfo.forcedCharset =
559                     atomservice.getAtom(forced_charset);
560             } catch (e) {}
561         }
562         b.web_navigation.reload(flags);
563     }
566 interactive("reload",
567     "Reload the current document.\n" +
568     "If a prefix argument is specified, the cache is bypassed.  If a "+
569     "DOM node is supplied via browser object, that node will be "+
570     "reloaded.",
571     function (I) {
572         check_buffer(I.buffer, content_buffer);
573         var element = yield read_browser_object(I);
574         reload(I.buffer, I.P, element, I.forced_charset);
575     },
576     $browser_object = null);
579  * browserDOMWindow: intercept window opening
580  */
581 function initialize_browser_dom_window (window) {
582     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
583         new browser_dom_window(window);
586 define_variable("browser_default_open_target", OPEN_NEW_BUFFER,
587     "Specifies how new window requests by content pages (e.g. by "+
588     "window.open from JavaScript or by using the target attribute of "+
589     "anchor and form elements) will be handled.  This will generally "+
590     "be `OPEN_NEW_BUFFER', `OPEN_NEW_BUFFER_BACKGROUND', or "+
591     "`OPEN_NEW_WINDOW'.");
594 function browser_dom_window (window) {
595     this.window = window;
596     this.next_target = null;
598 browser_dom_window.prototype = {
599     constructor: browser_dom_window,
600     QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
602     openURI: function (aURI, aOpener, aWhere, aContext) {
603         // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
604         var target = this.next_target;
605         if (target == null || target == FOLLOW_DEFAULT)
606             target = browser_default_open_target;
607         this.next_target = null;
609         /* Determine the opener buffer */
610         var opener = get_buffer_from_frame(this.window, aOpener);
612         switch (browser_default_open_target) {
613         case OPEN_CURRENT_BUFFER:
614             return aOpener.top;
615         case FOLLOW_CURRENT_FRAME:
616             return aOpener;
617         case OPEN_NEW_BUFFER:
618             var buffer = new content_buffer(this.window, $opener = opener);
619             this.window.buffers.current = buffer;
620             return buffer.top_frame;
621         case OPEN_NEW_BUFFER_BACKGROUND:
622             var buffer = new content_buffer(this.window, $opener = opener);
623             return buffer.top_frame;
624         case OPEN_NEW_WINDOW:
625         default: /* shouldn't be needed */
627             /* We don't call make_window here, because that will result
628              * in the URL being loaded as the top-level document,
629              * instead of within a browser buffer.  Instead, we can
630              * rely on Mozilla using browser.chromeURL. */
631             window_set_extra_arguments(
632                 {initial_buffer_creator: buffer_creator(content_buffer, $opener = opener)}
633             );
634             return null;
635         }
636     }
639 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
641 define_keywords("$display_name", "$enable", "$disable", "$doc");
642 function define_page_mode (name) {
643     keywords(arguments);
644     var display_name = arguments.$display_name;
645     var enable = arguments.$enable;
646     var disable = arguments.$disable;
647     var doc = arguments.$doc;
648     define_buffer_mode(name,
649                        $display_name = display_name,
650                        $class = "page_mode",
651                        $enable = function (buffer) {
652                            buffer.page = {
653                                local: { __proto__: buffer.local }
654                            };
655                            if (enable)
656                                enable(buffer);
657                            buffer.set_input_mode();
658                        },
659                        $disable = function (buffer) {
660                            if (disable)
661                                disable(buffer);
662                            buffer.page = null;
663                            buffer.default_browser_object_classes = {};
664                            buffer.set_input_mode();
665                        },
666                        $doc = doc);
668 ignore_function_for_get_caller_source_code_reference("define_page_mode");
671 define_variable("auto_mode_list", [],
672     "A list of mappings from URI regular expressions to page modes.");
674 function page_mode_auto_update (buffer) {
675     var uri = buffer.current_uri.spec;
676     var mode = predicate_alist_match(auto_mode_list, uri);
677     if (mode)
678         mode(buffer, true);
679     else if (buffer.page_mode)
680         conkeror[buffer.page_mode](buffer, false);
683 add_hook("content_buffer_location_change_hook", page_mode_auto_update);
685 provide("content-buffer");