bind ^ to browser-object-up-url, and ^^ to go-up
[conkeror.git] / modules / element.js
blobadcab844d642756de5743a3345b6aea0ddefaef2
1 /**
2  * (C) Copyright 2007-2008 John J. Foerch
3  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
4  *
5  * Portions of this file are derived from Vimperator,
6  * (C) Copyright 2006-2007 Martin Stubenschrott.
7  *
8  * Use, modification, and distribution are subject to the terms specified in the
9  * COPYING file.
10 **/
12 require("hints.js");
13 require("save.js");
14 require("mime-type-override.js");
15 require("minibuffer-read-mime-type.js");
17 var browser_object_classes = {};
19 /**
20  * handler is a coroutine called as: handler(buffer, prompt)
21  */
22 function browser_object_class (name, label, doc, handler) {
23     this.name = name;
24     this.handler = handler;
25     if (doc) this.doc = doc;
26     if (label) this.label = label;
29 function define_browser_object_class (name, label, doc, handler) {
30     var varname = 'browser_object_'+name.replace('-','_','g');
31     var ob = conkeror[varname] =
32         new browser_object_class (name, label, doc, handler);
33     interactive(
34         "browser-object-"+name,
35         "A prefix command to specify that the following command operate "+
36             "on objects of type: "+name+".",
37         function (ctx) { ctx.browser_object = ob; },
38         $prefix = true);
39     return ob;
42 function xpath_browser_object_handler (xpath_expression) {
43     return function (I, prompt) {
44         var result = yield I.buffer.window.minibuffer.read_hinted_element(
45             $buffer = I.buffer,
46             $prompt = prompt,
47             $hint_xpath_expression = xpath_expression);
48         yield co_return(result);
49     };
52 define_browser_object_class(
53     "images", "image", null,
54     xpath_browser_object_handler ("//img | //xhtml:img"));
56 define_browser_object_class(
57     "frames","frame", null,
58     function (I, prompt) {
59         check_buffer(I.buffer, content_buffer);
60         var doc = I.buffer.document;
61         if (doc.getElementsByTagName("frame").length == 0 &&
62             doc.getElementsByTagName("iframe").length == 0)
63         {
64             // only one frame (the top-level one), no need to use the hints system
65             yield co_return(I.buffer.top_frame);
66         }
67         var result = yield I.buffer.window.minibuffer.read_hinted_element(
68             $buffer = I.buffer,
69             $prompt = prompt,
70             $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
71         yield co_return(result);
72     });
74 define_browser_object_class(
75     "links", "link", null,
76     xpath_browser_object_handler (
77         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
78         "@role='link'] | " +
79         "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
80         "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand] | " +
81         "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
82         "//xhtml:button | //xhtml:select"));
84 define_browser_object_class(
85     "mathml", "MathML element", null,
86     xpath_browser_object_handler ("//m:math"));
88 define_browser_object_class(
89     "top", null, null,
90     function (I, prompt) { yield co_return(I.buffer.top_frame); });
92 define_browser_object_class(
93     "url", null, null,
94     function (I, prompt) {
95         check_buffer (I.buffer, content_buffer);
96         var result = yield I.buffer.window.minibuffer.read_url ($prompt = prompt);
97         yield co_return (result);
98     });
100 define_browser_object_class(
101     "pasteurl", null, null,
102     function (I, url) {
103         check_buffer (I.buffer, content_buffer);
104         let url = read_from_x_primary_selection();
105         yield co_return (url);
106     });
108 define_browser_object_class(
109     "file", null, null,
110     function (I, prompt) {
111         var result = yield I.buffer.window.minibuffer.read_file(
112             $prompt = prompt,
113             $history = I.command.name+"/file",
114             $initial_value = I.buffer.cwd.path);
115         yield co_return (result);
116     });
118 define_browser_object_class(
119     "alt", "Image Alt-text", null,
120     function (I, prompt) {
121         var result = yield I.buffer.window.minibuffer.read_hinted_element(
122             $buffer = I.buffer,
123             $prompt = prompt,
124             $hint_xpath_expression = "//img[@alt]");
125         yield (co_return (result.alt));
126     });
128 define_browser_object_class(
129     "title", "Element Title", null,
130     function (I, prompt) {
131         var result = yield I.buffer.window.minibuffer.read_hinted_element(
132             $buffer = I.buffer,
133             $prompt = prompt,
134             $hint_xpath_expression = "//*[@title]");
135         yield (co_return (result.title));
136     });
138 define_browser_object_class(
139     "title-or-alt", "Element Title or Alt-text", null,
140     function (I, prompt) {
141         var result = yield I.buffer.window.minibuffer.read_hinted_element(
142             $buffer = I.buffer,
143             $prompt = prompt,
144             $hint_xpath_expression = "//img[@alt] | //*[@title]");
145         yield (co_return (result.title ? result.title : result.alt));
146     });
148 define_browser_object_class(
149     "scrape-url", "url",
150     "Scrapes urls from the source code of the top-level document of buffer.",
151     function (I, prompt) {
152         check_buffer (I.buffer, content_buffer);
153         var completions = I.buffer.document.documentElement.innerHTML
154             .match(/http:[^\s>"]*/g)
155             .filter(remove_duplicates_filter());
156         var completer = prefix_completer($completions = completions);
157         var result = yield I.buffer.window.minibuffer.read(
158             $prompt = prompt,
159             $completer = completer,
160             $initial_value = null,
161             $auto_complete = "url",
162             $select,
163             $match_required = false);
164         yield co_return (result);
165     });
167 define_browser_object_class(
168     "up-url", "Up Url", null,
169     function (I, prompt) {
170         check_buffer(I.buffer, content_buffer);
171         var up = compute_url_up_path(I.buffer.current_URI.spec);
172         return I.buffer.current_URI.resolve(up);
173     });
176 function read_browser_object (I)
178     var browser_object = I.browser_object;
179     // literals cannot be overridden
180     if (browser_object instanceof Function)
181         yield co_return(browser_object());
182     if (! (browser_object instanceof browser_object_class))
183         yield co_return(browser_object);
185     var prompt = I.command.prompt;
186     if (! prompt) {
187         prompt = I.command.name.split(/-|_/).join(" ");
188         prompt = prompt[0].toUpperCase() + prompt.substring(1);
189     }
190     if (I.target != null)
191         prompt += TARGET_PROMPTS[I.target];
192     if (browser_object.label)
193         prompt += " (select " + browser_object.label + ")";
194     prompt += ":";
196     var result = yield browser_object.handler.call(null, I, prompt);
197     yield co_return(result);
201 function is_dom_node_or_window(elem) {
202     if (elem instanceof Ci.nsIDOMNode)
203         return true;
204     if (elem instanceof Ci.nsIDOMWindow)
205         return true;
206     return false;
210  * This is a simple wrapper function that sets focus to elem, and
211  * bypasses the automatic focus prevention system, which might
212  * otherwise prevent this from happening.
213  */
214 function browser_set_element_focus(buffer, elem, prevent_scroll) {
215     if (!is_dom_node_or_window(elem))
216         return;
218     buffer.last_user_input_received = Date.now();
219     if (prevent_scroll)
220         set_focus_no_scroll(buffer.window, elem);
221     else
222         elem.focus();
225 function browser_element_focus(buffer, elem)
227     if (!is_dom_node_or_window(elem))
228         return;
230     if (elem instanceof Ci.nsIDOMXULTextBoxElement)  {
231         // Focus the input field instead
232         elem = elem.wrappedJSObject.inputField;
233     }
235     browser_set_element_focus(buffer, elem);
236     if (elem instanceof Ci.nsIDOMWindow) {
237         return;
238     }
239     // If it is not a window, it must be an HTML element
240     var x = 0;
241     var y = 0;
242     if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
243         elem.contentWindow.focus();
244         return;
245     }
246     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
247         var coords = elem.getAttribute("coords").split(",");
248         x = Number(coords[0]);
249         y = Number(coords[1]);
250     }
252     var doc = elem.ownerDocument;
253     var evt = doc.createEvent("MouseEvents");
255     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
256     elem.dispatchEvent(evt);
259 function browser_object_follow(buffer, target, elem)
261     browser_set_element_focus(buffer, elem, true /* no scroll */);
263     // XXX: would be better to let nsILocalFile objects be load_specs
264     if (elem instanceof Ci.nsILocalFile)
265         elem = elem.path;
267     var no_click = (is_load_spec(elem) ||
268                     (elem instanceof Ci.nsIDOMWindow) ||
269                     (elem instanceof Ci.nsIDOMHTMLFrameElement) ||
270                     (elem instanceof Ci.nsIDOMHTMLIFrameElement) ||
271                     (elem instanceof Ci.nsIDOMHTMLLinkElement) ||
272                     (elem instanceof Ci.nsIDOMHTMLImageElement &&
273                      !elem.hasAttribute("onmousedown") && !elem.hasAttribute("onclick")));
275     if (target == FOLLOW_DEFAULT && !no_click) {
276         var x = 1, y = 1;
277         if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
278             var coords = elem.getAttribute("coords").split(",");
279             if (coords.length >= 2) {
280                 x = Number(coords[0]) + 1;
281                 y = Number(coords[1]) + 1;
282             }
283         }
284         browser_follow_link_with_click(buffer, elem, x, y);
285         return;
286     }
288     var spec = element_get_load_spec(elem);
289     if (spec == null) {
290         throw interactive_error("Element has no associated URL");
291         return;
292     }
294     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
295         // it is nonsensical to follow a javascript url in a different
296         // buffer or window
297         target = FOLLOW_DEFAULT;
298     } else if (!(buffer instanceof content_buffer) &&
299         (target == FOLLOW_CURRENT_FRAME ||
300          target == FOLLOW_DEFAULT ||
301          target == FOLLOW_TOP_FRAME ||
302          target == OPEN_CURRENT_BUFFER))
303     {
304         target = OPEN_NEW_BUFFER;
305     }
307     switch (target) {
308     case FOLLOW_CURRENT_FRAME:
309         var current_frame = load_spec_source_frame(spec);
310         if (current_frame && current_frame != buffer.top_frame) {
311             var target_obj = get_web_navigation_for_frame(current_frame);
312             apply_load_spec(target_obj, spec);
313             break;
314         }
315     case FOLLOW_DEFAULT:
316     case FOLLOW_TOP_FRAME:
317     case OPEN_CURRENT_BUFFER:
318         buffer.load(spec);
319         break;
320     case OPEN_NEW_WINDOW:
321     case OPEN_NEW_BUFFER:
322     case OPEN_NEW_BUFFER_BACKGROUND:
323         create_buffer(buffer.window,
324                       buffer_creator(content_buffer,
325                                      $load = spec,
326                                      $configuration = buffer.configuration),
327                       target);
328     }
332  * Follow a link-like element by generating fake mouse events.
333  */
334 function browser_follow_link_with_click(buffer, elem, x, y) {
335     var doc = elem.ownerDocument;
336     var view = doc.defaultView;
338     var evt = doc.createEvent("MouseEvents");
339     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
340                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
341     elem.dispatchEvent(evt);
343     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
344                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
345     elem.dispatchEvent(evt);
348 function element_get_load_spec(elem) {
350     if (is_load_spec(elem))
351         return elem;
353     var spec = null;
355     if (elem instanceof Ci.nsIDOMWindow)
356         spec = load_spec({document: elem.document});
358     else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
359              elem instanceof Ci.nsIDOMHTMLIFrameElement)
360         spec = load_spec({document: elem.contentDocument});
362     else {
363         var url = null;
364         var title = null;
366         if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
367             elem instanceof Ci.nsIDOMHTMLAreaElement ||
368             elem instanceof Ci.nsIDOMHTMLLinkElement) {
369             if (!elem.hasAttribute("href"))
370                 return null; // nothing can be done, as no nesting within these elements is allowed
371             url = elem.href;
372             title = elem.title || elem.textContent;
373         }
374         else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
375             url = elem.src;
376             title = elem.title || elem.alt;
377         }
378         else {
379             var node = elem;
380             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
381                 node = node.parentNode;
382             if (node) {
383                 if (node.hasAttribute("href"))
384                     url = node.href;
385                 else
386                     node = null;
387             }
388             if (!node) {
389                 // Try simple XLink
390                 node = elem;
391                 while (node) {
392                     if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
393                         url = node.getAttributeNS(XLINK_NS, "href");
394                         break;
395                     }
396                     node = node.parentNode;
397                 }
398                 if (url)
399                     url = makeURLAbsolute(node.baseURI, url);
400                 title = node.title || node.textContent;
401             }
402         }
403         if (url && url.length > 0) {
404             if (title && title.length == 0)
405                 title = null;
406             spec = load_spec({uri: url, source_frame: elem.ownerDocument.defaultView, title: title});
407         }
408     }
409     return spec;
413 function follow (I, target) {
414     if (target == null)
415         target = FOLLOW_DEFAULT;
416     I.target = target;
417     var element = yield read_browser_object(I);
418     // XXX: to follow in the current buffer requires that the current
419     // buffer be a content_buffer.  this is perhaps not the best place
420     // for this check, because FOLLOW_DEFAULT could signify new buffer
421     // or new window.
422     check_buffer (I.buffer, content_buffer);
423     browser_object_follow(I.buffer, target, element);
426 function follow_new_buffer (I) {
427     yield follow(I, OPEN_NEW_BUFFER);
430 function follow_new_buffer_background (I) {
431     yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
434 function follow_new_window (I) {
435     yield follow(I, OPEN_NEW_WINDOW);
438 function follow_top (I) {
439     yield follow(I, FOLLOW_TOP_FRAME);
442 function follow_current_frame (I) {
443     yield follow(I, FOLLOW_CURRENT_FRAME);
446 function follow_current_buffer (I) {
447     yield follow(I, OPEN_CURRENT_BUFFER);
451 function element_get_load_target_label(element) {
452     if (element instanceof Ci.nsIDOMWindow)
453         return "page";
454     if (element instanceof Ci.nsIDOMHTMLFrameElement)
455         return "frame";
456     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
457         return "iframe";
458     return null;
461 function element_get_operation_label(element, op_name, suffix) {
462     var target_label = element_get_load_target_label(element);
463     if (target_label != null)
464         target_label = " " + target_label;
465     else
466         target_label = "";
468     if (suffix != null)
469         suffix = " " + suffix;
470     else
471         suffix = "";
473     return op_name + target_label + suffix + ":";
477 function browser_element_copy(buffer, elem)
479     var spec = element_get_load_spec(elem);
480     var text = null;
481     if (spec)
482         text = load_spec_uri_string(spec);
483     else  {
484         if (!(elem instanceof Ci.nsIDOMNode))
485             throw interactive_error("Element has no associated text to copy.");
486         switch (elem.localName) {
487         case "INPUT":
488         case "TEXTAREA":
489             text = elem.value;
490             break;
491         case "SELECT":
492             if (elem.selectedIndex >= 0)
493                 text = elem.item(elem.selectedIndex).text;
494             break;
495         default:
496             text = elem.textContent;
497             break;
498         }
499     }
500     browser_set_element_focus(buffer, elem);
501     writeToClipboard (text);
502     buffer.window.minibuffer.message ("Copied: " + text);
506 var view_source_use_external_editor = false, view_source_function = null;
507 function browser_object_view_source(buffer, target, elem)
509     if (view_source_use_external_editor || view_source_function)
510     {
511         var spec = element_get_load_spec(elem);
512         if (spec == null) {
513             throw interactive_error("Element has no associated URL");
514             return;
515         }
517         let [file, temp] = yield download_as_temporary(spec,
518                                                        $buffer = buffer,
519                                                        $action = "View source");
520         if (view_source_use_external_editor)
521             yield open_file_with_external_editor(file, $temporary = temp);
522         else
523             yield view_source_function(file, $temporary = temp);
524         return;
525     }
527     var win = null;
528     var window = buffer.window;
529     if (elem.localName) {
530         switch (elem.localName.toLowerCase()) {
531         case "frame": case "iframe":
532             win = elem.contentWindow;
533             break;
534         case "math":
535             view_mathml_source (window, charset, elem);
536             return;
537         default:
538             throw new Error("Invalid browser element");
539         }
540     } else
541         win = elem;
542     win.focus();
544     var url_s = win.location.href;
545     if (url_s.substring (0,12) != "view-source:") {
546         try {
547             browser_object_follow(buffer, target, "view-source:" + url_s);
548         } catch(e) { dump_error(e); }
549     } else {
550         window.minibuffer.message ("Already viewing source");
551     }
554 function view_source (I, target) {
555     I.target = target;
556     var element = yield read_browser_object(I);
557     yield browser_object_view_source(I.buffer, (target == null ? OPEN_CURRENT_BUFFER : target), element);
560 function view_source_new_buffer (I) {
561     yield view_source(I, OPEN_NEW_BUFFER);
564 function view_source_new_window (I) {
565     yield view_source(I, OPEN_NEW_WINDOW);
569 function browser_element_shell_command(buffer, elem, command) {
570     var spec = element_get_load_spec(elem);
571     if (spec == null) {
572         throw interactive_error("Element has no associated URL");
573         return;
574     }
575     yield download_as_temporary(spec,
576                                 $buffer = buffer,
577                                 $shell_command = command,
578                                 $shell_command_cwd = buffer.cwd);