2 * (C) Copyright 2004-2007 Shawn Betts
3 * (C) Copyright 2007-2009,2011-2012 John J. Foerch
4 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
6 * Use, modification, and distribution are subject to the terms specified in the
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(
41 buffer.keymaps.push(m[tag]);
45 push_keymaps('normal');
47 let p = elem.parentNode;
48 while (p && !(p instanceof Ci.nsIDOMHTMLFormElement))
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);
58 return push_keymaps('text');
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)
71 return push_keymaps('embed');
73 var frame = buffer.focused_frame;
74 if (frame && frame.document.designMode &&
75 frame.document.designMode == "on")
77 return push_keymaps('richedit');
80 switch (elem.contentEditable) {
82 return push_keymaps('richedit');
86 elem = elem.parentNode;
93 define_keywords("$load");
94 function content_buffer (window) {
96 this.constructor_begin();
98 conkeror.buffer.call(this, window, forward_keywords(arguments));
100 this.browser.addProgressListener(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.page_modes = [];
125 this.ignore_initial_blank = true;
127 var lspec = arguments.$load;
129 if (lspec.url == "about:blank")
130 this.ignore_initial_blank = false;
132 /* Ensure that an existing load of about:blank is stopped */
133 this.web_navigation.stop(Ci.nsIWebNavigation.STOP_ALL);
139 this.modalities.push(content_buffer_modality);
140 this.content_modalities = [{
141 normal: content_buffer_normal_keymap,
142 form: content_buffer_form_keymap,
143 checkbox: content_buffer_checkbox_keymap,
144 radio: content_buffer_checkbox_keymap,
145 submit: content_buffer_button_keymap,
146 reset: content_buffer_button_keymap,
147 text: content_buffer_text_keymap,
148 textarea: content_buffer_textarea_keymap,
149 select: content_buffer_select_keymap,
150 anchor: content_buffer_anchor_keymap,
151 button: content_buffer_button_keymap,
152 embed: content_buffer_embed_keymap,
153 richedit: content_buffer_richedit_keymap
156 this.constructor_end();
159 content_buffer.prototype = {
160 constructor: content_buffer,
162 destroy: function () {
163 this.browser.removeProgressListener(this);
164 buffer.prototype.destroy.call(this);
167 content_modalities: null,
169 get scrollX () { return this.top_frame.scrollX; },
170 get scrollY () { return this.top_frame.scrollY; },
171 get scrollMaxX () { return this.top_frame.scrollMaxX; },
172 get scrollMaxY () { return this.top_frame.scrollMaxY; },
174 /* Used to display the correct URI when the buffer opens initially
175 * even before loading has progressed far enough for currentURI to
176 * contain the correct URI. */
179 get display_uri_string () {
180 if (this._display_uri)
181 return this._display_uri;
182 if (this.current_uri)
183 return this.current_uri.spec;
187 get title () { return this.browser.contentTitle; },
188 get description () { return this.display_uri_string; },
190 load: function (load_spec) {
191 apply_load_spec(this, load_spec);
198 // nsIWebProgressListener interface
200 QueryInterface: generate_QI(Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference),
202 // This method is called to indicate state changes.
203 onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
206 if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
207 this._request_count++;
208 } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
209 const NS_ERROR_UNKNOWN_HOST = 2152398878;
210 if (--this._request_count > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
211 // to prevent bug 235825: wait for the request handled
212 // by the automatic keyword resolver
215 // since we (try to) only handle STATE_STOP of the last request,
216 // the count of open requests should now be 0
217 this._request_count = 0;
220 if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
221 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
222 // It's okay to clear what the user typed when we start
223 // loading a document. If the user types, this counter gets
224 // set to zero, if the document load ends without an
225 // onLocationChange, this counter gets decremented
226 // (so we keep it while switching tabs after failed loads)
228 content_buffer_started_loading_hook.run(this);
229 } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
230 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
231 if (this.loading == true) {
232 this.loading = false;
233 content_buffer_finished_loading_hook.run(this);
237 if (aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP |
238 Ci.nsIWebProgressListener.STATE_START)) {
240 this.set_default_message("Done");
244 // This method is called to indicate progress changes for the
245 // currently loading page.
246 onProgressChange: function (webProgress, request, curSelf, maxSelf,
249 content_buffer_progress_change_hook.run(this, request, curSelf, maxSelf, curTotal, maxTotal);
252 // This method is called to indicate a change to the current location.
253 // The url can be gotten as location.spec.
254 onLocationChange: function (webProgress, request, location) {
255 /* Attempt to ignore onLocationChange calls due to the initial
256 * loading of about:blank by all xul:browser elements. */
257 if (location.spec == "about:blank" && this.ignore_initial_blank)
260 this.ignore_initial_blank = false;
262 //dumpln("spec: " + location.spec +" ;;; " + this.display_uri_string);
263 /* Use the real location URI now */
264 this._display_uri = null;
265 this.set_input_mode();
267 local: { __proto__: this.local }
269 this.default_browser_object_classes = {};
270 content_buffer_location_change_hook.run(this, request, location);
271 buffer_description_change_hook.run(this);
274 // This method is called to indicate a status changes for the currently
275 // loading page. The message is already formatted for display.
276 // Status messages could be displayed in the minibuffer output area.
277 onStatusChange: function (webProgress, request, status, msg) {
278 this.set_default_message(msg);
279 content_buffer_status_change_hook.run(this, request, status, msg);
282 // This method is called when the security state of the browser changes.
283 onSecurityChange: function (webProgress, request, state) {
284 //FIXME: implement this.
286 const WPL = Components.interfaces.nsIWebProgressListener;
288 if (state & WPL.STATE_IS_INSECURE) {
289 // update visual indicator
291 var level = "unknown";
292 if (state & WPL.STATE_IS_SECURE) {
293 if (state & WPL.STATE_SECURE_HIGH)
295 else if (state & WPL.STATE_SECURE_MED)
297 else if (state & WPL.STATE_SECURE_LOW)
299 } else if (state & WPL_STATE_IS_BROKEN) {
302 // provide a visual indicator of the security state here.
307 /* Inherit from buffer */
308 __proto__: buffer.prototype
312 add_hook("current_content_buffer_finished_loading_hook",
314 buffer.window.minibuffer.show("Done");
317 add_hook("current_content_buffer_status_change_hook",
318 function (buffer, request, status, msg) {
319 buffer.set_default_message(msg);
324 define_variable("read_url_handler_list", [],
325 "A list of handler functions which transform a typed url into a valid " +
326 "url or webjump. If the typed input is not valid then each function " +
327 "on this list is tried in turn. The handler function is called with " +
328 "a single string argument and it should return either a string or " +
329 "null. The result of the first function on the list that returns a " +
330 "string is used in place of the input.");
333 * read_url_make_default_webjump_handler returns a function that
334 * transforms any input into the given webjump. It should be the last
335 * handler on read_url_handler_list (because any input is
338 function read_url_make_default_webjump_handler (default_webjump) {
339 return function (input) {
340 return default_webjump + " " + input;
345 * read_url_make_blank_url_handler returns a function that replaces a
346 * blank (empty) input with the given url (or webjump). The url may
347 * perform some function, eg. "javascript:location.reload()".
349 function read_url_make_blank_url_handler (url) {
350 return function (input) {
351 if (input.length == 0)
357 minibuffer.prototype.try_read_url_handlers = function (input) {
359 for (var i = 0; i < read_url_handler_list.length; ++i) {
360 if ((result = read_url_handler_list[i](input)))
366 define_variable("url_completion_use_webjumps", true,
367 "Specifies whether URL completion should complete webjumps.");
369 define_variable("url_completion_use_bookmarks", true,
370 "Specifies whether URL completion should complete bookmarks.");
372 define_variable("url_completion_use_history", false,
373 "Specifies whether URL completion should complete using browser "+
376 define_variable("url_completion_sort_order", "visitcount_descending",
377 "Gives the default sort order for history and bookmark completion.\n"+
378 "The value is given as a string, and the available options include: "+
379 "'none', 'title_ascending', 'date_ascending', 'uri_ascending', "+
380 "'visitcount_ascending', 'keyword_ascending', 'dateadded_ascending', "+
381 "'lastmodified_ascending', 'tags_ascending', and 'annotation_ascending'. "+
382 "For every 'ascending' option, there is a corresponding 'descending' "+
383 "option. Additionally, with XULRunner 6 and later, the options "+
384 "'frecency_ascending' and 'frecency_descending' are available. See also "+
385 "<https://developer.mozilla.org/en/NsINavHistoryQueryOptions#Sorting_methods>.");
387 define_variable("minibuffer_read_url_select_initial", true,
388 "Specifies whether a URL presented in the minibuffer for editing "+
389 "should be selected. This affects find-alternate-url.");
392 minibuffer_auto_complete_preferences["url"] = true;
393 minibuffer.prototype.read_url = function () {
394 keywords(arguments, $prompt = "URL:", $history = "url", $initial_value = "",
395 $use_webjumps = url_completion_use_webjumps,
396 $use_history = url_completion_use_history,
397 $use_bookmarks = url_completion_use_bookmarks,
398 $sort_order = url_completion_sort_order);
399 var completer = url_completer($use_webjumps = arguments.$use_webjumps,
400 $use_bookmarks = arguments.$use_bookmarks,
401 $use_history = arguments.$use_history,
402 $sort_order = arguments.$sort_order);
403 var result = yield this.read(
404 $prompt = arguments.$prompt,
405 $history = arguments.$history,
406 $completer = completer,
407 $initial_value = arguments.$initial_value,
408 $auto_complete = "url",
409 $select = minibuffer_read_url_select_initial,
410 $match_required = false);
411 if (!possibly_valid_url(result) && !get_webjump(result))
412 result = this.try_read_url_handlers(result);
413 if (result == "") // well-formedness check. (could be better!)
414 throw ("invalid url or webjump (\""+ result +"\")");
415 yield co_return(load_spec(result));
422 function overlink_update_status (buffer, node) {
423 if (node && node.href.length > 0)
424 buffer.window.minibuffer.show("Link: " + node.href);
426 buffer.window.minibuffer.clear();
429 function overlink_predicate (node) {
430 while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
431 node = node.parentNode;
435 function overlink_initialize (buffer) {
436 buffer.current_overlink = null;
437 buffer.overlink_mouseover = function (event) {
438 if (buffer != buffer.window.buffers.current ||
439 event.target == buffer.browser)
443 var node = overlink_predicate(event.target);
445 buffer.current_overlink = event.target;
446 overlink_update_status(buffer, node);
449 buffer.overlink_mouseout = function (event) {
450 if (buffer != buffer.window.buffers.current)
452 if (buffer.current_overlink == event.target) {
453 buffer.current_overlink = null;
454 overlink_update_status(buffer, null);
457 buffer.browser.addEventListener("mouseover", buffer.overlink_mouseover, true);
458 buffer.browser.addEventListener("mouseout", buffer.overlink_mouseout, true);
461 define_global_mode("overlink_mode",
463 add_hook("create_buffer_hook", overlink_initialize);
464 for_each_buffer(overlink_initialize);
466 function disable () {
467 remove_hook("create_buffer_hook", overlink_initialize);
468 for_each_buffer(function (b) {
469 b.browser.removeEventListener("mouseover", b.overlink_mouseover, true);
470 b.browser.removeEventListener("mouseout", b.overlink_mouseout, true);
471 delete b.current_overlink;
472 delete b.overlink_mouseover;
473 delete b.overlink_mouseout;
481 * Navigation Commands
483 function go_back (b, prefix) {
485 go_forward(b, -prefix);
487 check_buffer(b, content_buffer);
489 if (b.web_navigation.canGoBack) {
490 var hist = b.web_navigation.sessionHistory;
491 var idx = hist.index - prefix;
494 b.web_navigation.gotoIndex(idx);
496 throw interactive_error("Can't go back");
500 "Go back in the session history for the current buffer.",
501 function (I) {go_back(I.buffer, I.p);});
504 function go_forward (b, prefix) {
508 check_buffer(b, content_buffer);
510 if (b.web_navigation.canGoForward) {
511 var hist = b.web_navigation.sessionHistory;
512 var idx = hist.index + prefix;
513 if (idx >= hist.count) idx = hist.count-1;
514 b.web_navigation.gotoIndex(idx);
516 throw interactive_error("Can't go forward");
519 interactive("forward",
520 "Go forward in the session history for the current buffer.",
521 function (I) {go_forward(I.buffer, I.p);});
524 function stop_loading (b) {
525 check_buffer(b, content_buffer);
526 b.web_navigation.stop(Ci.nsIWebNavigation.STOP_NETWORK);
529 interactive("stop-loading",
530 "Stop loading the current document.",
531 function (I) {stop_loading(I.buffer);});
534 function reload (b, bypass_cache, element, forced_charset) {
535 check_buffer(b, content_buffer);
537 if (element instanceof Ci.nsIDOMHTMLImageElement) {
539 var cache = Cc['@mozilla.org/image/cache;1']
540 .getService(Ci.imgICache);
541 cache.removeEntry(make_uri(element.src));
544 element.parentNode.replaceChild(element.cloneNode(true), element);
545 } else if (b.current_uri.spec != b.display_uri_string) {
546 apply_load_spec(b, load_spec({ uri: b.display_uri_string,
547 forced_charset: forced_charset }));
549 var flags = bypass_cache == null ?
550 Ci.nsIWebNavigation.LOAD_FLAGS_NONE :
551 Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
553 if (! forced_charset && forced_charset_list)
554 forced_charset = predicate_alist_match(forced_charset_list,
557 if (forced_charset) {
559 var atomservice = Cc['@mozilla.org/atom-service;1']
560 .getService(Ci.nsIAtomService);
561 b.web_navigation.documentCharsetInfo.forcedCharset =
562 atomservice.getAtom(forced_charset);
565 b.web_navigation.reload(flags);
569 interactive("reload",
570 "Reload the current document.\n" +
571 "If a prefix argument is specified, the cache is bypassed. If a "+
572 "DOM node is supplied via browser object, that node will be "+
575 check_buffer(I.buffer, content_buffer);
576 var element = yield read_browser_object(I);
577 reload(I.buffer, I.P, element, I.forced_charset);
579 $browser_object = null);
582 * browserDOMWindow: intercept window opening
584 function initialize_browser_dom_window (window) {
585 window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
586 new browser_dom_window(window);
589 define_variable("browser_default_open_target", OPEN_NEW_BUFFER,
590 "Specifies how new window requests by content pages (e.g. by "+
591 "window.open from JavaScript or by using the target attribute of "+
592 "anchor and form elements) will be handled. This will generally "+
593 "be `OPEN_NEW_BUFFER', `OPEN_NEW_BUFFER_BACKGROUND', or "+
594 "`OPEN_NEW_WINDOW'.");
597 function browser_dom_window (window) {
598 this.window = window;
599 this.next_target = null;
601 browser_dom_window.prototype = {
602 constructor: browser_dom_window,
603 QueryInterface: generate_QI(Ci.nsIBrowserDOMWindow),
605 openURI: function (aURI, aOpener, aWhere, aContext) {
606 // Reference: http://www.xulplanet.com/references/xpcomref/ifaces/nsIBrowserDOMWindow.html
607 var target = this.next_target;
608 if (target == null || target == FOLLOW_DEFAULT)
609 target = browser_default_open_target;
610 this.next_target = null;
612 /* Determine the opener buffer */
613 var opener = get_buffer_from_frame(this.window, aOpener);
615 switch (browser_default_open_target) {
616 case OPEN_CURRENT_BUFFER:
618 case FOLLOW_CURRENT_FRAME:
620 case OPEN_NEW_BUFFER:
621 var buffer = new content_buffer(this.window, $opener = opener);
622 this.window.buffers.current = buffer;
623 return buffer.top_frame;
624 case OPEN_NEW_BUFFER_BACKGROUND:
625 var buffer = new content_buffer(this.window, $opener = opener);
626 return buffer.top_frame;
627 case OPEN_NEW_WINDOW:
628 default: /* shouldn't be needed */
630 /* We don't call make_window here, because that will result
631 * in the URL being loaded as the top-level document,
632 * instead of within a browser buffer. Instead, we can
633 * rely on Mozilla using browser.chromeURL. */
634 window_set_extra_arguments(
635 {initial_buffer_creator: buffer_creator(content_buffer, $opener = opener)}
642 add_hook("window_initialize_early_hook", initialize_browser_dom_window);
649 define_variable("page_modes", {},
650 "Object containing all activated page-modes. "+
651 "(See `page_mode_activate`.)");
653 define_keywords("$test");
654 function page_mode (name, enable, disable) {
656 buffer_mode.call(this, name, enable, disable,
657 forward_keywords(arguments));
658 this.test = arguments.$test;
660 page_mode.prototype = {
661 constructor: page_mode,
662 __proto__: buffer_mode.prototype,
664 enable: function (buffer) {
665 buffer_mode.prototype.enable.call(this, buffer);
666 buffer.page_modes.push(this.name);
667 buffer.set_input_mode();
669 disable: function (buffer) {
670 buffer_mode.prototype.disable.call(this, buffer);
671 var i = buffer.page_modes.indexOf(this.name);
673 buffer.page_modes.splice(i, 1);
674 buffer.set_input_mode();
678 function define_page_mode (name, test, enable, disable) {
679 keywords(arguments, $constructor = page_mode);
680 define_buffer_mode(name, enable, disable,
682 forward_keywords(arguments));
684 ignore_function_for_get_caller_source_code_reference("define_page_mode");
687 define_keywords("$modality");
688 function keymaps_page_mode (name, enable, disable) {
690 page_mode.call(this, name, enable, disable,
691 forward_keywords(arguments));
692 this.modality = arguments.$modality;
694 keymaps_page_mode.prototype = {
695 constructor: keymaps_page_mode,
696 __proto__: page_mode.prototype,
698 _enable: function (buffer) {
699 buffer.content_modalities.push(this.modality);
701 _disable: function (buffer) {
702 var i = buffer.content_modalities.indexOf(this.modality);
704 buffer.content_modalities.splice(i, 1);
707 function define_keymaps_page_mode (name, test, modality) {
708 keywords(arguments, $constructor = keymaps_page_mode);
709 define_buffer_mode(name, null, null,
711 $modality = modality,
712 forward_keywords(arguments));
714 ignore_function_for_get_caller_source_code_reference("define_keymaps_page_mode");
717 function page_mode_activate (page_mode) {
718 page_modes[page_mode.name] = page_mode;
721 function page_mode_deactivate (page_mode) {
722 delete page_modes[page_mode.name];
726 function page_mode_update (buffer) {
727 for (var i = buffer.page_modes.length - 1; i >= 0; --i) {
728 var p = buffer.page_modes[i];
729 conkeror[p].disable(buffer);
731 var uri = buffer.current_uri;
732 for (let [name, m] in Iterator(page_modes)) {
733 if (m.test instanceof RegExp) {
734 if (m.test.exec(uri.spec))
736 } else if (m.test(uri))
741 add_hook("content_buffer_location_change_hook", page_mode_update);
743 provide("content-buffer");