Prepare new Debian package
[conkeror.git] / modules / content-buffer.js
blob0f8b86e5829da6550be17b1a898269a9cc521c25
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2009 John J. Foerch
4  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
10 require("buffer.js");
11 require("load-spec.js");
13 define_variable("homepage", "chrome://conkeror-help/content/help.html",
14                 "The url loaded by default for new content buffers.");
16 define_buffer_local_hook("content_buffer_finished_loading_hook");
17 define_buffer_local_hook("content_buffer_started_loading_hook");
18 define_buffer_local_hook("content_buffer_progress_change_hook");
19 define_buffer_local_hook("content_buffer_location_change_hook");
20 define_buffer_local_hook("content_buffer_status_change_hook");
21 define_buffer_local_hook("content_buffer_focus_change_hook");
22 define_buffer_local_hook("content_buffer_overlink_change_hook");
23 define_buffer_local_hook("content_buffer_dom_link_added_hook");
24 define_buffer_local_hook("content_buffer_popup_blocked_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");
34 define_input_mode("text", "content_buffer_text_keymap", $display_name = "input:TEXT");
35 define_input_mode("textarea", "content_buffer_textarea_keymap", $display_name = "input:TEXTAREA");
36 define_input_mode("richedit", "content_buffer_richedit_keymap", $display_name = "input:RICHEDIT");
37 define_input_mode("select", "content_buffer_select_keymap", $display_name = "input:SELECT");
38 define_input_mode("checkbox", "content_buffer_checkbox_keymap", $display_name = "input:CHECKBOX/RADIOBUTTON");
39 define_input_mode("button", "content_buffer_button_keymap", $display_name = "input:BUTTON");
41 function content_buffer_modality (buffer) {
42     var elem = buffer.focused_element;
43     buffer.keymaps.push(content_buffer_normal_keymap);
44     if (elem) {
45         let p = elem.parentNode;
46         while (p && !(p instanceof Ci.nsIDOMHTMLFormElement))
47             p = p.parentNode;
48         if (p)
49             buffer.keymaps.push(content_buffer_form_keymap);
50     }
51     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
52         switch ((elem.getAttribute("type") || "").toLowerCase()) {
53         case "checkbox":
54             checkbox_input_mode(buffer, true);
55             break;
56         case "radio":
57             checkbox_input_mode(buffer, true);
58             break;
59         case "submit":
60             button_input_mode(buffer, true);
61             break;
62         case "reset":
63             button_input_mode(buffer, true);
64             break;
65         default:
66             text_input_mode(buffer, true);
67             break;
68         }
69         return;
70     }
71     if (elem instanceof Ci.nsIDOMHTMLTextAreaElement) {
72         textarea_input_mode(buffer, true);
73         return;
74     }
75     if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
76         select_input_mode(buffer, true);
77         return;
78     }
79     if (elem instanceof Ci.nsIDOMHTMLAnchorElement) {
80         buffer.keymaps.push(content_buffer_anchor_keymap);
81         return;
82     }
83     var frame = buffer.focused_frame;
84     if (frame && frame.document.designMode == "on") {
85         richedit_input_mode(buffer, true);
86         return;
87     }
88     while (elem) {
89         switch (elem.contentEditable) {
90         case "true":
91             richedit_input_mode(buffer, true);
92             return;
93         case "false":
94             return;
95         default: // "inherit"
96             elem = elem.parentNode;
97         }
98     }
102 /* If browser is null, create a new browser */
103 define_keywords("$load");
104 function content_buffer (window, element) {
105     keywords(arguments);
106     this.constructor_begin();
107     try {
108         conkeror.buffer.call(this, window, element, forward_keywords(arguments));
110         this.browser.addProgressListener(this);
111         var buffer = this;
112         this.browser.addEventListener("DOMTitleChanged", function (event) {
113             buffer_title_change_hook.run(buffer);
114         }, true /* capture */);
116         this.browser.addEventListener("scroll", function (event) {
117             buffer_scroll_hook.run(buffer);
118         }, true /* capture */);
120         this.browser.addEventListener("focus", function (event) {
121             content_buffer_focus_change_hook.run(buffer, event);
122         }, true /* capture */);
124         this.browser.addEventListener("mouseover", function (event) {
125             var node = event.target;
126             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
127                 node = node.parentNode;
128             if (node) {
129                 content_buffer_overlink_change_hook.run(buffer, node.href);
130                 buffer.current_overlink = event.target;
131             }
132         }, true /* capture */);
134         this.browser.addEventListener("mouseout", function (event) {
135             if (buffer.current_overlink == event.target) {
136                 buffer.current_overlink = null;
137                 content_buffer_overlink_change_hook.run(buffer, "");
138             }
139         }, true /* capture */);
141         // initialize buffer.current_overlink in case mouseout happens
142         // before mouseover.
143         buffer.current_overlink = null;
145         this.browser.addEventListener("DOMLinkAdded", function (event) {
146             content_buffer_dom_link_added_hook.run(buffer, event);
147         }, true /* capture */);
149         this.browser.addEventListener("DOMPopupBlocked", function (event) {
150             dumpln("Blocked popup: " + event.popupWindowURI.spec);
151             content_buffer_popup_blocked_hook.run(buffer, event);
152         }, true /* capture */);
154         this.ignore_initial_blank = true;
156         var lspec = arguments.$load;
157         if (lspec) {
158             if (lspec.url == "about:blank")
159                 this.ignore_initial_blank = false;
160             else {
161                 /* Ensure that an existing load of about:blank is stopped */
162                 this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
164                 this.load(lspec);
165             }
166         }
168         this.modalities.push(content_buffer_modality);
170     } finally {
171         this.constructor_end();
172     }
174 content_buffer.prototype = {
175     constructor : content_buffer,
177     destroy: function () {
178         this.browser.removeProgressListener(this);
179         buffer.prototype.destroy.call(this);
180     },
182     get scrollX () { return this.top_frame.scrollX; },
183     get scrollY () { return this.top_frame.scrollY; },
184     get scrollMaxX () { return this.top_frame.scrollMaxX; },
185     get scrollMaxY () { return this.top_frame.scrollMaxY; },
187     /* Used to display the correct URI when the buffer opens initially
188      * even before loading has progressed far enough for currentURI to
189      * contain the correct URI. */
190     _display_uri : null,
192     get display_uri_string () {
193         if (this._display_uri)
194             return this._display_uri;
195         if (this.current_uri)
196             return this.current_uri.spec;
197         return "";
198     },
200     get title () { return this.browser.contentTitle; },
201     get description () { return this.display_uri_string; },
203     load : function (load_spec) {
204         apply_load_spec(this, load_spec);
205     },
207     _request_count: 0,
209     loading : false,
211     /* nsIWebProgressListener */
212     QueryInterface: generate_QI(Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference),
214     // This method is called to indicate state changes.
215     onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
217         const WPL = Components.interfaces.nsIWebProgressListener;
219         var flagstr = "";
220         if (aStateFlags & WPL.STATE_START)
221             flagstr += ",start";
222         if (aStateFlags & WPL.STATE_STOP)
223             flagstr += ",stop";
224         if (aStateFlags & WPL.STATE_IS_REQUEST)
225             flagstr += ",request";
226         if (aStateFlags & WPL.STATE_IS_DOCUMENT)
227             flagstr += ",document";
228         if (aStateFlags & WPL.STATE_IS_NETWORK)
229             flagstr += ",network";
230         if (aStateFlags & WPL.STATE_IS_WINDOW)
231             flagstr += ",window";
232         dumpln("onStateChange: " + flagstr + ", status: " + aStatus);
234         if (!aRequest)
235             return;
237         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
238             this._request_count++;
239         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
240             const NS_ERROR_UNKNOWN_HOST = 2152398878;
241             if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
242                 // to prevent bug 235825: wait for the request handled
243                 // by the automatic keyword resolver
244                 return;
245             }
246             // since we (try to) only handle STATE_STOP of the last request,
247             // the count of open requests should now be 0
248             this._request_count = 0;
249         }
251         if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
252             aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
253             // It's okay to clear what the user typed when we start
254             // loading a document. If the user types, this counter gets
255             // set to zero, if the document load ends without an
256             // onLocationChange, this counter gets decremented
257             // (so we keep it while switching tabs after failed loads)
258             //dumpln("*** started loading");
259             this.loading = true;
260             content_buffer_started_loading_hook.run(this);
261         } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
262                    aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
263             if (this.loading == true) {
264                 //dumpln("*** finished loading");
265                 this.loading = false;
266                 content_buffer_finished_loading_hook.run(this);
267             }
268         }
270         if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
271                            Ci.nsIWebProgressListener.STATE_START)) {
272             if (!this.loading)
273                 this.set_default_message("Done");
274         }
275     },
277     /* This method is called to indicate progress changes for the currently
278        loading page. */
279     onProgressChange: function (webProgress, request, curSelf, maxSelf,
280                                 curTotal, maxTotal) {
281         content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
282     },
284     /* This method is called to indicate a change to the current location.
285        The url can be gotten as location.spec. */
286     onLocationChange : function (webProgress, request, location) {
287         /* Attempt to ignore onLocationChange calls due to the initial
288          * loading of about:blank by all xul:browser elements. */
289         if (location.spec == "about:blank" && this.ignore_initial_blank)
290             return;
292         this.ignore_initial_blank = false;
294         //dumpln("spec: " + location.spec  +" ;;; " + this.display_uri_string);
295         /* Use the real location URI now */
296         this._display_uri = null;
297         this.set_input_mode();
298         content_buffer_location_change_hook.run(this, request, location);
299         buffer_description_change_hook.run(this);
300     },
302     // This method is called to indicate a status changes for the currently
303     // loading page.  The message is already formatted for display.
304     // Status messages could be displayed in the minibuffer output area.
305     onStatusChange: function (webProgress, request, status, msg) {
306         this.set_default_message(msg);
307         content_buffer_status_change_hook.run(this, request, status, msg);
308     },
310     // This method is called when the security state of the browser changes.
311     onSecurityChange: function (webProgress, request, state) {
312         /* FIXME: currently this isn't used */
314         /*
315         const WPL = Components.interfaces.nsIWebProgressListener;
317         if (state & WPL.STATE_IS_INSECURE) {
318             // update visual indicator
319         } else {
320             var level = "unknown";
321             if (state & WPL.STATE_IS_SECURE) {
322                 if (state & WPL.STATE_SECURE_HIGH)
323                     level = "high";
324                 else if (state & WPL.STATE_SECURE_MED)
325                     level = "medium";
326                 else if (state & WPL.STATE_SECURE_LOW)
327                     level = "low";
328             } else if (state & WPL_STATE_IS_BROKEN) {
329                 level = "mixed";
330             }
331             // provide a visual indicator of the security state here.
332         }
333         */
334     },
336     /* Inherit from buffer */
338     __proto__ : buffer.prototype
342 add_hook("current_content_buffer_finished_loading_hook",
343          function (buffer) {
344                  buffer.window.minibuffer.show("Done");
345          });
347 add_hook("current_content_buffer_status_change_hook",
348          function (buffer, request, status, msg) {
349              buffer.set_default_message(msg);
350          });
354 define_variable("read_url_handler_list", [],
355     "A list of handler functions which transform a typed url into a valid " +
356     "url or webjump.  If the typed input is not valid then each function " +
357     "on this list is tried in turn.  The handler function is called with " +
358     "a single string argument and it should return either a string or " +
359     "null.  The result of the first function on the list that returns a " +
360     "string is used in place of the input.");
363  * read_url_make_default_webjump_handler returns a function that
364  * transforms any input into the given webjump.  It should be the last
365  * handler on read_url_handler_list (because any input is
366  * accepted).
367  */
368 function read_url_make_default_webjump_handler (default_webjump) {
369     return function (input) {
370         return default_webjump + " " + input;
371     };
375  * read_url_make_blank_url_handler returns a function that replaces a
376  * blank (empty) input with the given url (or webjump).  The url may
377  * perform some function, eg. "javascript:location.reload()".
378  */
379 function read_url_make_blank_url_handler (url) {
380     return function (input) {
381         if (input.length == 0)
382             return url;
383         return null;
384     };
387 minibuffer.prototype.try_read_url_handlers = function (input) {
388     var result;
389     for (var i = 0; i < read_url_handler_list.length; ++i) {
390         if ((result = read_url_handler_list[i](input)))
391             return result;
392     }
393     return input;
396 define_variable("url_completion_use_webjumps", true,
397     "Specifies whether URL completion should complete webjumps.");
399 define_variable("url_completion_use_bookmarks", true,
400     "Specifies whether URL completion should complete bookmarks.");
402 define_variable("url_completion_use_history", false,
403     "Specifies whether URL completion should complete using browser "+
404     "history.");
406 define_variable("minibuffer_read_url_select_initial", true,
407     "Specifies whether a URL  presented in the minibuffer for editing "+
408     "should be selected.  This affects find-alternate-url.");
411 minibuffer_auto_complete_preferences["url"] = true;
412 minibuffer.prototype.read_url = function () {
413     keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
414              $use_webjumps = url_completion_use_webjumps,
415              $use_history = url_completion_use_history,
416              $use_bookmarks = url_completion_use_bookmarks);
417     var completer = url_completer($use_webjumps = arguments.$use_webjumps,
418         $use_bookmarks = arguments.$use_bookmarks,
419         $use_history = arguments.$use_history);
420     var result = yield this.read(
421         $prompt = arguments.$prompt,
422         $history = arguments.$history,
423         $completer = completer,
424         $initial_value = arguments.$initial_value,
425         $auto_complete = "url",
426         $select = minibuffer_read_url_select_initial,
427         $match_required = false);
428     if (!possibly_valid_url(result) && !get_webjump(result))
429         result = this.try_read_url_handlers(result);
430     if (result == "") // well-formedness check. (could be better!)
431         throw ("invalid url or webjump (\""+ result +"\")");
432     yield co_return(result);
435 I.content_charset = interactive_method(
436     $sync = function (ctx) {
437         var buffer = ctx.buffer;
438         if (!(buffer instanceof content_buffer))
439             throw new Error("Current buffer is of invalid type");
440         // -- Charset of content area of focusedWindow
441         var focusedWindow = buffer.focused_frame;
442         if (focusedWindow)
443             return focusedWindow.document.characterSet;
444         else
445             return null;
446     });
449 I.content_selection = interactive_method(
450     $sync = function (ctx) {
451         // -- Selection of content area of focusedWindow
452         var focusedWindow = this.buffers.current.focused_frame;
453         return focusedWindow.getSelection ();
454     });
456 function overlink_update_status (buffer, text) {
457     if (text.length > 0)
458         buffer.window.minibuffer.show("Link: " + text);
459     else
460         buffer.window.minibuffer.show("");
463 define_global_mode("overlink_mode", function () {
464     add_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
465 }, function () {
466     remove_hook("current_content_buffer_overlink_change_hook", overlink_update_status);
469 overlink_mode(true);
472 function go_back (b, prefix) {
473     if (prefix < 0)
474         go_forward(b, -prefix);
476     check_buffer(b, content_buffer);
478     if (b.web_navigation.canGoBack) {
479         var hist = b.web_navigation.sessionHistory;
480         var idx = hist.index - prefix;
481         if (idx < 0)
482             idx = 0;
483         b.web_navigation.gotoIndex(idx);
484     } else
485         throw interactive_error("Can't go back");
488 interactive("back",
489     "Go back in the session history for the current buffer.",
490     function (I) {go_back(I.buffer, I.p);});
493 function go_forward (b, prefix) {
494     if (prefix < 0)
495         go_back(b, -prefix);
497     check_buffer(b, content_buffer);
499     if (b.web_navigation.canGoForward) {
500         var hist = b.web_navigation.sessionHistory;
501         var idx = hist.index + prefix;
502         if (idx >= hist.count) idx = hist.count-1;
503         b.web_navigation.gotoIndex(idx);
504     } else
505         throw interactive_error("Can't go forward");
508 interactive("forward",
509             "Go forward in the session history for the current buffer.",
510             function (I) {go_forward(I.buffer, I.p);});
513 function stop_loading (b) {
514     check_buffer(b, content_buffer);
515     b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
518 interactive("stop-loading",
519             "Stop loading the current document.",
520             function (I) {stop_loading(I.buffer);});
523 function reload (b, bypass_cache, element, forced_charset) {
524     check_buffer(b, content_buffer);
525     if (element) {
526         if (element instanceof Ci.nsIDOMHTMLImageElement) {
527             try {
528                 var cache = Cc['@mozilla.org/image/cache;1']
529                     .getService(Ci.imgICache);
530                 cache.removeEntry(make_uri(element.src));
531             } catch (e) {}
532         }
533         element.parentNode.replaceChild(element.cloneNode(true), element);
534     } else {
535         var flags = bypass_cache == null ?
536             Ci.nsIWebNavigation.LOAD_FLAGS_NONE :
537             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
539         if (! forced_charset && forced_charset_list)
540             forced_charset = predicate_alist_match(forced_charset_list,
541                                                    b.current_uri.spec);
543         if (forced_charset) {
544             try {
545                 var atomservice = Cc['@mozilla.org/atom-service;1']
546                     .getService(Ci.nsIAtomService);
547                 b.web_navigation.documentCharsetInfo.forcedCharset =
548                     atomservice.getAtom(forced_charset);
549             } catch (e) {}
550         }
551         b.web_navigation.reload(flags);
552     }
555 interactive("reload",
556     "Reload the current document.\n" +
557     "If a prefix argument is specified, the cache is bypassed.  If a "+
558     "DOM node is supplied via browser object, that node will be "+
559     "reloaded.",
560     function (I) {
561         check_buffer(I.buffer, content_buffer);
562         var element = yield read_browser_object(I);
563         reload(I.buffer, I.P, element, I.forced_charset);
564     });
567  * browserDOMWindow: intercept window opening
568  */
569 function initialize_browser_dom_window (window) {
570     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
571         new browser_dom_window(window);
574 define_variable("browser_default_open_target", OPEN_NEW_BUFFER,
575     "Specifies how new window requests by content pages (e.g. by "+
576     "window.open from JavaScript or by using the target attribute of "+
577     "anchor and form elements) will be handled.  This will generally "+
578     "be `OPEN_NEW_BUFFER', `OPEN_NEW_BUFFER_BACKGROUND', or "+
579     "`OPEN_NEW_WINDOW'.");
582 function browser_dom_window (window) {
583     this.window = window;
584     this.next_target = null;
586 browser_dom_window.prototype = {
587     QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
589     openURI : function (aURI, aOpener, aWhere, aContext) {
591         // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
592         var target = this.next_target;
593         if (target == null || target == FOLLOW_DEFAULT)
594             target = browser_default_open_target;
595         this.next_target = null;
597         /* Determine the opener buffer */
598         var opener = get_buffer_from_frame(this.window, aOpener);
600         switch (browser_default_open_target) {
601         case OPEN_CURRENT_BUFFER:
602             return aOpener.top;
603         case FOLLOW_CURRENT_FRAME:
604             return aOpener;
605         case OPEN_NEW_BUFFER:
606             var buffer = new content_buffer(this.window, null /* element */, $opener = opener);
607             this.window.buffers.current = buffer;
608             return buffer.top_frame;
609         case OPEN_NEW_BUFFER_BACKGROUND:
610             var buffer = new content_buffer(this.window, null /* element */, $opener = opener);
611             return buffer.top_frame;
612         case OPEN_NEW_WINDOW:
613         default: /* shouldn't be needed */
615             /* We don't call make_window here, because that will result
616              * in the URL being loaded as the top-level document,
617              * instead of within a browser buffer.  Instead, we can
618              * rely on Mozilla using browser.chromeURL. */
619             window_set_extra_arguments(
620                 {initial_buffer_creator: buffer_creator(content_buffer, $opener = opener)}
621             );
622             return null;
623         }
624     }
627 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
629 define_keywords("$display_name", "$enable", "$disable", "$doc");
630 function define_page_mode (name) {
631     keywords(arguments);
632     var display_name = arguments.$display_name;
633     var enable = arguments.$enable;
634     var disable = arguments.$disable;
635     var doc = arguments.$doc;
636     define_buffer_mode(name,
637                        $display_name = display_name,
638                        $class = "page_mode",
639                        $enable = function (buffer) {
640                            buffer.page = {
641                                local: { __proto__: buffer.local }
642                            };
643                            if (enable)
644                                enable(buffer);
645                            buffer.set_input_mode();
646                        },
647                        $disable = function (buffer) {
648                            if (disable)
649                                disable(buffer);
650                            buffer.page = null;
651                            buffer.default_browser_object_classes = {};
652                            buffer.set_input_mode();
653                        },
654                        $doc = doc);
656 ignore_function_for_get_caller_source_code_reference("define_page_mode");
659 define_variable("auto_mode_list", [],
660     "A list of mappings from URI regular expressions to page modes.");
662 function page_mode_auto_update (buffer) {
663     var uri = buffer.current_uri.spec;
664     var mode = predicate_alist_match(auto_mode_list, uri);
665     if (mode)
666         mode(buffer, true);
667     else if (buffer.page_mode)
668         conkeror[buffer.page_mode](buffer, false);
671 add_hook("content_buffer_location_change_hook", page_mode_auto_update);