whitespace and style cleanup
[conkeror.git] / modules / content-buffer.js
blob3b8dd9f4f63e3781e5f5d35b22a330ce24552549
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) {
36     keywords(arguments);
37     this.constructor_begin();
38     try {
39         conkeror.buffer.call(this, window, element, forward_keywords(arguments));
41         this.browser.addProgressListener(this);
42         var buffer = this;
43         this.browser.addEventListener("DOMTitleChanged", function (event) {
44                                           buffer_title_change_hook.run(buffer);
45                                       }, true /* capture */, false /* ignore untrusted events */);
47         this.browser.addEventListener("scroll", function (event) {
48                                           buffer_scroll_hook.run(buffer);
49                                       }, true /* capture */, false /* ignore untrusted events */);
51         this.browser.addEventListener("focus", function (event) {
52                                           content_buffer_focus_change_hook.run(buffer, event);
53                                       }, true /* capture */, false /* ignore untrusted events */);
55         this.browser.addEventListener("mouseover", function (event) {
56                                           var node = event.target;
57                                           while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
58                                               node = node.parentNode;
59                                           if (node) {
60                                               content_buffer_overlink_change_hook.run(buffer, node.href);
61                                               buffer.current_overlink = event.target;
62                                           }
63                                       }, true, false);
65         this.browser.addEventListener("mouseout", function (event) {
66                                           if (buffer.current_overlink == event.target) {
67                                               buffer.current_overlink = null;
68                                               content_buffer_overlink_change_hook.run(buffer, "");
69                                           }
70                                       }, true, false);
72         this.browser.addEventListener("mousedown", function (event) {
73                                           buffer.last_user_input_received = Date.now();
74                                       }, true, false);
76         this.browser.addEventListener("keypress", function (event) {
77                                           buffer.last_user_input_received = Date.now();
78                                       }, true, false);
80         this.browser.addEventListener("DOMLinkAdded", function (event) {
81                                           content_buffer_dom_link_added_hook.run(buffer, event);
82                                       }, true, false);
84         buffer.last_user_input_received = null;
86         /* FIXME: Add a handler for blocked popups, and also PopupWindow event */
87         /*
88          this.browser.addEventListener("DOMPopupBlocked", function (event) {
89          dumpln("PopupWindow: " + event);
90          }, true, false);
91          */
93         normal_input_mode(this);
95         this.ignore_initial_blank = true;
97         var lspec = arguments.$load;
98         if (lspec) {
99             if (lspec.url == "about:blank")
100                 this.ignore_initial_blank = false;
101             else {
102                 /* Ensure that an existing load of about:blank is stopped */
103                 this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
105                 this.load(lspec);
106             }
107         }
108     } finally {
109         this.constructor_end();
110     }
112 content_buffer.prototype = {
113     constructor : content_buffer,
115     get scrollX () { return this.top_frame.scrollX; },
116     get scrollY () { return this.top_frame.scrollY; },
117     get scrollMaxX () { return this.top_frame.scrollMaxX; },
118     get scrollMaxY () { return this.top_frame.scrollMaxY; },
120     /* Used to display the correct URI when the buffer opens initially
121      * even before loading has progressed far enough for currentURI to
122      * contain the correct URI. */
123     _display_URI : null,
125     get display_URI_string () {
126         if (this._display_URI)
127             return this._display_URI;
128         if (this.current_URI)
129             return this.current_URI.spec;
130         return "";
131     },
133     get title () { return this.browser.contentTitle; },
134     get description () { return this.display_URI_string; },
136     load : function (load_spec) {
137         apply_load_spec(this, load_spec);
138     },
140     _request_count: 0,
142     loading : false,
144     /* nsIWebProgressListener */
145     QueryInterface: generate_QI(Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference),
147     // This method is called to indicate state changes.
148     onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
150         const WPL = Components.interfaces.nsIWebProgressListener;
152         var flagstr = "";
153         if (aStateFlags & WPL.STATE_START)
154             flagstr += ",start";
155         if (aStateFlags & WPL.STATE_STOP)
156             flagstr += ",stop";
157         if (aStateFlags & WPL.STATE_IS_REQUEST)
158             flagstr += ",request";
159         if (aStateFlags & WPL.STATE_IS_DOCUMENT)
160             flagstr += ",document";
161         if (aStateFlags & WPL.STATE_IS_NETWORK)
162             flagstr += ",network";
163         if (aStateFlags & WPL.STATE_IS_WINDOW)
164             flagstr += ",window";
165         dumpln("onStateChange: " + flagstr + ", status: " + aStatus);
167         if (!aRequest)
168             return;
171         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
172             this._request_count++;
173         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
174             const NS_ERROR_UNKNOWN_HOST = 2152398878;
175             if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
176                 // to prevent bug 235825: wait for the request handled
177                 // by the automatic keyword resolver
178                 return;
179             }
180             // since we (try to) only handle STATE_STOP of the last request,
181             // the count of open requests should now be 0
182             this._request_count = 0;
183         }
185         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
186             aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
187             // It's okay to clear what the user typed when we start
188             // loading a document. If the user types, this counter gets
189             // set to zero, if the document load ends without an
190             // onLocationChange, this counter gets decremented
191             // (so we keep it while switching tabs after failed loads)
192             //dumpln("*** started loading");
193             this.loading = true;
194             content_buffer_started_loading_hook.run(this);
195             this.last_user_input_received = null;
196         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
197                    aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
198             if (this.loading == true) {
199                 //dumpln("*** finished loading");
200                 this.loading = false;
201                 content_buffer_finished_loading_hook.run(this);
202             }
203         }
205         if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
206                            Ci.nsIWebProgressListener.STATE_START)) {
207             if (!this.loading)
208                 this.set_default_message("Done");
209         }
210     },
212     /* This method is called to indicate progress changes for the currently
213        loading page. */
214     onProgressChange: function (webProgress, request, curSelf, maxSelf,
215                                 curTotal, maxTotal) {
216         content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
217     },
219     /* This method is called to indicate a change to the current location.
220        The url can be gotten as location.spec. */
221     onLocationChange : function (webProgress, request, location) {
222         /* Attempt to ignore onLocationChange calls due to the initial
223          * loading of about:blank by all xul:browser elements. */
224         if (location.spec == "about:blank" && this.ignore_initial_blank)
225             return;
227         this.ignore_initial_blank = false;
229         //dumpln("spec: " + location.spec  +" ;;; " + this.display_URI_string);
230         /* Use the real location URI now */
231         this._display_URI = null;
232         content_buffer_location_change_hook.run(this, request, location);
233         this.last_user_input_received = null;
234         buffer_description_change_hook.run(this);
235     },
237     // This method is called to indicate a status changes for the currently
238     // loading page.  The message is already formatted for display.
239     // Status messages could be displayed in the minibuffer output area.
240     onStatusChange: function (webProgress, request, status, msg) {
241         this.set_default_message(msg);
242         content_buffer_status_change_hook.run(this, request, status, msg);
243     },
245     // This method is called when the security state of the browser changes.
246     onSecurityChange: function (webProgress, request, state) {
247         /* FIXME: currently this isn't used */
249         /*
250         const WPL = Components.interfaces.nsIWebProgressListener;
252         if (state & WPL.STATE_IS_INSECURE) {
253             // update visual indicator
254         } else {
255             var level = "unknown";
256             if (state & WPL.STATE_IS_SECURE) {
257                 if (state & WPL.STATE_SECURE_HIGH)
258                     level = "high";
259                 else if (state & WPL.STATE_SECURE_MED)
260                     level = "medium";
261                 else if (state & WPL.STATE_SECURE_LOW)
262                     level = "low";
263             } else if (state & WPL_STATE_IS_BROKEN) {
264                 level = "mixed";
265             }
266             // provide a visual indicator of the security state here.
267         }
268         */
269     },
271     /* Inherit from buffer */
273     __proto__ : buffer.prototype
277 add_hook("current_content_buffer_finished_loading_hook",
278          function (buffer) {
279                  buffer.window.minibuffer.show("Done");
280          });
282 add_hook("current_content_buffer_status_change_hook",
283          function (buffer, request, status, msg) {
284              buffer.set_default_message(msg);
285          });
289 define_variable("read_url_handler_list", [],
290     "A list of handler functions which transform a typed url into a valid " +
291     "url or webjump.  If the typed input is not valid then each function " +
292     "on this list is tried in turn.  The handler function is called with " +
293     "a single string argument and it should return either a string or " +
294     "null.  The result of the first function on the list that returns a " +
295     "string is used in place of the input.");
298  * read_url_make_default_webjump_handler returns a function that
299  * transforms any input into the given webjump.  It should be the last
300  * handler on read_url_handler_list (because any input is
301  * accepted).
302  */
303 function read_url_make_default_webjump_handler (default_webjump) {
304     return function (input) {
305         return default_webjump + " " + input;
306     };
310  * read_url_make_blank_url_handler returns a function that replaces a
311  * blank (empty) input with the given url (or webjump).  The url may
312  * perform some function, eg. "javascript:location.reload()".
313  */
314 function read_url_make_blank_url_handler (url) {
315     return function (input) {
316         if (input.length == 0)
317             return url;
318         return null;
319     };
322 minibuffer.prototype.try_read_url_handlers = function (input) {
323     var result;
324     for (var i = 0; i < read_url_handler_list.length; ++i) {
325         if ((result = read_url_handler_list[i](input)))
326             return result;
327     }
328     return input;
331 define_variable("url_completion_use_webjumps", true,
332     "Specifies whether URL completion should complete webjumps.");
334 define_variable("url_completion_use_bookmarks", true,
335     "Specifies whether URL completion should complete bookmarks.");
337 define_variable("url_completion_use_history", false,
338     "Specifies whether URL completion should complete using browser "+
339     "history.");
341 define_variable("minibuffer_read_url_select_initial", true,
342     "Specifies whether a URL  presented in the minibuffer for editing "+
343     "should be selected.  This affects find-alternate-url.");
346 minibuffer_auto_complete_preferences["url"] = true;
347 minibuffer.prototype.read_url = function () {
348     keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
349              $use_webjumps = url_completion_use_webjumps,
350              $use_history = url_completion_use_history,
351              $use_bookmarks = url_completion_use_bookmarks);
352     var completer = url_completer($use_webjumps = arguments.$use_webjumps,
353         $use_bookmarks = arguments.$use_bookmarks,
354         $use_history = arguments.$use_history);
355     var result = yield this.read(
356         $prompt = arguments.$prompt,
357         $history = arguments.$history,
358         $completer = completer,
359         $initial_value = arguments.$initial_value,
360         $auto_complete = "url",
361         $select = minibuffer_read_url_select_initial,
362         $match_required = false);
363     if (!possibly_valid_url(result) && !getWebJump(result))
364         result = this.try_read_url_handlers(result);
365     if (result == "") // well-formedness check. (could be better!)
366         throw ("invalid url or webjump (\""+ result +"\")");
367     yield co_return(result);
370 I.content_charset = interactive_method(
371     $sync = function (ctx) {
372         var buffer = ctx.buffer;
373         if (!(buffer instanceof content_buffer))
374             throw new Error("Current buffer is of invalid type");
375         // -- Charset of content area of focusedWindow
376         var focusedWindow = buffer.focused_frame;
377         if (focusedWindow)
378             return focusedWindow.document.characterSet;
379         else
380             return null;
381     });
384 I.content_selection = interactive_method(
385     $sync = function (ctx) {
386         // -- Selection of content area of focusedWindow
387         var focusedWindow = this.buffers.current.focused_frame;
388         return focusedWindow.getSelection ();
389     });
391 function overlink_update_status (buffer, text) {
392     if (text.length > 0)
393         buffer.window.minibuffer.show("Link: " + text);
394     else
395         buffer.window.minibuffer.show("");
398 define_global_mode("overlink_mode",
399                    function () {
400                        add_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
401                    },
402                    function () {
403                        remove_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
404                    });
406 overlink_mode(true);
409 function go_back (b, prefix) {
410     if (prefix < 0)
411         go_forward(b, -prefix);
413     check_buffer(b, content_buffer);
415     if (b.web_navigation.canGoBack) {
416         var hist = b.web_navigation.sessionHistory;
417         var idx = hist.index - prefix;
418         if (idx < 0)
419             idx = 0;
420         b.web_navigation.gotoIndex(idx);
421     } else
422         throw interactive_error("Can't go back");
425 interactive("go-back",
426     "Go back in the session history for the current buffer.",
427     function (I) {go_back(I.buffer, I.p);});
430 function go_forward (b, prefix) {
431     if (prefix < 0)
432         go_back(b, -prefix);
434     check_buffer(b, content_buffer);
436     if (b.web_navigation.canGoForward) {
437         var hist = b.web_navigation.sessionHistory;
438         var idx = hist.index + prefix;
439         if (idx >= hist.count) idx = hist.count-1;
440         b.web_navigation.gotoIndex(idx);
441     } else
442         throw interactive_error("Can't go forward");
445 interactive("go-forward",
446             "Go back in the session history for the current buffer.",
447             function (I) {go_forward(I.buffer, I.p);});
450 function stop_loading (b) {
451     check_buffer(b, content_buffer);
452     b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
455 interactive("stop-loading",
456             "Stop loading the current document.",
457             function (I) {stop_loading(I.buffer);});
460 function reload (b, bypass_cache, element, forced_charset) {
461     check_buffer(b, content_buffer);
462     if (element) {
463         if (element instanceof Ci.nsIDOMHTMLImageElement) {
464             try {
465                 var cache = Cc['@mozilla.org/image/cache;1']
466                     .getService(Ci.imgICache);
467                 cache.removeEntry(make_uri(element.src));
468             } catch (e) {}
469         }
470         element.parentNode.replaceChild(element.cloneNode(true), element);
471     } else {
472         var flags = bypass_cache == null ?
473             Ci.nsIWebNavigation.LOAD_FLAGS_NONE :
474             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
476         if (! forced_charset && forced_charset_list)
477             forced_charset = predicate_alist_match(forced_charset_list,
478                                                    b.current_URI.spec);
480         if (forced_charset) {
481             try {
482                 var atomservice = Cc['@mozilla.org/atom-service;1']
483                     .getService(Ci.nsIAtomService);
484                 b.web_navigation.documentCharsetInfo.forcedCharset =
485                     atomservice.getAtom(forced_charset);
486             } catch (e) {}
487         }
488         b.web_navigation.reload(flags);
489     }
492 interactive("reload",
493     "Reload the current document.\n" +
494     "If a prefix argument is specified, the cache is bypassed.  If a "+
495     "DOM node is supplied via browser object, that node will be "+
496     "reloaded.",
497     function (I) {
498         check_buffer(I.buffer, content_buffer);
499         var element = yield read_browser_object(I);
500         reload(I.buffer, I.P, element);
501     });
504  * browserDOMWindow: intercept window opening
505  */
506 function initialize_browser_dom_window (window) {
507     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
508         new browser_dom_window(window);
511 define_variable("browser_default_open_target", OPEN_NEW_BUFFER,
512     "Specifies how new window requests by content pages (e.g. by "+
513     "window.open from JavaScript or by using the target attribute of "+
514     "anchor and form elements) will be handled.  This will generally "+
515     "be `OPEN_NEW_BUFFER', `OPEN_NEW_BUFFER_BACKGROUND', or "+
516     "`OPEN_NEW_WINDOW'.");
519 function browser_dom_window (window) {
520     this.window = window;
521     this.next_target = null;
523 browser_dom_window.prototype = {
524     QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
526     openURI : function (aURI, aOpener, aWhere, aContext) {
528         // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
529         var target = this.next_target;
530         if (target == null || target == FOLLOW_DEFAULT)
531             target = browser_default_open_target;
532         this.next_target = null;
534         /* Determine the opener buffer */
535         var opener = get_buffer_from_frame(this.window, aOpener.top);
537         switch (browser_default_open_target) {
538         case OPEN_CURRENT_BUFFER:
539             return aOpener.top;
540         case FOLLOW_CURRENT_FRAME:
541             return aOpener;
542         case OPEN_NEW_BUFFER:
543             var buffer = new content_buffer(this.window, null /* element */, $opener = opener);
544             this.window.buffers.current = buffer;
545             return buffer.top_frame;
546         case OPEN_NEW_BUFFER_BACKGROUND:
547             var buffer = new content_buffer(this.window, null /* element */, $opener = opener);
548             return buffer.top_frame;
549         case OPEN_NEW_WINDOW:
550         default: /* shouldn't be needed */
552             /* We don't call make_window here, because that will result
553              * in the URL being loaded as the top-level document,
554              * instead of within a browser buffer.  Instead, we can
555              * rely on Mozilla using browser.chromeURL. */
556             window_set_extra_arguments(
557                 {initial_buffer_creator: buffer_creator(content_buffer, $opener = opener)}
558             );
559             return null;
560         }
561     }
564 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
566 define_keywords("$enable", "$disable", "$doc", "$keymaps");
567 function define_page_mode(name, display_name) {
568     keywords(arguments);
569     var enable = arguments.$enable;
570     var disable = arguments.$disable;
571     var doc = arguments.$doc;
572     var keymaps = arguments.$keymaps;
573     function page_mode_update_keymap (buffer) {
574         if (keymaps[buffer.input_mode])
575             buffer.keymap = keymaps[buffer.input_mode];
576     }
577     define_buffer_mode(name, display_name,
578                        $class = "page_mode",
579                        $enable = function (buffer) {
580                            buffer.page = {
581                                local: { __proto__: buffer.local }
582                            };
583                            if (enable)
584                                enable(buffer);
585                            if (keymaps) {
586                                add_hook.call(buffer, "input_mode_change_hook",
587                                              page_mode_update_keymap);
588                                page_mode_update_keymap(buffer);
589                            }
590                        },
591                        $disable = function (buffer) {
592                            if (disable)
593                                disable(buffer);
594                            buffer.page = null;
595                            buffer.default_browser_object_classes = {};
596                            if (keymaps) {
597                                remove_hook.call(buffer, "input_mode_change_hook",
598                                                 page_mode_update_keymap);
599                                content_buffer_update_keymap_for_input_mode(buffer);
600                            }
601                        },
602                        $doc = doc);
604 ignore_function_for_get_caller_source_code_reference("define_page_mode");
607 define_variable("auto_mode_list", [],
608     "A list of mappings from URI regular expressions to page modes.");
610 function page_mode_auto_update (buffer) {
611     var uri = buffer.current_URI.spec;
612     var mode = predicate_alist_match(auto_mode_list, uri);
613     if (mode)
614         mode(buffer, true);
615     else if (buffer.page_mode)
616         conkeror[buffer.page_mode](buffer, false);
619 add_hook("content_buffer_location_change_hook", page_mode_auto_update);