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
16 require("mime-type-override.js");
17 require("minibuffer-read-mime-type.js");
19 var browser_object_classes = {};
22 * browser_object_class
24 * In normal cases, make a new browser_object_class with the function,
25 * `define_browser_object_class'.
27 * name: See note on `define_browser_object_class'.
31 * handler: a coroutine called as: handler(I, prompt). `I' is a normal
32 * interactive context. `prompt' is there to pass along as the
33 * $prompt of various minibuffer read procedures, if needed.
35 * $hint: short string (usually verb and noun) to describe the UI
36 * of the browser object class to the user. Only used by
37 * browser object classes which make use of the minibuffer.
39 define_keywords("$hint");
40 function browser_object_class (name, doc, handler) {
43 this.handler = handler;
45 this.hint = arguments.$hint;
49 * define_browser_object_class
51 * In normal cases, make a new browser_object_class with the function,
52 * `define_browser_object_class'.
54 * name: the name of the browser object class. multiword names should be
55 * hyphenated. From this name, a variable browser_object_NAME and
56 * an interactive command browser-object-NAME will be generated.
58 * Other arguments are as for `browser_object_class'.
61 function define_browser_object_class (name, doc, handler) {
63 var varname = 'browser_object_'+name.replace('-','_','g');
64 var ob = conkeror[varname] =
65 new browser_object_class(name, doc, handler,
66 forward_keywords(arguments));
67 interactive("browser-object-"+name,
68 "A prefix command to specify that the following command operate "+
69 "on objects of type: "+name+".",
70 function (I) { I.browser_object = ob; },
76 * xpath_browser_object_handler
78 * This generates a function of the type needed for a handler of a
79 * browser object class. The handler uses `read_hinted_element' of
80 * hints.js to let the user pick a DOM node from those matched by
83 function xpath_browser_object_handler (xpath_expression) {
84 return function (I, prompt) {
85 var result = yield I.buffer.window.minibuffer.read_hinted_element(
88 $hint_xpath_expression = xpath_expression);
89 yield co_return(result);
93 define_browser_object_class("images",
94 "Browser object class for selecting an html:img via hinting.",
95 xpath_browser_object_handler("//img | //xhtml:img"),
96 $hint = "select image");
98 define_browser_object_class("frames",
99 "Browser object class for selecting a frame or iframe via hinting.",
100 function (I, prompt) {
101 var doc = I.buffer.document;
102 // Check for any frames or visible iframes
103 var skip_hints = true;
104 if (doc.getElementsByTagName("frame").length > 0)
107 let topwin = I.buffer.top_frame;
108 let iframes = doc.getElementsByTagName("iframe");
109 for (var i = 0, nframes = iframes.length; i < nframes; i++) {
110 let style = topwin.getComputedStyle(iframes[i], "");
111 if (style.display == "none" || style.visibility == "hidden")
118 // only one frame (the top-level one), no need to use the hints system
119 yield co_return(I.buffer.top_frame);
121 var result = yield I.buffer.window.minibuffer.read_hinted_element(
124 $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
125 yield co_return(result);
127 $hint = "select frame");
129 define_browser_object_class("links",
130 "Browser object class for selecting a hyperlink, form field, "+
131 "or link-like element, via hinting.",
132 xpath_browser_object_handler(
133 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or "+
134 "@oncommand or @role='link' or @role='button' or @role='menuitem'] | "+
135 "//input[not(@type='hidden')] | //a[@href] | //area | "+
136 "//iframe | //textarea | //button | //select | "+
137 "//*[@contenteditable = 'true'] | "+
138 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or "+
139 "@oncommand or @role='link' or @role='button' or @role='menuitem'] | "+
140 "//xhtml:input[not(@type='hidden')] | //xhtml:a[@href] | //xhtml:area | "+
141 "//xhtml:iframe | //xhtml:textarea | //xhtml:button | //xhtml:select | " +
142 "//xhtml:*[@contenteditable = 'true'] | "+
144 $hint = "select link");
146 define_browser_object_class("mathml",
147 "Browser object class for selecting a MathML node via hinting.",
148 xpath_browser_object_handler("//m:math"),
149 $hint = "select MathML element");
151 define_browser_object_class("top",
152 "Browser object class which returns the top frame of the document.",
153 function (I, prompt) { return I.buffer.top_frame; });
155 define_browser_object_class("url",
156 "Browser object class which prompts the user for an url or webjump.",
157 function (I, prompt) {
158 var result = yield I.buffer.window.minibuffer.read_url($prompt = prompt);
159 yield co_return(result);
161 $hint = "enter URL/webjump");
163 define_browser_object_class("paste-url",
164 "Browser object which reads an url from the X Primary Selection, "+
165 "falling back on the clipboard for operating systems which lack one.",
166 function (I, prompt) {
167 var url = read_from_x_primary_selection();
169 url = url.replace(/^\s*|\s*$/,"");
170 // add http:// if needed
171 if (url.match(/^[^:]+\./)) {
172 url = "http://" + url;
175 return make_uri(url).spec;
177 throw new interactive_error("error: malformed url: "+url);
181 define_browser_object_class("file",
182 "Browser object which prompts for a file name.",
183 function (I, prompt) {
184 var result = yield I.buffer.window.minibuffer.read_file(
186 $history = I.command.name+"/file",
187 $initial_value = I.local.cwd.path);
188 yield co_return(result);
190 $hint = "enter file name");
192 define_browser_object_class("alt",
193 "Browser object class which returns the alt text of an html:img, "+
194 "selected via hinting",
195 function (I, prompt) {
196 var result = yield I.buffer.window.minibuffer.read_hinted_element(
199 $hint_xpath_expression = "//img[@alt] | //xhtml:img[@alt]");
200 yield co_return(result.alt);
202 $hint = "select image for alt-text");
204 define_browser_object_class("title",
205 "Browser object class which returns the title attribute of an element, "+
206 "selected via hinting",
207 function (I, prompt) {
208 var result = yield I.buffer.window.minibuffer.read_hinted_element(
211 $hint_xpath_expression = "//*[@title] | //xhtml:*[@title]");
212 yield co_return(result.title);
214 $hint = "select element for title attribute");
216 define_browser_object_class("title-or-alt",
217 "Browser object which is the union of browser-object-alt and "+
218 "browser-object-title, with title having higher precedence in "+
219 "the case of an element that has both.",
220 function (I, prompt) {
221 var result = yield I.buffer.window.minibuffer.read_hinted_element(
224 $hint_xpath_expression = "//img[@alt] | //*[@title] | //xhtml:img[@alt] | //xhtml:*[@title]");
225 yield co_return(result.title ? result.title : result.alt);
227 $hint = "select element for title or alt-text");
229 define_browser_object_class("scrape-url",
230 "Browser object which lets the user choose an url from a list of "+
231 "urls scraped from the source code of the document.",
232 function (I, prompt) {
233 var completions = I.buffer.document.documentElement.innerHTML
234 .match(/https?:[^\s<>)"]*/g)
235 .filter(remove_duplicates_filter());
236 var completer = all_word_completer($completions = completions);
237 var result = yield I.buffer.window.minibuffer.read(
239 $completer = completer,
240 $initial_value = null,
241 $auto_complete = "url",
243 $match_required = false);
244 yield co_return(result);
246 $hint = "choose scraped URL");
248 define_browser_object_class("up-url",
249 "Browser object which returns the url one level above the current one.",
250 function (I, prompt) {
251 var up = compute_url_up_path(I.buffer.current_uri.spec);
252 return I.buffer.current_uri.resolve(up);
255 define_browser_object_class("focused-element",
256 "Browser object which returns the focused element.",
257 function (I, prompt) { return I.buffer.focused_element; });
259 define_browser_object_class("dom-node", null,
260 xpath_browser_object_handler("//* | //xhtml:*"),
261 $hint = "select DOM node");
263 define_browser_object_class("fragment-link",
264 "Browser object class which returns a link to the specified fragment of a page",
265 function (I, prompt) {
266 var elem = yield I.buffer.window.minibuffer.read_hinted_element(
269 $hint_xpath_expression = "//*[@id] | //a[@name] | //xhtml:*[@id] | //xhtml:a[@name]");
270 yield co_return(page_fragment_load_spec(elem));
272 $hint = "select element to link to");
274 interactive("browser-object-text",
275 "Composable browser object which returns the text of another object.",
277 // our job here is to modify the interactive context.
278 // set I.browser_object to a browser_object which calls the
279 // original one, then returns its text.
280 var b = I.browser_object;
281 I.browser_object = function (I) {
282 I.browser_object = b;
283 var e = yield read_browser_object(I);
284 if (e instanceof Ci.nsIDOMHTMLImageElement)
285 yield co_return(e.getAttribute("alt"));
286 yield co_return(e.textContent);
291 function get_browser_object (I) {
292 var obj = I.browser_object;
295 // if there was no interactive browser-object,
296 // binding_browser_object becomes the default.
297 if (obj === undefined) {
298 obj = I.binding_browser_object;
300 // if the command's default browser object is a non-null literal,
301 // it overrides an interactive browser-object, but not a binding
303 if (cmd.browser_object != null &&
304 (! (cmd.browser_object instanceof browser_object_class)) &&
305 (I.binding_browser_object === undefined))
307 obj = cmd.browser_object;
309 // if we still have no browser-object, look for a page-mode
310 // default, or finally the command default.
311 if (obj === undefined) {
313 I.buffer.default_browser_object_classes[cmd.name]) ||
320 function read_browser_object (I) {
321 var browser_object = get_browser_object(I);
322 if (browser_object === undefined)
323 throw interactive_error("No browser object");
326 // literals cannot be overridden
327 if (browser_object instanceof Function) {
328 result = yield browser_object(I);
329 yield co_return(result);
331 if (! (browser_object instanceof browser_object_class))
332 yield co_return(browser_object);
334 var prompt = I.command.prompt;
336 prompt = I.command.name.split(/-|_/).join(" ");
337 prompt = prompt[0].toUpperCase() + prompt.substring(1);
339 if (I.target != null)
340 prompt += TARGET_PROMPTS[I.target];
341 if (browser_object.hint)
342 prompt += " (" + browser_object.hint + ")";
345 result = yield browser_object.handler.call(null, I, prompt);
346 yield co_return(result);
351 * This is a simple wrapper function that sets focus to elem, and
352 * bypasses the automatic focus prevention system, which might
353 * otherwise prevent this from happening.
355 function browser_set_element_focus (buffer, elem, prevent_scroll) {
356 if (!element_dom_node_or_window_p(elem))
361 set_focus_no_scroll(buffer.window, elem);
366 function browser_element_focus (buffer, elem) {
367 if (!element_dom_node_or_window_p(elem))
370 if (elem instanceof Ci.nsIDOMXULTextBoxElement)
371 elem = elem.wrappedJSObject.inputField; // focus the input field
373 browser_set_element_focus(buffer, elem);
374 if (elem instanceof Ci.nsIDOMWindow)
377 // If it is not a window, it must be an HTML element
380 if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
381 elem instanceof Ci.nsIDOMHTMLIFrameElement)
383 elem.contentWindow.focus();
386 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
387 var coords = elem.getAttribute("coords").split(",");
388 x = Number(coords[0]);
389 y = Number(coords[1]);
392 var doc = elem.ownerDocument;
393 var evt = doc.createEvent("MouseEvents");
395 evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
396 elem.dispatchEvent(evt);
399 function browser_object_follow (buffer, target, elem) {
400 // XXX: would be better to let nsILocalFile objects be load_specs
401 if (elem instanceof Ci.nsILocalFile)
405 if (elem instanceof load_spec)
406 e = load_spec_element(elem);
410 browser_set_element_focus(buffer, e, true /* no scroll */);
412 var no_click = (((elem instanceof load_spec) &&
413 load_spec_forced_charset(elem)) ||
414 (e instanceof load_spec) ||
415 (e instanceof Ci.nsIDOMWindow) ||
416 (e instanceof Ci.nsIDOMHTMLFrameElement) ||
417 (e instanceof Ci.nsIDOMHTMLIFrameElement) ||
418 (e instanceof Ci.nsIDOMHTMLLinkElement) ||
419 (e instanceof Ci.nsIDOMHTMLImageElement &&
420 !e.hasAttribute("onmousedown") && !e.hasAttribute("onclick")));
422 if (target == FOLLOW_DEFAULT && !no_click) {
424 if (e instanceof Ci.nsIDOMHTMLAreaElement) {
425 var coords = e.getAttribute("coords").split(",");
426 if (coords.length >= 2) {
427 x = Number(coords[0]) + 1;
428 y = Number(coords[1]) + 1;
431 dom_node_click(e, x, y);
435 var spec = load_spec(elem);
437 if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
438 // it is nonsensical to follow a javascript url in a different
440 target = FOLLOW_DEFAULT;
441 } else if (!(buffer instanceof content_buffer) &&
442 (target == FOLLOW_CURRENT_FRAME ||
443 target == FOLLOW_DEFAULT ||
444 target == OPEN_CURRENT_BUFFER))
446 target = OPEN_NEW_BUFFER;
450 case FOLLOW_CURRENT_FRAME:
451 var current_frame = load_spec_source_frame(spec);
452 if (current_frame && current_frame != buffer.top_frame) {
453 var target_obj = get_web_navigation_for_frame(current_frame);
454 apply_load_spec(target_obj, spec);
458 case OPEN_CURRENT_BUFFER:
461 case OPEN_NEW_WINDOW:
462 case OPEN_NEW_BUFFER:
463 case OPEN_NEW_BUFFER_BACKGROUND:
464 create_buffer(buffer.window,
465 buffer_creator(content_buffer,
473 * Follow a link-like element by generating fake mouse events.
475 function dom_node_click (elem, x, y) {
476 var doc = elem.ownerDocument;
477 var view = doc.defaultView;
479 var evt = doc.createEvent("MouseEvents");
480 evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
481 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
482 elem.dispatchEvent(evt);
484 evt = doc.createEvent("MouseEvents");
485 evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
486 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
487 elem.dispatchEvent(evt);
489 evt = doc.createEvent("MouseEvents");
490 evt.initMouseEvent("mouseup", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
491 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
492 elem.dispatchEvent(evt);
496 function follow (I, target) {
498 target = FOLLOW_DEFAULT;
500 if (target == OPEN_CURRENT_BUFFER)
501 check_buffer(I.buffer, content_buffer);
502 var element = yield read_browser_object(I);
504 element = load_spec(element);
505 if (I.forced_charset)
506 element.forced_charset = I.forced_charset;
508 browser_object_follow(I.buffer, target, element);
511 function follow_new_buffer (I) {
512 yield follow(I, OPEN_NEW_BUFFER);
515 function follow_new_buffer_background (I) {
516 yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
519 function follow_new_window (I) {
520 yield follow(I, OPEN_NEW_WINDOW);
523 function follow_current_frame (I) {
524 yield follow(I, FOLLOW_CURRENT_FRAME);
527 function follow_current_buffer (I) {
528 yield follow(I, OPEN_CURRENT_BUFFER);
532 function element_get_load_target_label (element) {
533 if (element instanceof Ci.nsIDOMWindow)
535 if (element instanceof Ci.nsIDOMHTMLFrameElement)
537 if (element instanceof Ci.nsIDOMHTMLIFrameElement)
542 function element_get_operation_label (element, op_name, suffix) {
543 var target_label = element_get_load_target_label(element);
544 if (target_label != null)
545 target_label = " " + target_label;
550 suffix = " " + suffix;
554 return op_name + target_label + suffix + ":";
558 function browser_element_copy (buffer, elem) {
560 var spec = load_spec(elem);
563 if (typeof elem == "string" || elem instanceof String)
566 text = load_spec_uri_string(spec);
568 if (!(elem instanceof Ci.nsIDOMNode))
569 throw interactive_error("Element has no associated text to copy.");
570 var tag = elem.localName.toLowerCase();
571 if ((tag == "input" || tag == "button") &&
572 elem.type == "submit" && elem.form && elem.form.action)
574 text = elem.form.action;
575 } else if (tag == "input" || tag == "textarea") {
577 } else if (tag == "select") {
578 if (elem.selectedIndex >= 0)
579 text = elem.item(elem.selectedIndex).text;
581 text = elem.textContent;
584 browser_set_element_focus(buffer, elem);
585 writeToClipboard(text);
586 buffer.window.minibuffer.message("Copied: " + text);
590 define_variable("view_source_use_external_editor", false,
591 "When true, the `view-source' command will send its document to "+
592 "your external editor.");
594 define_variable("view_source_function", null,
595 "May be set to a user-defined function for viewing source code. "+
596 "The function should accept an nsILocalFile of the filename as "+
597 "its one positional argument, and it will also be called with "+
598 "the keyword `$temporary', whose value will be true if the file "+
599 "is considered temporary, and therefore the function must take "+
600 "responsibility for deleting it.");
602 function browser_object_view_source (buffer, target, elem) {
603 if (view_source_use_external_editor || view_source_function) {
604 var spec = load_spec(elem);
606 let [file, temp] = yield download_as_temporary(spec,
608 $action = "View source");
609 if (view_source_use_external_editor)
610 yield open_file_with_external_editor(file, $temporary = temp);
612 yield view_source_function(file, $temporary = temp);
617 var window = buffer.window;
618 if (elem.localName) {
619 switch (elem.localName.toLowerCase()) {
620 case "frame": case "iframe":
621 win = elem.contentWindow;
624 view_mathml_source(window, charset, elem);
627 throw new Error("Invalid browser element");
633 var url_s = win.location.href;
634 if (url_s.substring (0,12) != "view-source:") {
636 browser_object_follow(buffer, target, "view-source:" + url_s);
637 } catch(e) { dump_error(e); }
640 browser_object_follow(buffer, target, url_s.replace(/^view-source\:/, ''));
641 } catch(e) { dump_error(e); }
645 function view_source (I, target) {
648 target = OPEN_CURRENT_BUFFER;
649 var element = yield read_browser_object(I);
650 yield browser_object_view_source(I.buffer, target, element);
653 function view_source_new_buffer (I) {
654 yield view_source(I, OPEN_NEW_BUFFER);
657 function view_source_new_window (I) {
658 yield view_source(I, OPEN_NEW_WINDOW);
662 function browser_element_shell_command (buffer, elem, command, cwd) {
663 var spec = load_spec(elem);
664 yield download_as_temporary(spec,
666 $shell_command = command,
667 $shell_command_cwd = cwd);