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