2 * (C) Copyright 2007-2009 John J. Foerch
3 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5 * Portions of this file are derived from Vimperator,
6 * (C) Copyright 2006-2007 Martin Stubenschrott.
8 * Use, modification, and distribution are subject to the terms specified in the
14 require("mime-type-override.js");
15 require("minibuffer-read-mime-type.js");
17 var browser_object_classes = {};
20 * browser_object_class
22 * In normal cases, make a new browser_object_class with the function,
23 * `define_browser_object_class'.
25 * name: See note on `define_browser_object_class'.
29 * handler: a coroutine called as: handler(I, prompt). `I' is a normal
30 * interactive context. `prompt' is there to pass along as the
31 * $prompt of various minibuffer read procedures, if needed.
33 * $hint: short string (usually verb and noun) to describe the UI
34 * of the browser object class to the user. Only used by
35 * browser object classes which make use of the minibuffer.
37 define_keywords("$hint");
38 function browser_object_class (name, doc, handler) {
41 this.handler = handler;
43 this.hint = arguments.$hint;
47 * define_browser_object_class
49 * In normal cases, make a new browser_object_class with the function,
50 * `define_browser_object_class'.
52 * name: the name of the browser object class. multiword names should be
53 * hyphenated. From this name, a variable browser_object_NAME and
54 * an interactive command browser-object-NAME will be generated.
56 * Other arguments are as for `browser_object_class'.
59 function define_browser_object_class (name, doc, handler) {
61 var varname = 'browser_object_'+name.replace('-','_','g');
62 var ob = conkeror[varname] =
63 new browser_object_class(name, doc, handler,
64 forward_keywords(arguments));
65 interactive("browser-object-"+name,
66 "A prefix command to specify that the following command operate "+
67 "on objects of type: "+name+".",
68 function (I) { I.browser_object = ob; },
74 * xpath_browser_object_handler
76 * This generates a function of the type needed for a handler of a
77 * browser object class. The handler uses `read_hinted_element' of
78 * hints.js to let the user pick a DOM node from those matched by
81 function xpath_browser_object_handler (xpath_expression) {
82 return function (I, prompt) {
83 var result = yield I.buffer.window.minibuffer.read_hinted_element(
86 $hint_xpath_expression = xpath_expression);
87 yield co_return(result);
91 define_browser_object_class("images",
92 "Browser object class for selecting an html:img via hinting.",
93 xpath_browser_object_handler("//img | //xhtml:img"),
94 $hint = "select image");
96 define_browser_object_class("frames",
97 "Browser object class for selecting a frame or iframe via hinting.",
98 function (I, prompt) {
99 var doc = I.buffer.document;
100 // Check for any frames or visible iframes
101 var skip_hints = true;
102 if (doc.getElementsByTagName("frame").length > 0)
105 let topwin = I.buffer.top_frame;
106 let iframes = doc.getElementsByTagName("iframe");
107 for (var i = 0, nframes = iframes.length; i < nframes; i++) {
108 let style = topwin.getComputedStyle(iframes[i], "");
109 if (style.display == "none" || style.visibility == "hidden")
116 // only one frame (the top-level one), no need to use the hints system
117 yield co_return(I.buffer.top_frame);
119 var result = yield I.buffer.window.minibuffer.read_hinted_element(
122 $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
123 yield co_return(result);
125 $hint = "select frame");
127 define_browser_object_class("links",
128 "Browser object class for selecting a hyperlink, form field, "+
129 "or link-like element, via hinting.",
130 xpath_browser_object_handler(
131 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or "+
132 "@oncommand or @role='link' or @role='button' or @role='menuitem'] | "+
133 "//input[not(@type='hidden')] | //a[@href] | //area | "+
134 "//iframe | //textarea | //button | //select | "+
135 "//*[@contenteditable = 'true'] | "+
136 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or "+
137 "@oncommand or @role='link' or @role='button' or @role='menuitem'] | "+
138 "//xhtml:input[not(@type='hidden')] | //xhtml:a[@href] | //xhtml:area | "+
139 "//xhtml:iframe | //xhtml:textarea | //xhtml:button | //xhtml:select | " +
140 "//xhtml:*[@contenteditable = 'true'] | "+
142 $hint = "select link");
144 define_browser_object_class("mathml",
145 "Browser object class for selecting a MathML node via hinting.",
146 xpath_browser_object_handler("//m:math"),
147 $hint = "select MathML element");
149 define_browser_object_class("top",
150 "Browser object class which returns the top frame of the document.",
151 function (I, prompt) { return I.buffer.top_frame; });
153 define_browser_object_class("url",
154 "Browser object class which prompts the user for an url or webjump.",
155 function (I, prompt) {
156 var result = yield I.buffer.window.minibuffer.read_url($prompt = prompt);
157 yield co_return(result);
159 $hint = "enter URL/webjump");
161 define_browser_object_class("paste-url",
162 "Browser object which reads an url from the X Primary Selection, "+
163 "falling back on the clipboard for operating systems which lack one.",
164 function (I, prompt) {
165 var url = read_from_x_primary_selection();
167 url = url.replace(/^\s*|\s*$/,"");
168 // add http:// if needed
169 if (url.match(/^[^:]+\./)) {
170 url = "http://" + url;
173 return make_uri(url).spec;
175 throw new interactive_error("error: malformed url: "+url);
179 define_browser_object_class("file",
180 "Browser object which prompts for a file name.",
181 function (I, prompt) {
182 var result = yield I.buffer.window.minibuffer.read_file(
184 $history = I.command.name+"/file",
185 $initial_value = I.local.cwd.path);
186 yield co_return(result);
188 $hint = "enter file name");
190 define_browser_object_class("alt",
191 "Browser object class which returns the alt text of an html:img, "+
192 "selected via hinting",
193 function (I, prompt) {
194 var result = yield I.buffer.window.minibuffer.read_hinted_element(
197 $hint_xpath_expression = "//img[@alt] | //xhtml:img[@alt]");
198 yield co_return(result.alt);
200 $hint = "select image for alt-text");
202 define_browser_object_class("title",
203 "Browser object class which returns the title attribute of an element, "+
204 "selected via hinting",
205 function (I, prompt) {
206 var result = yield I.buffer.window.minibuffer.read_hinted_element(
209 $hint_xpath_expression = "//*[@title] | //xhtml:*[@title]");
210 yield co_return(result.title);
212 $hint = "select element for title attribute");
214 define_browser_object_class("title-or-alt",
215 "Browser object which is the union of browser-object-alt and "+
216 "browser-object-title, with title having higher precedence in "+
217 "the case of an element that has both.",
218 function (I, prompt) {
219 var result = yield I.buffer.window.minibuffer.read_hinted_element(
222 $hint_xpath_expression = "//img[@alt] | //*[@title] | //xhtml:img[@alt] | //xhtml:*[@title]");
223 yield co_return(result.title ? result.title : result.alt);
225 $hint = "select element for title or alt-text");
227 define_browser_object_class("scrape-url",
228 "Browser object which lets the user choose an url from a list of "+
229 "urls scraped from the source code of the document.",
230 function (I, prompt) {
231 var completions = I.buffer.document.documentElement.innerHTML
232 .match(/https?:[^\s<>)"]*/g)
233 .filter(remove_duplicates_filter());
234 var completer = all_word_completer($completions = completions);
235 var result = yield I.buffer.window.minibuffer.read(
237 $completer = completer,
238 $initial_value = null,
239 $auto_complete = "url",
241 $match_required = false);
242 yield co_return(result);
244 $hint = "choose scraped URL");
246 define_browser_object_class("up-url",
247 "Browser object which returns the url one level above the current one.",
248 function (I, prompt) {
249 return compute_up_url(I.buffer.current_uri);
252 define_browser_object_class("focused-element",
253 "Browser object which returns the focused element.",
254 function (I, prompt) { return I.buffer.focused_element; });
256 define_browser_object_class("dom-node", null,
257 xpath_browser_object_handler("//* | //xhtml:*"),
258 $hint = "select DOM node");
260 define_browser_object_class("fragment-link",
261 "Browser object class which returns a link to the specified fragment of a page",
262 function (I, prompt) {
263 var elem = yield I.buffer.window.minibuffer.read_hinted_element(
266 $hint_xpath_expression = "//*[@id] | //a[@name] | //xhtml:*[@id] | //xhtml:a[@name]");
267 yield co_return(page_fragment_load_spec(elem));
269 $hint = "select element to link to");
271 interactive("browser-object-text",
272 "Composable browser object which returns the text of another object.",
274 // our job here is to modify the interactive context.
275 // set I.browser_object to a browser_object which calls the
276 // original one, then returns its text.
277 var b = I.browser_object;
278 I.browser_object = function (I) {
279 I.browser_object = b;
280 var e = yield read_browser_object(I);
281 if (e instanceof Ci.nsIDOMHTMLImageElement)
282 yield co_return(e.getAttribute("alt"));
283 yield co_return(e.textContent);
288 function get_browser_object (I) {
289 var obj = I.browser_object;
292 // if there was no interactive browser-object,
293 // binding_browser_object becomes the default.
294 if (obj === undefined) {
295 obj = I.binding_browser_object;
297 // if the command's default browser object is a non-null literal,
298 // it overrides an interactive browser-object, but not a binding
300 if (cmd.browser_object != null &&
301 (! (cmd.browser_object instanceof browser_object_class)) &&
302 (I.binding_browser_object === undefined))
304 obj = cmd.browser_object;
306 // if we still have no browser-object, look for a page-mode
307 // default, or finally the command default.
308 if (obj === undefined) {
310 I.buffer.default_browser_object_classes[cmd.name]) ||
317 function read_browser_object (I) {
318 var browser_object = get_browser_object(I);
319 if (browser_object === undefined)
320 throw interactive_error("No browser object");
323 // literals cannot be overridden
324 if (browser_object instanceof Function) {
325 result = yield browser_object(I);
326 yield co_return(result);
328 if (! (browser_object instanceof browser_object_class))
329 yield co_return(browser_object);
331 var prompt = I.command.prompt;
333 prompt = I.command.name.split(/-|_/).join(" ");
334 prompt = prompt[0].toUpperCase() + prompt.substring(1);
336 if (I.target != null)
337 prompt += TARGET_PROMPTS[I.target];
338 if (browser_object.hint)
339 prompt += " (" + browser_object.hint + ")";
342 result = yield browser_object.handler.call(null, I, prompt);
343 yield co_return(result);
348 * This is a simple wrapper function that sets focus to elem, and
349 * bypasses the automatic focus prevention system, which might
350 * otherwise prevent this from happening.
352 function browser_set_element_focus (buffer, elem, prevent_scroll) {
353 if (! dom_node_or_window_p(elem))
358 set_focus_no_scroll(buffer.window, elem);
363 function browser_element_focus (buffer, elem) {
364 if (! dom_node_or_window_p(elem))
367 if (elem instanceof Ci.nsIDOMXULTextBoxElement)
368 elem = elem.wrappedJSObject.inputField; // focus the input field
370 browser_set_element_focus(buffer, elem);
371 if (elem instanceof Ci.nsIDOMWindow)
374 // If it is not a window, it must be an HTML element
377 if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
378 elem instanceof Ci.nsIDOMHTMLIFrameElement)
380 elem.contentWindow.focus();
383 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
384 var coords = elem.getAttribute("coords").split(",");
385 x = Number(coords[0]);
386 y = Number(coords[1]);
389 var doc = elem.ownerDocument;
390 var evt = doc.createEvent("MouseEvents");
392 evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
393 elem.dispatchEvent(evt);
396 function browser_object_follow (buffer, target, elem) {
397 // XXX: would be better to let nsILocalFile objects be load_specs
398 if (elem instanceof Ci.nsILocalFile)
402 if (elem instanceof load_spec)
403 e = load_spec_element(elem);
407 browser_set_element_focus(buffer, e, true /* no scroll */);
409 var no_click = (((elem instanceof load_spec) &&
410 load_spec_forced_charset(elem)) ||
411 (e instanceof load_spec) ||
412 (e instanceof Ci.nsIDOMWindow) ||
413 (e instanceof Ci.nsIDOMHTMLFrameElement) ||
414 (e instanceof Ci.nsIDOMHTMLIFrameElement) ||
415 (e instanceof Ci.nsIDOMHTMLLinkElement) ||
416 (e instanceof Ci.nsIDOMHTMLImageElement &&
417 !e.hasAttribute("onmousedown") && !e.hasAttribute("onclick")));
419 if (target == FOLLOW_DEFAULT && !no_click) {
421 if (e instanceof Ci.nsIDOMHTMLAreaElement) {
422 var coords = e.getAttribute("coords").split(",");
423 if (coords.length >= 2) {
424 x = Number(coords[0]) + 1;
425 y = Number(coords[1]) + 1;
428 dom_node_click(e, x, y);
432 var spec = load_spec(elem);
434 if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
435 // it is nonsensical to follow a javascript url in a different
437 target = FOLLOW_DEFAULT;
438 } else if (!(buffer instanceof content_buffer) &&
439 (target == FOLLOW_CURRENT_FRAME ||
440 target == FOLLOW_DEFAULT ||
441 target == OPEN_CURRENT_BUFFER))
443 target = OPEN_NEW_BUFFER;
447 case FOLLOW_CURRENT_FRAME:
448 var current_frame = load_spec_source_frame(spec);
449 if (current_frame && current_frame != buffer.top_frame) {
450 var target_obj = get_web_navigation_for_frame(current_frame);
451 apply_load_spec(target_obj, spec);
455 case OPEN_CURRENT_BUFFER:
458 case OPEN_NEW_WINDOW:
459 case OPEN_NEW_BUFFER:
460 case OPEN_NEW_BUFFER_BACKGROUND:
461 if (dom_node_or_window_p(e))
465 create_buffer(buffer.window,
466 buffer_creator(content_buffer,
474 * Follow a link-like element by generating fake mouse events.
476 function dom_node_click (elem, x, y) {
477 var doc = elem.ownerDocument;
478 var view = doc.defaultView;
480 var evt = doc.createEvent("MouseEvents");
481 evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
482 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
483 elem.dispatchEvent(evt);
485 evt = doc.createEvent("MouseEvents");
486 evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
487 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
488 elem.dispatchEvent(evt);
490 evt = doc.createEvent("MouseEvents");
491 evt.initMouseEvent("mouseup", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
492 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
493 elem.dispatchEvent(evt);
497 function follow (I, target) {
499 target = FOLLOW_DEFAULT;
501 if (target == OPEN_CURRENT_BUFFER)
502 check_buffer(I.buffer, content_buffer);
503 var element = yield read_browser_object(I);
505 element = load_spec(element);
506 if (I.forced_charset)
507 element.forced_charset = I.forced_charset;
509 browser_object_follow(I.buffer, target, element);
512 function follow_new_buffer (I) {
513 yield follow(I, OPEN_NEW_BUFFER);
516 function follow_new_buffer_background (I) {
517 yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
520 function follow_new_window (I) {
521 yield follow(I, OPEN_NEW_WINDOW);
524 function follow_current_frame (I) {
525 yield follow(I, FOLLOW_CURRENT_FRAME);
528 function follow_current_buffer (I) {
529 yield follow(I, OPEN_CURRENT_BUFFER);
533 function element_get_load_target_label (element) {
534 if (element instanceof Ci.nsIDOMWindow)
536 if (element instanceof Ci.nsIDOMHTMLFrameElement)
538 if (element instanceof Ci.nsIDOMHTMLIFrameElement)
543 function element_get_operation_label (element, op_name, suffix) {
544 var target_label = element_get_load_target_label(element);
545 if (target_label != null)
546 target_label = " " + target_label;
551 suffix = " " + suffix;
555 return op_name + target_label + suffix + ":";
559 function browser_element_copy (buffer, elem) {
561 var spec = load_spec(elem);
564 if (typeof elem == "string" || elem instanceof String)
567 text = load_spec_uri_string(spec);
569 if (!(elem instanceof Ci.nsIDOMNode))
570 throw interactive_error("Element has no associated text to copy.");
571 var tag = elem.localName.toLowerCase();
572 if ((tag == "input" || tag == "button") &&
573 elem.type == "submit" && elem.form && elem.form.action)
575 text = elem.form.action;
576 } else if (tag == "input" || tag == "textarea") {
578 } else if (tag == "select") {
579 if (elem.selectedIndex >= 0)
580 text = elem.item(elem.selectedIndex).text;
582 text = elem.textContent;
585 browser_set_element_focus(buffer, elem);
586 writeToClipboard(text);
587 buffer.window.minibuffer.message("Copied: " + text);
591 define_variable("view_source_use_external_editor", false,
592 "When true, the `view-source' command will send its document to "+
593 "your external editor.");
595 define_variable("view_source_function", null,
596 "May be set to a user-defined function for viewing source code. "+
597 "The function should accept an nsILocalFile of the filename as "+
598 "its one positional argument, and it will also be called with "+
599 "the keyword `$temporary', whose value will be true if the file "+
600 "is considered temporary, and therefore the function must take "+
601 "responsibility for deleting it.");
603 function browser_object_view_source (buffer, target, elem) {
604 if (view_source_use_external_editor || view_source_function) {
605 var spec = load_spec(elem);
607 let [file, temp] = yield download_as_temporary(spec,
609 $action = "View source");
610 if (view_source_use_external_editor)
611 yield open_file_with_external_editor(file, $temporary = temp);
613 yield view_source_function(file, $temporary = temp);
618 var window = buffer.window;
619 if (elem.localName) {
620 switch (elem.localName.toLowerCase()) {
621 case "frame": case "iframe":
622 win = elem.contentWindow;
625 view_mathml_source(window, charset, elem);
628 throw new Error("Invalid browser element");
634 var url_s = win.location.href;
635 if (url_s.substring (0,12) != "view-source:") {
637 browser_object_follow(buffer, target, "view-source:" + url_s);
638 } catch(e) { dump_error(e); }
641 browser_object_follow(buffer, target, url_s.replace(/^view-source\:/, ''));
642 } catch(e) { dump_error(e); }
646 function view_source (I, target) {
649 target = OPEN_CURRENT_BUFFER;
650 var element = yield read_browser_object(I);
651 yield browser_object_view_source(I.buffer, target, element);
654 function view_source_new_buffer (I) {
655 yield view_source(I, OPEN_NEW_BUFFER);
658 function view_source_new_window (I) {
659 yield view_source(I, OPEN_NEW_WINDOW);
663 function browser_element_shell_command (buffer, elem, command, cwd) {
664 var spec = load_spec(elem);
665 yield download_as_temporary(spec,
667 $shell_command = command,
668 $shell_command_cwd = cwd);