Set buffer.loading false before content_buffer_finished_loading_hook
[conkeror.git] / modules / content-buffer.js
blob43d61f31fdcdca76c74077f839968ed1920505ea
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2008 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("load-spec.js");
12 require_later("content-buffer-input.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_overlink_change_hook");
24 define_buffer_local_hook("content_buffer_dom_link_added_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");
31 define_current_buffer_hook("current_content_buffer_overlink_change_hook", "content_buffer_overlink_change_hook");
33 /* If browser is null, create a new browser */
34 define_keywords("$load");
35 function content_buffer(window, element)
37     keywords(arguments);
38     this.constructor_begin();
39     try {
41         conkeror.buffer.call(this, window, element, forward_keywords(arguments));
43         this.browser.addProgressListener(this);
44         var buffer = this;
45         this.browser.addEventListener("DOMTitleChanged", function (event) {
46                                           buffer_title_change_hook.run(buffer);
47                                       }, true /* capture */, false /* ignore untrusted events */);
49         this.browser.addEventListener("scroll", function (event) {
50                                           buffer_scroll_hook.run(buffer);
51                                       }, true /* capture */, false /* ignore untrusted events */);
53         this.browser.addEventListener("focus", function (event) {
54                                           content_buffer_focus_change_hook.run(buffer, event);
55                                       }, true /* capture */, false /* ignore untrusted events */);
57         this.browser.addEventListener("mouseover", function (event) {
58                                           var node = event.target;
59                                           while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
60                                               node = node.parentNode;
61                                           if (node) {
62                                               content_buffer_overlink_change_hook.run(buffer, node.href);
63                                               buffer.current_overlink = event.target;
64                                           }
65                                       }, true, false);
67         this.browser.addEventListener("mouseout", function (event) {
68                                           if (buffer.current_overlink == event.target) {
69                                               buffer.current_overlink = null;
70                                               content_buffer_overlink_change_hook.run(buffer, "");
71                                           }
72                                       }, true, false);
74         this.browser.addEventListener("mousedown", function (event) {
75                                           buffer.last_user_input_received = Date.now();
76                                       }, true, false);
78         this.browser.addEventListener("keypress", function (event) {
79                                           buffer.last_user_input_received = Date.now();
80                                       }, true, false);
82         this.browser.addEventListener("DOMLinkAdded", function (event) {
83                                           content_buffer_dom_link_added_hook.run(buffer, event);
84                                       }, true, false);
86         buffer.last_user_input_received = null;
88         /* FIXME: Add a handler for blocked popups, and also PopupWindow event */
89         /*
90          this.browser.addEventListener("DOMPopupBlocked", function (event) {
91          dumpln("PopupWindow: " + event);
92          }, true, false);
93          */
95         normal_input_mode(this);
97         this.ignore_initial_blank = true;
99         var lspec = arguments.$load;
100         if (lspec) {
101             if (lspec.url == "about:blank")
102                 this.ignore_initial_blank = false;
103             else {
104                 /* Ensure that an existing load of about:blank is stopped */
105                 this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
107                 this.load(lspec);
108             }
109         }
110     } finally {
111         this.constructor_end();
112     }
114 content_buffer.prototype = {
115     constructor : content_buffer,
117     get scrollX () { return this.top_frame.scrollX; },
118     get scrollY () { return this.top_frame.scrollY; },
119     get scrollMaxX () { return this.top_frame.scrollMaxX; },
120     get scrollMaxY () { return this.top_frame.scrollMaxY; },
123     /* Used to display the correct URI when the buffer opens initially
124      * even before loading has progressed far enough for currentURI to
125      * contain the correct URI. */
126     _display_URI : null,
128     get display_URI_string () {
129         if (this._display_URI)
130             return this._display_URI;
131         if (this.current_URI)
132             return this.current_URI.spec;
133         return "";
134     },
136     get title() { return this.browser.contentTitle; },
137     get description () { return this.display_URI_string; },
139     load : function (load_spec) {
140         apply_load_spec(this, load_spec);
141     },
143     _request_count: 0,
145     loading : false,
147     /* nsIWebProgressListener */
148     QueryInterface: generate_QI(Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference),
150     // This method is called to indicate state changes.
151     onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
153         const WPL = Components.interfaces.nsIWebProgressListener;
155         var flagstr = "";
156         if (aStateFlags & WPL.STATE_START)
157             flagstr += ",start";
158         if (aStateFlags & WPL.STATE_STOP)
159             flagstr += ",stop";
160         if (aStateFlags & WPL.STATE_IS_REQUEST)
161             flagstr += ",request";
162         if (aStateFlags & WPL.STATE_IS_DOCUMENT)
163             flagstr += ",document";
164         if (aStateFlags & WPL.STATE_IS_NETWORK)
165             flagstr += ",network";
166         if (aStateFlags & WPL.STATE_IS_WINDOW)
167             flagstr += ",window";
168         dumpln("onStateChange: " + flagstr + ", status: " + aStatus);
170         if (!aRequest)
171             return;
174         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
175             this._request_count++;
176         }
177         else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
178             const NS_ERROR_UNKNOWN_HOST = 2152398878;
179             if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
180                 // to prevent bug 235825: wait for the request handled
181                 // by the automatic keyword resolver
182                 return;
183             }
184             // since we (try to) only handle STATE_STOP of the last request,
185             // the count of open requests should now be 0
186             this._request_count = 0;
187         }
189         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
190             aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
191             // It's okay to clear what the user typed when we start
192             // loading a document. If the user types, this counter gets
193             // set to zero, if the document load ends without an
194             // onLocationChange, this counter gets decremented
195             // (so we keep it while switching tabs after failed loads)
196             //dumpln("*** started loading");
197             this.loading = true;
198             content_buffer_started_loading_hook.run(this);
199             this.last_user_input_received = null;
200         }
201         else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
202                  aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
203             if (this.loading == true)  {
204                 //dumpln("*** finished loading");
205                 this.loading = false;
206                 content_buffer_finished_loading_hook.run(this);
207             }
208         }
210         if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
211                            Ci.nsIWebProgressListener.STATE_START)) {
212             if (!this.loading)
213                 this.set_default_message("Done");
214         }
215     },
217     /* This method is called to indicate progress changes for the currently
218        loading page. */
219     onProgressChange: function(webProgress, request, curSelf, maxSelf,
220                                curTotal, maxTotal) {
221         content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
222     },
224     /* This method is called to indicate a change to the current location.
225        The url can be gotten as location.spec. */
226     onLocationChange : function(webProgress, request, location) {
227         /* Attempt to ignore onLocationChange calls due to the initial
228          * loading of about:blank by all xul:browser elements. */
229         if (location.spec == "about:blank" && this.ignore_initial_blank)
230             return;
232         this.ignore_initial_blank = false;
234         //dumpln("spec: " + location.spec  +" ;;; " + this.display_URI_string);
235         /* Use the real location URI now */
236         this._display_URI = null;
237         content_buffer_location_change_hook.run(this, request, location);
238         this.last_user_input_received = null;
239         buffer_description_change_hook.run(this);
240     },
242     // This method is called to indicate a status changes for the currently
243     // loading page.  The message is already formatted for display.
244     // Status messages could be displayed in the minibuffer output area.
245     onStatusChange: function(webProgress, request, status, msg) {
246         this.set_default_message(msg);
247         content_buffer_status_change_hook.run(this, request, status, msg);
248     },
250     // This method is called when the security state of the browser changes.
251     onSecurityChange: function(webProgress, request, state) {
252         /* FIXME: currently this isn't used */
254         /*
255         const WPL = Components.interfaces.nsIWebProgressListener;
257         if (state & WPL.STATE_IS_INSECURE) {
258             // update visual indicator
259         } else {
260             var level = "unknown";
261             if (state & WPL.STATE_IS_SECURE) {
262                 if (state & WPL.STATE_SECURE_HIGH)
263                     level = "high";
264                 else if (state & WPL.STATE_SECURE_MED)
265                     level = "medium";
266                 else if (state & WPL.STATE_SECURE_LOW)
267                     level = "low";
268             } else if (state & WPL_STATE_IS_BROKEN) {
269                 level = "mixed";
270             }
271             // provide a visual indicator of the security state here.
272         }
273         */
274     },
276     /* Inherit from buffer */
278     __proto__ : buffer.prototype
282 add_hook("current_content_buffer_finished_loading_hook",
283          function (buffer) {
284                  buffer.window.minibuffer.show("Done");
285          });
287 add_hook("current_content_buffer_status_change_hook",
288          function (buffer, request, status, msg) {
289              buffer.set_default_message(msg);
290          });
294 define_variable("read_url_handler_list", [],
295     "A list of handler functions which transform a typed url into a valid " +
296     "url or webjump.  If the typed input is not valid then each function " +
297     "on this list is tried in turn.  The handler function is called with " +
298     "a single string argument and it should return either a string or " +
299     "null.  The result of the first function on the list that returns a " +
300     "string is used in place of the input.");
302 /* read_url_make_default_webjump_handler returns a function that
303  * transforms any input into the given webjump.  It should be the last
304  * handler on read_url_handler_list (because any input is
305  * accepted). */
306 function read_url_make_default_webjump_handler(default_webjump) {
307     return function(input) {
308         return default_webjump + " " + input;
309     };
312 /* read_url_make_blank_url_handler returns a function that replaces a
313  * blank (empty) input with the given url (or webjump).  The url may
314  * perform some function, eg. "javascript:location.reload()". */
315 function read_url_make_blank_url_handler(url) {
316     return function(input) {
317         if (input.length == 0)
318             return url;
319         return null;
320     };
323 minibuffer.prototype.try_read_url_handlers = function(input) {
324     var result;
325     for (var i = 0; i < read_url_handler_list.length; ++i)
326         if ((result = read_url_handler_list[i](input)))
327             return result;
328     return input;
331 define_variable("url_completion_use_webjumps", true, "Specifies whether URL completion should complete webjumps.");
332 define_variable("url_completion_use_bookmarks", true, "Specifies whether URL completion should complete bookmarks.");
333 define_variable("url_completion_use_history", false,
334                      "Specifies whether URL completion should complete using browser history.");
336 define_variable("minibuffer_read_url_select_initial", true,
337                 "Specifies whether a URL  presented in the minibuffer for editing should be selected.  This affects find-alternate-url.");
339 minibuffer_auto_complete_preferences["url"] = true;
340 minibuffer.prototype.read_url = function () {
341     keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
342              $use_webjumps = url_completion_use_webjumps,
343              $use_history = url_completion_use_history,
344              $use_bookmarks = url_completion_use_bookmarks);
345     var completer = url_completer ($use_webjumps = arguments.$use_webjumps,
346         $use_bookmarks = arguments.$use_bookmarks,
347         $use_history = arguments.$use_history);
348     var result = yield this.read(
349         $prompt = arguments.$prompt,
350         $history = arguments.$history,
351         $completer = completer,
352         $initial_value = arguments.$initial_value,
353         $auto_complete = "url",
354         $select = minibuffer_read_url_select_initial,
355         $match_required = false);
356     if (!possibly_valid_url(result) && !getWebJump(result))
357         result = this.try_read_url_handlers(result);
358     if (result == "") // well-formedness check. (could be better!)
359         throw ("invalid url or webjump (\""+ result +"\")");
360     yield co_return(result);
363 I.content_charset = interactive_method(
364     $sync = function (ctx) {
365         var buffer = ctx.buffer;
366         if (!(buffer instanceof content_buffer))
367             throw new Error("Current buffer is of invalid type");
368         // -- Charset of content area of focusedWindow
369         var focusedWindow = buffer.focused_frame;
370         if (focusedWindow)
371             return focusedWindow.document.characterSet;
372         else
373             return null;
374     });
377 I.content_selection = interactive_method(
378     $sync = function (ctx) {
379         // -- Selection of content area of focusedWindow
380         var focusedWindow = this.buffers.current.focused_frame;
381         return focusedWindow.getSelection ();
382     });
384 function overlink_update_status(buffer, text) {
385     if (text.length > 0)
386         buffer.window.minibuffer.show("Link: " + text);
387     else
388         buffer.window.minibuffer.show("");
391 define_global_mode("overlink_mode",
392                    function () {
393                        add_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
394                    },
395                    function () {
396                        remove_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
397                    });
399 overlink_mode(true);
402 function go_back (b, prefix)
404     if (prefix < 0)
405         go_forward(b, -prefix);
407     check_buffer(b, content_buffer);
409     if (b.web_navigation.canGoBack)
410     {
411         var hist = b.web_navigation.sessionHistory;
412         var idx = hist.index - prefix;
413         if (idx < 0)
414             idx = 0;
415         b.web_navigation.gotoIndex(idx);
416     } else
417         throw interactive_error("Can't go back");
419 interactive(
420     "go-back",
421     "Go back in the session history for the current buffer.",
422     function (I) {go_back(I.buffer, I.p);});
424 function go_forward (b, prefix)
426     if (prefix < 0)
427         go_back(b, -prefix);
429     check_buffer(b, content_buffer);
431     if (b.web_navigation.canGoForward)
432     {
433         var hist = b.web_navigation.sessionHistory;
434         var idx = hist.index + prefix;
435         if (idx >= hist.count) idx = hist.count-1;
436         b.web_navigation.gotoIndex(idx);
437     } else
438         throw interactive_error("Can't go forward");
440 interactive("go-forward",
441             "Go back in the session hisory for the current buffer.",
442             function (I) {go_forward(I.buffer, I.p);});
444 function stop_loading (b)
446     check_buffer(b, content_buffer);
447     b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
449 interactive("stop-loading",
450             "Stop loading the current document.",
451             function (I) {stop_loading(I.buffer);});
453 function reload (b, bypass_cache, element, forced_charset)
455     check_buffer(b, content_buffer);
456     if (element) {
457         element.parentNode.replaceChild(element.cloneNode(true), element);
458     } else {
459         var flags = bypass_cache == null ?
460             Ci.nsIWebNavigation.LOAD_FLAGS_NONE :
461             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
462         if (forced_charset) {
463             try {
464                 var atomservice = Cc['@mozilla.org/atom-service;1']
465                     .getService(Ci.nsIAtomService);
466                 b.web_navigation.documentCharsetInfo.forcedCharset =
467                     atomservice.getAtom(forced_charset);
468             } catch (e) {}
469         }
470         b.web_navigation.reload(flags);
471     }
473 interactive("reload",
474             "Reload the current document.\n" +
475             "If a prefix argument is specified, the cache is bypassed.  If a "+
476             "DOM node is supplied via browser object, that node will be "+
477             "reloaded.",
478             function (I) {
479                 check_buffer(I.buffer, content_buffer);
480                 var element = yield read_browser_object(I);
481                 reload(I.buffer, I.P, element);
482             });
485  * browserDOMWindow: intercept window opening
486  */
487 function initialize_browser_dom_window(window) {
488     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
489         new browser_dom_window(window);
492 define_variable("browser_default_open_target", OPEN_NEW_BUFFER,
493                 "Specifies how new window requests by content pages "+
494                 "(e.g. by window.open from JavaScript or by using the "+
495                 "target attribute of anchor and form elements) will be "+
496                 "handled.  This will generally be `OPEN_NEW_BUFFER', "+
497                 "`OPEN_NEW_BUFFER_BACKGROUND', or `OPEN_NEW_WINDOW'.");
499 function browser_dom_window(window) {
500     this.window = window;
501     this.next_target = null;
503 browser_dom_window.prototype = {
504     QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
506     openURI : function(aURI, aOpener, aWhere, aContext) {
508         // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
509         var target = this.next_target;
510         if (target == null || target == FOLLOW_DEFAULT)
511             target = browser_default_open_target;
512         this.next_target = null;
514         /* Determine the opener buffer */
515         var opener_buffer = get_buffer_from_frame(this.window, aOpener.top);
516         var config = opener_buffer ? opener_buffer.configuration : null;
518         switch (browser_default_open_target) {
519         case OPEN_CURRENT_BUFFER:
520         case FOLLOW_TOP_FRAME:
521             return aOpener.top;
522         case FOLLOW_CURRENT_FRAME:
523             return aOpener;
524         case OPEN_NEW_BUFFER:
525             var buffer = new content_buffer(this.window, null /* element */, $configuration = config);
526             this.window.buffers.current = buffer;
527             return buffer.top_frame;
528         case OPEN_NEW_BUFFER_BACKGROUND:
529             var buffer = new content_buffer(this.window, null /* element */, $configuration = config);
530             return buffer.top_frame;
531         case OPEN_NEW_WINDOW:
532         default: /* shouldn't be needed */
534             /* We don't call make_window here, because that will result
535              * in the URL being loaded as the top-level document,
536              * instead of within a browser buffer.  Instead, we can
537              * rely on Mozilla using browser.chromeURL. */
538             window_set_extra_arguments(
539                 {initial_buffer_creator: buffer_creator(content_buffer, $configuration = config)}
540             );
541             return null;
542         }
543     }
546 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
548 define_keywords("$enable", "$disable", "$doc");
549 function define_page_mode(name, display_name) {
550     keywords(arguments);
551     var enable = arguments.$enable;
552     var disable = arguments.$disable;
553     var doc = arguments.$doc;
554     define_buffer_mode(name, display_name,
555                        $class = "page_mode",
556                        $enable = enable,
557                        $disable = function (buffer) {
558                            if (disable)
559                                disable(buffer);
560                            buffer.local_variables = {};
561                            buffer.default_browser_object_classes = {};
562                        },
563                        $doc = doc);
565 ignore_function_for_get_caller_source_code_reference("define_page_mode");
567 define_variable("auto_mode_list", [], "A list of mappings from URI regular expressions to page modes.");
568 function page_mode_auto_update(buffer) {
569     var uri = buffer.current_URI.spec;
570     var mode = predicate_alist_match(auto_mode_list, uri);
571     if (mode)
572         mode(buffer, true);
573     else if (buffer.page_mode)
574         conkeror[buffer.page_mode](buffer, false);
577 add_hook("content_buffer_location_change_hook", page_mode_auto_update);