Add facility to allow user functions to transform typed URLs.
[conkeror.git] / modules / content-buffer.js
blob799d82db8ea9d8dd7eb22b0ffa2408b939aaf6d0
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_buffer_local_hook("content_buffer_finished_loading_hook");
15 define_buffer_local_hook("content_buffer_started_loading_hook");
16 define_buffer_local_hook("content_buffer_progress_change_hook");
17 define_buffer_local_hook("content_buffer_location_change_hook");
18 define_buffer_local_hook("content_buffer_status_change_hook");
19 define_buffer_local_hook("content_buffer_focus_change_hook");
20 define_buffer_local_hook("content_buffer_overlink_change_hook");
21 define_buffer_local_hook("content_buffer_dom_link_added_hook");
23 define_current_buffer_hook("current_content_buffer_finished_loading_hook", "content_buffer_finished_loading_hook");
24 define_current_buffer_hook("current_content_buffer_progress_change_hook", "content_buffer_progress_change_hook");
25 define_current_buffer_hook("current_content_buffer_location_change_hook", "content_buffer_location_change_hook");
26 define_current_buffer_hook("current_content_buffer_status_change_hook", "content_buffer_status_change_hook");
27 define_current_buffer_hook("current_content_buffer_focus_change_hook", "content_buffer_focus_change_hook");
28 define_current_buffer_hook("current_content_buffer_overlink_change_hook", "content_buffer_overlink_change_hook");
30 /* If browser is null, create a new browser */
31 define_keywords("$load");
32 function content_buffer(window, element)
34     keywords(arguments);
35     this.constructor_begin();
36     try {
38         conkeror.buffer.call(this, window, element, forward_keywords(arguments));
40         this.browser.addProgressListener(this);
41         var buffer = this;
42         this.browser.addEventListener("DOMTitleChanged", function (event) {
43                                           buffer_title_change_hook.run(buffer);
44                                       }, true /* capture */, false /* ignore untrusted events */);
46         this.browser.addEventListener("scroll", function (event) {
47                                           buffer_scroll_hook.run(buffer);
48                                       }, true /* capture */, false /* ignore untrusted events */);
50         this.browser.addEventListener("focus", function (event) {
51                                           content_buffer_focus_change_hook.run(buffer, event);
52                                       }, true /* capture */, false /* ignore untrusted events */);
54         this.browser.addEventListener("mouseover", function (event) {
55                                           var node = event.target;
56                                           while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
57                                               node = node.parentNode;
58                                           if (node) {
59                                               content_buffer_overlink_change_hook.run(buffer, node.href);
60                                               buffer.current_overlink = event.target;
61                                           }
62                                       }, true, false);
64         this.browser.addEventListener("mouseout", function (event) {
65                                           if (buffer.current_overlink == event.target) {
66                                               buffer.current_overlink = null;
67                                               content_buffer_overlink_change_hook.run(buffer, "");
68                                           }
69                                       }, true, false);
71         this.browser.addEventListener("mousedown", function (event) {
72                                           buffer.last_user_input_received = Date.now();
73                                       }, true, false);
75         this.browser.addEventListener("keypress", function (event) {
76                                           buffer.last_user_input_received = Date.now();
77                                       }, true, false);
79         this.browser.addEventListener("DOMLinkAdded", function (event) {
80                                           content_buffer_dom_link_added_hook.run(buffer, event);
81                                       }, true, false);
83         buffer.last_user_input_received = null;
85         /* FIXME: Add a handler for blocked popups, and also PopupWindow event */
86         /*
87          this.browser.addEventListener("DOMPopupBlocked", function (event) {
88          dumpln("PopupWindow: " + event);
89          }, true, false);
90          */
92         normal_input_mode(this);
94         this.ignore_initial_blank = true;
96         var lspec = arguments.$load;
97         if (lspec) {
98             if (lspec.url == "about:blank")
99                 this.ignore_initial_blank = false;
100             else {
101                 /* Ensure that an existing load of about:blank is stopped */
102                 this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
104                 this.load(lspec);
105             }
106         }
107     } finally {
108         this.constructor_end();
109     }
111 content_buffer.prototype = {
112     constructor : content_buffer,
114     get scrollX () { return this.top_frame.scrollX; },
115     get scrollY () { return this.top_frame.scrollY; },
116     get scrollMaxX () { return this.top_frame.scrollMaxX; },
117     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         }
174         else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
175             const NS_ERROR_UNKNOWN_HOST = 2152398878;
176             if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
177                 // to prevent bug 235825: wait for the request handled
178                 // by the automatic keyword resolver
179                 return;
180             }
181             // since we (try to) only handle STATE_STOP of the last request,
182             // the count of open requests should now be 0
183             this._request_count = 0;
184         }
186         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
187             aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
188             // It's okay to clear what the user typed when we start
189             // loading a document. If the user types, this counter gets
190             // set to zero, if the document load ends without an
191             // onLocationChange, this counter gets decremented
192             // (so we keep it while switching tabs after failed loads)
193             //dumpln("*** started loading");
194             this.loading = true;
195             content_buffer_started_loading_hook.run(this);
196             this.last_user_input_received = null;
197         }
198         else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
199                  aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
200             if (this.loading == true)  {
201                 //dumpln("*** finished loading");
202                 content_buffer_finished_loading_hook.run(this);
203                 this.loading = false;
204             }
205         }
207         if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
208                            Ci.nsIWebProgressListener.STATE_START)) {
209             if (!this.loading)
210                 this.set_default_message("Done");
211         }
212     },
214     /* This method is called to indicate progress changes for the currently
215        loading page. */
216     onProgressChange: function(webProgress, request, curSelf, maxSelf,
217                                curTotal, maxTotal) {
218         content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
219     },
221     /* This method is called to indicate a change to the current location.
222        The url can be gotten as location.spec. */
223     onLocationChange : function(webProgress, request, location) {
224         /* Attempt to ignore onLocationChange calls due to the initial
225          * loading of about:blank by all xul:browser elements. */
226         if (location.spec == "about:blank" && this.ignore_initial_blank)
227             return;
229         this.ignore_initial_blank = false;
231         //dumpln("spec: " + location.spec  +" ;;; " + this.display_URI_string);
232         /* Use the real location URI now */
233         this._display_URI = null;
234         content_buffer_location_change_hook.run(this, request, location);
235         this.last_user_input_received = null;
236         buffer_description_change_hook.run(this);
237     },
239     // This method is called to indicate a status changes for the currently
240     // loading page.  The message is already formatted for display.
241     // Status messages could be displayed in the minibuffer output area.
242     onStatusChange: function(webProgress, request, status, msg) {
243         this.set_default_message(msg);
244         content_buffer_status_change_hook.run(this, request, status, msg);
245     },
247     // This method is called when the security state of the browser changes.
248     onSecurityChange: function(webProgress, request, state) {
249         /* FIXME: currently this isn't used */
251         /*
252         const WPL = Components.interfaces.nsIWebProgressListener;
254         if (state & WPL.STATE_IS_INSECURE) {
255             // update visual indicator
256         } else {
257             var level = "unknown";
258             if (state & WPL.STATE_IS_SECURE) {
259                 if (state & WPL.STATE_SECURE_HIGH)
260                     level = "high";
261                 else if (state & WPL.STATE_SECURE_MED)
262                     level = "medium";
263                 else if (state & WPL.STATE_SECURE_LOW)
264                     level = "low";
265             } else if (state & WPL_STATE_IS_BROKEN) {
266                 level = "mixed";
267             }
268             // provide a visual indicator of the security state here.
269         }
270         */
271     },
273     /* Inherit from buffer */
275     __proto__ : buffer.prototype
279 add_hook("current_content_buffer_finished_loading_hook",
280          function (buffer) {
281                  buffer.window.minibuffer.show("Done");
282          });
284 add_hook("current_content_buffer_status_change_hook",
285          function (buffer, request, status, msg) {
286              buffer.set_default_message(msg);
287          });
291 define_variable("read_url_handler_list", [],
292     "A list of handler functions which transform a typed url into a valid " +
293     "url or webjump.  If the typed input is not valid then each function " +
294     "on this list is tried in turn.  The handler function is called with " +
295     "a single string argument and it should return either a string or " +
296     "null.  The result of the first function on the list that returns a " +
297     "string is used in place of the input.");
299 /* read_url_make_default_webjump_handler returns a function that
300  * transforms any input into the given webjump.  It should be the last
301  * handler on read_url_handler_list (because any input is
302  * accepted). */
303 function read_url_make_default_webjump_handler(default_webjump) {
304     return function(input) {
305         return default_webjump + " " + input;
306     }
309 /* read_url_make_blank_url_handler returns a function that replaces a
310  * blank (empty) input with the given url (or webjump).  The url may
311  * perform some function, eg. "javascript:location.reload()". */
312 function read_url_make_blank_url_handler(url) {
313     return function(input) {
314         if (input.length == 0)
315             return url;
316         return null;
317     }
320 minibuffer.prototype.try_read_url_handlers = function(input) {
321     var result;
322     for (var i = 0; i < read_url_handler_list.length; ++i)
323         if (result = read_url_handler_list[i](input))
324             return result;
325     return input;
328 define_variable("url_completion_use_webjumps", true, "Specifies whether URL completion should complete webjumps.");
329 define_variable("url_completion_use_bookmarks", true, "Specifies whether URL completion should complete bookmarks.");
330 define_variable("url_completion_use_history", false,
331                      "Specifies whether URL completion should complete using browser history.");
333 define_variable("minibuffer_read_url_select_initial", true,
334                 "Specifies whether a URL  presented in the minibuffer for editing should be selected.  This affects find-alternate-url.");
336 minibuffer_auto_complete_preferences["url"] = true;
337 minibuffer.prototype.read_url = function () {
338     keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
339              $use_webjumps = url_completion_use_webjumps,
340              $use_history = url_completion_use_history,
341              $use_bookmarks = url_completion_use_bookmarks);
342     var completer = url_completer ($use_webjumps = arguments.$use_webjumps,
343         $use_bookmarks = arguments.$use_bookmarks,
344         $use_history = arguments.$use_history);
345     var result = yield this.read(
346         $prompt = arguments.$prompt,
347         $history = arguments.$history,
348         $completer = completer,
349         $initial_value = arguments.$initial_value,
350         $auto_complete = "url",
351         $select = minibuffer_read_url_select_initial,
352         $match_required = false);
353     if (!possibly_valid_url(result) && !getWebJump(result))
354         result = this.try_read_url_handlers(result);
355     if (result == "") // well-formedness check. (could be better!)
356         throw ("invalid url or webjump (\""+ result +"\")");
357     yield co_return(result);
360 I.content_charset = interactive_method(
361     $sync = function (ctx) {
362         var buffer = ctx.buffer;
363         if (!(buffer instanceof content_buffer))
364             throw new Error("Current buffer is of invalid type");
365         // -- Charset of content area of focusedWindow
366         var focusedWindow = buffer.focused_frame;
367         if (focusedWindow)
368             return focusedWindow.document.characterSet;
369         else
370             return null;
371     });
374 I.content_selection = interactive_method(
375     $sync = function (ctx) {
376         // -- Selection of content area of focusedWindow
377         var focusedWindow = this.buffers.current.focused_frame;
378         return focusedWindow.getSelection ();
379     });
381 function overlink_update_status(buffer, text) {
382     if (text.length > 0)
383         buffer.window.minibuffer.show("Link: " + text);
384     else
385         buffer.window.minibuffer.show("");
388 define_global_mode("overlink_mode",
389                    function () {
390                        add_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
391                    },
392                    function () {
393                        remove_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
394                    });
396 overlink_mode(true);
399 function go_back (b, prefix)
401     if (prefix < 0)
402         go_forward(b, -prefix);
404     check_buffer(b, content_buffer);
406     if (b.web_navigation.canGoBack)
407     {
408         var hist = b.web_navigation.sessionHistory;
409         var idx = hist.index - prefix;
410         if (idx < 0)
411             idx = 0;
412         b.web_navigation.gotoIndex(idx);
413     } else
414         throw interactive_error("Can't go back");
416 interactive(
417     "go-back",
418     "Go back in the session history for the current buffer.",
419     function (I) {go_back(I.buffer, I.p);});
421 function go_forward (b, prefix)
423     if (prefix < 0)
424         go_back(b, -prefix);
426     check_buffer(b, content_buffer);
428     if (b.web_navigation.canGoForward)
429     {
430         var hist = b.web_navigation.sessionHistory;
431         var idx = hist.index + prefix;
432         if (idx >= hist.count) idx = hist.count-1;
433         b.web_navigation.gotoIndex(idx);
434     } else
435         throw interactive_error("Can't go forward");
437 interactive("go-forward",
438             "Go back in the session hisory for the current buffer.",
439             function (I) {go_forward(I.buffer, I.p);});
441 function stop_loading (b)
443     check_buffer(b, content_buffer);
444     b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
446 interactive("stop-loading",
447             "Stop loading the current document.",
448             function (I) {stop_loading(I.buffer);});
450 function reload (b, bypass_cache)
452     check_buffer(b, content_buffer);
453     var flags = bypass_cache != null ?
454         Ci.nsIWebNavigation.LOAD_FLAGS_NONE : Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
455     b.web_navigation.reload(flags);
457 interactive("reload",
458             "Reload the current document.\n" +
459             "If a prefix argument is specified, the cache is bypassed.",
460             function (I) {reload(I.buffer, I.P);});
463  * browserDOMWindow: intercept window opening
464  */
465 function initialize_browser_dom_window(window) {
466     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
467         new browser_dom_window(window);
470 define_variable("browser_default_open_target", OPEN_NEW_BUFFER, "Specifies how new window requests by content pages (e.g. by window.open from JavaScript or by using the target attribute of anchor and form elements) will be handled.  This will generally be `OPEN_NEW_BUFFER', `OPEN_NEW_BUFFER_BACKGROUND', or `OPEN_NEW_WINDOW'.");
472 function browser_dom_window(window) {
473     this.window = window;
474     this.next_target = null;
476 browser_dom_window.prototype = {
477     QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
479     openURI : function(aURI, aOpener, aWhere, aContext) {
481         // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
482         var target = this.next_target;
483         if (target == null || target == FOLLOW_DEFAULT)
484             target = browser_default_open_target;
485         this.next_target = null;
487         /* Determine the opener buffer */
488         var opener_buffer = get_buffer_from_frame(this.window, aOpener.top);
489         var config = opener_buffer ? opener_buffer.configuration : null;
491         switch (browser_default_open_target) {
492         case OPEN_CURRENT_BUFFER:
493         case FOLLOW_TOP_FRAME:
494             return aOpener.top;
495         case FOLLOW_CURRENT_FRAME:
496             return aOpener;
497         case OPEN_NEW_BUFFER:
498             var buffer = new content_buffer(this.window, null /* element */, $configuration = config);
499             this.window.buffers.current = buffer;
500             return buffer.top_frame;
501         case OPEN_NEW_BUFFER_BACKGROUND:
502             var buffer = new content_buffer(this.window, null /* element */, $configuration = config);
503             return buffer.top_frame;
504         case OPEN_NEW_WINDOW:
505         default: /* shouldn't be needed */
507             /* We don't call make_window here, because that will result
508              * in the URL being loaded as the top-level document,
509              * instead of within a browser buffer.  Instead, we can
510              * rely on Mozilla using browser.chromeURL. */
511             window_set_extra_arguments(
512                 {initial_buffer_creator: buffer_creator(content_buffer, $configuration = config)}
513             );
514             return null;
515         }
516     }
519 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
521 define_keywords("$enable", "$disable", "$doc");
522 function define_page_mode(name, display_name) {
523     keywords(arguments);
524     var enable = arguments.$enable;
525     var disable = arguments.$disable;
526     var doc = arguments.$doc;
527     define_buffer_mode(name, display_name,
528                        $class = "page_mode",
529                        $enable = enable,
530                        $disable = function (buffer) {
531                            if (disable)
532                                disable(buffer);
533                            buffer.local_variables = {};
534                            buffer.default_browser_object_classes = {};
535                        },
536                        $doc = doc);
538 ignore_function_for_get_caller_source_code_reference("define_page_mode");
540 define_variable("auto_mode_list", [], "A list of mappings from URI regular expressions to page modes.");
541 function page_mode_auto_update(buffer) {
542     var uri = buffer.current_URI.spec;
543     var mode = predicate_alist_match(auto_mode_list, uri);
544     if (mode)
545         mode(buffer, true);
546     else if (buffer.page_mode)
547         conkeror[buffer.page_mode](buffer, false);
550 add_hook("content_buffer_location_change_hook", page_mode_auto_update);