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