Improve browser object class prompts
[conkeror.git] / modules / element.js
blob5db83b11924b305b8af47a62d0a0a3a13ef31bd6
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");
15 var browser_object_classes = {};
17 /**
18  * handler is a coroutine called as: handler(buffer, prompt)
19  */
20 define_keywords("$doc", "$action", "$label", "$handler", "$xpath_expression");
21 function define_browser_object_class(name) {
22     keywords(arguments, $xpath_expression = undefined);
23     var handler = arguments.$handler;
24     let xpath_expression = arguments.$xpath_expression;
25     if (handler === undefined && xpath_expression != undefined) {
26         handler = function (buf, prompt) {
27             var result = yield buf.window.minibuffer.read_hinted_element(
28                 $buffer = buf,
29                 $prompt = prompt,
30                 $hint_xpath_expression = xpath_expression);
31             yield co_return(result);
32         };
33     }
34     var base_obj = browser_object_classes[name];
35     if (base_obj == null)
36         base_obj = browser_object_classes[name] = {};
37     var obj;
38     if (arguments.$action) {
39         name = name + "/" + arguments.$action;
40         obj = browser_object_classes[name];
41         if (obj == null)
42             obj = browser_object_classes[name] = {__proto__: base_obj};
43     } else
44         obj = base_obj;
45     if (arguments.$label !== undefined)
46         obj.label = arguments.$label;
47     if (arguments.$doc !== undefined)
48         obj.doc = arguments.$doc;
49     if (handler !== undefined)
50         obj.handler = handler;
53 define_browser_object_class("images",
54                             $label = "image",
55                             $xpath_expression = "//img | //xhtml:img");
57 define_browser_object_class("frames", $label = "frame", $handler = function (buf, prompt) {
58     check_buffer(buf, content_buffer);
59     var doc = buf.document;
60     if (doc.getElementsByTagName("frame").length == 0 &&
61         doc.getElementsByTagName("iframe").length == 0)
62     {
63         // only one frame (the top-level one), no need to use the hints system
64         yield co_return(buf.top_frame);
65     }
67     var result = yield buf.window.minibuffer.read_hinted_element(
68         $buffer = buf,
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", $label = "link",
76     $xpath_expression =
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("mathml", $label = "MathML element", $xpath_expression = "//m:math");
86 define_browser_object_class("top", $handler = function (buf, prompt) { yield co_return(buf.top_frame); });
88 define_browser_object_class("url", $handler = function (buf, prompt) {
89                                 check_buffer (buf, content_buffer);
90                                 var result = yield buf.window.minibuffer.read_url ($prompt = prompt);
91                                 yield co_return (result);
92                             });
94 define_variable(
95     "default_browser_object_classes",
96     {
97         follow: "links",
98         follow_top: "frames",
99         focus: "frames",
100         save: "links",
101         copy: "links",
102         view_source: "frames",
103         bookmark: "frames",
104         save_page: "frames",
105         save_page_complete: "top",
106         save_page_as_text: "frames",
107         default: "links"
108     },
109     "Specifies the default object class for each operation.\n" +
110         "This variable should be an object literal with string-valued properties that specify one of the defined browser object classes.  If a property named after the operation is not present, the \"default\" property is consulted instead.");
112 interactive_context.prototype.browser_object_class = function (action_name) {
113     var cls =
114         this._browser_object_class ||
115         this.get("default_browser_object_classes")[action_name] ||
116         this.get("default_browser_object_classes")["default"];
117     return cls;
120 function browser_object_class_selector(name) {
121     return function (ctx, active_keymap, overlay_keymap, top_keymap) {
122         ctx._browser_object_class = name;
123         ctx.overlay_keymap = top_keymap;
124     }
127 function lookup_browser_object_class(class_name, action) {
128     var obj;
129     if (action != null) {
130         obj = browser_object_classes[class_name + "/" + action];
131         if (obj)
132             return obj;
133     }
134     return browser_object_classes[class_name];
137 interactive_context.prototype.read_browser_object = function(action, action_name, target)
139     var object_class_name = this.browser_object_class(action);
140     var object_class = lookup_browser_object_class(object_class_name, action);
142     var prompt = action_name;
143     var label = object_class.label || object_class_name;
144     if (target != null)
145         prompt += TARGET_PROMPTS[target];
146     prompt += " (select " + label + "):";
148     var result = yield object_class.handler.call(null, this.buffer, prompt);
149     yield co_return(result);
153 function is_dom_node_or_window(elem) {
154     if (elem instanceof Ci.nsIDOMNode)
155         return true;
156     if (elem instanceof Ci.nsIDOMWindow)
157         return true;
158     return false;
162  * This is a simple wrapper function that sets focus to elem, and
163  * bypasses the automatic focus prevention system, which might
164  * otherwise prevent this from happening.
165  */
166 function browser_set_element_focus(buffer, elem, prevent_scroll) {
167     if (!is_dom_node_or_window(elem))
168         return;
170     buffer.last_user_input_received = Date.now();
171     if (prevent_scroll)
172         set_focus_no_scroll(buffer.window, elem);
173     else
174         elem.focus();
177 function browser_element_focus(buffer, elem)
179     if (!is_dom_node_or_window(elem))
180         return;
182     if (elem instanceof Ci.nsIDOMXULTextBoxElement)  {
183         // Focus the input field instead
184         elem = elem.wrappedJSObject.inputField;
185     }
187     browser_set_element_focus(buffer, elem);
188     if (elem instanceof Ci.nsIDOMWindow) {
189         return;
190     }
191     // If it is not a window, it must be an HTML element
192     var x = 0;
193     var y = 0;
194     if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
195         elem.contentWindow.focus();
196         return;
197     }
198     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
199         var coords = elem.getAttribute("coords").split(",");
200         x = Number(coords[0]);
201         y = Number(coords[1]);
202     }
204     var doc = elem.ownerDocument;
205     var evt = doc.createEvent("MouseEvents");
206     var doc = elem.ownerDocument;
208     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
209     elem.dispatchEvent(evt);
212 function browser_element_follow(buffer, target, elem)
214     browser_set_element_focus(buffer, elem, true /* no scroll */);
216     var no_click = (is_load_spec(elem) ||
217                     (elem instanceof Ci.nsIDOMWindow) ||
218                     (elem instanceof Ci.nsIDOMHTMLFrameElement) ||
219                     (elem instanceof Ci.nsIDOMHTMLIFrameElement) ||
220                     (elem instanceof Ci.nsIDOMHTMLLinkElement) ||
221                     (elem instanceof Ci.nsIDOMHTMLImageElement &&
222                      !elem.hasAttribute("onmousedown") && !elem.hasAttribute("onclick")));
224     if (target == FOLLOW_DEFAULT && !no_click) {
225         var x = 1, y = 1;
226         if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
227             var coords = elem.getAttribute("coords").split(",");
228             if (coords.length >= 2) {
229                 x = Number(coords[0]) + 1;
230                 y = Number(coords[1]) + 1;
231             }
232         }
233         browser_follow_link_with_click(buffer, elem, x, y);
234         return;
235     }
237     var spec = element_get_load_spec(elem);
238     if (spec == null) {
239         throw interactive_error("Element has no associated URL");
240         return;
241     }
243     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
244         // This URL won't work
245         throw interactive_error("Can't load javascript URL");
246     }
248     switch (target) {
249     case FOLLOW_CURRENT_FRAME:
250         var current_frame = load_spec_source_frame(spec);
251         if (current_frame && current_frame != buffer.top_frame) {
252             var target_obj = get_web_navigation_for_frame(current_frame);
253             apply_load_spec(target_obj, spec);
254             break;
255         }
256     case FOLLOW_DEFAULT:
257     case FOLLOW_TOP_FRAME:
258     case OPEN_CURRENT_BUFFER:
259         buffer.load(spec);
260         break;
261     case OPEN_NEW_WINDOW:
262     case OPEN_NEW_BUFFER:
263     case OPEN_NEW_BUFFER_BACKGROUND:
264         create_buffer(buffer.window,
265                       buffer_creator(content_buffer,
266                                      $load = spec,
267                                      $configuration = buffer.configuration),
268                       target);
269     }
273  * Follow a link-like element by generating fake mouse events.
274  */
275 function browser_follow_link_with_click(buffer, elem, x, y) {
276     var doc = elem.ownerDocument;
277     var view = doc.defaultView;
279     var evt = doc.createEvent("MouseEvents");
280     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
281                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
282     elem.dispatchEvent(evt);
284     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
285                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
286     elem.dispatchEvent(evt);
289 function element_get_load_spec(elem) {
291     if (is_load_spec(elem))
292         return elem;
294     var spec = null;
296     if (elem instanceof Ci.nsIDOMWindow)
297         spec = load_spec({document: elem.document});
299     else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
300              elem instanceof Ci.nsIDOMHTMLIFrameElement)
301         spec = load_spec({document: elem.contentDocument});
303     else {
304         var url = null;
305         var title = null;
307         if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
308             elem instanceof Ci.nsIDOMHTMLAreaElement ||
309             elem instanceof Ci.nsIDOMHTMLLinkElement) {
310             if (!elem.hasAttribute("href"))
311                 return null; // nothing can be done, as no nesting within these elements is allowed
312             url = elem.href;
313             title = elem.title || elem.textContent;
314         }
315         else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
316             url = elem.src;
317             title = elem.title || elem.alt;
318         }
319         else {
320             var node = elem;
321             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
322                 node = node.parentNode;
323             if (node && !node.hasAttribute("href"))
324                 node = null;
325             else
326                 url = node.href;
327             if (!node) {
328                 // Try simple XLink
329                 node = elem;
330                 while (node) {
331                     if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
332                         url = linkNode.getAttributeNS(XLINK_NS, "href");
333                         break;
334                     }
335                     node = node.parentNode;
336                 }
337                 if (url)
338                     url = makeURLAbsolute(node.baseURI, url);
339                 title = node.title || node.textContent;
340             }
341         }
342         if (url && url.length > 0) {
343             if (title && title.length == 0)
344                 title = null;
345             spec = load_spec({uri: url, source_frame: elem.ownerDocument.defaultView, title: title});
346         }
347     }
348     return spec;
351 interactive("follow", function (I) {
352     var target = I.browse_target("follow");
353     var element = yield I.read_browser_object("follow", "Follow", target);
354     browser_element_follow(I.buffer, target, element);
357 interactive("follow-top", function (I) {
358     var target = I.browse_target("follow-top");
359     var element = yield I.read_browser_object("follow_top", "Follow", target);
360     browser_element_follow(I.buffer, target, element);
363 interactive("focus", function (I) {
364     var element = yield I.read_browser_object("focus", "Focus");
365     browser_element_focus(I.buffer, element);
368 function element_get_load_target_label(element) {
369     if (element instanceof Ci.nsIDOMWindow)
370         return "page";
371     if (element instanceof Ci.nsIDOMHTMLFrameElement)
372         return "frame";
373     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
374         return "iframe";
375     return null;
378 function element_get_operation_label(element, op_name, suffix) {
379     var target_label = element_get_load_target_label(element);
380     if (target_label != null)
381         target_label = " " + target_label;
382     else
383         target_label = "";
385     if (suffix != null)
386         suffix = " " + suffix;
387     else
388         suffix = "";
390     return op_name + target_label + suffix + ":";
393 interactive("save", function (I) {
394     var element = yield I.read_browser_object("save", "Save");
396     var spec = element_get_load_spec(element);
397     if (spec == null)
398         throw interactive_error("Element has no associated URI");
400     var panel;
401     panel = create_info_panel(I.window, "download-panel",
402                               [["downloading",
403                                 element_get_operation_label(element, "Saving"),
404                                 load_spec_uri_string(spec)],
405                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
407     try {
408         var file = yield I.minibuffer.read_file_check_overwrite(
409             $prompt = "Save as:",
410             $initial_value = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer),
411             $history = "save");
413     } finally {
414         panel.destroy();
415     }
417     save_uri(spec, file,
418              $buffer = I.buffer,
419              $use_cache = false);
422 function browser_element_copy(buffer, elem)
424     var spec = element_get_load_spec(elem);
425     var text = null;
426     if (spec)
427         text = load_spec_uri_string(spec);
428     else  {
429         if (!(elem instanceof Ci.nsIDOMNode))
430             throw interactive_error("Element has no associated text to copy.");
431         switch (elem.localName) {
432         case "INPUT":
433         case "TEXTAREA":
434             text = elem.value;
435             break;
436         case "SELECT":
437             if (elem.selectedIndex >= 0)
438                 text = elem.item(elem.selectedIndex).text;
439             break;
440         default:
441             text = elem.textContent;
442             break;
443         }
444     }
445     writeToClipboard (text);
446     buffer.window.minibuffer.message ("Copied: " + text);
450 interactive("copy", function (I) {
451     var element = yield I.read_browser_object("copy", "Copy");
452     browser_element_copy(I.buffer, element);
455 var view_source_use_external_editor = false, view_source_function = null;
456 function browser_element_view_source(buffer, target, elem)
458     if (view_source_use_external_editor || view_source_function)
459     {
460         var spec = element_get_load_spec(elem);
461         if (spec == null) {
462             throw interactive_error("Element has no associated URL");
463             return;
464         }
466         let [file, temp] = yield download_as_temporary(spec,
467                                                        $buffer = buffer,
468                                                        $action = "View source");
469         if (view_source_use_external_editor)
470             yield open_file_with_external_editor(file, $temporary = temp);
471         else
472             yield view_source_function(file, $temporary = temp);
473         return;
474     }
476     var win = null;
477     var window = buffer.window;
478     if (elem.localName) {
479         switch (elem.localName.toLowerCase()) {
480         case "frame": case "iframe":
481             win = elem.contentWindow;
482             break;
483         case "math":
484             view_mathml_source (window, charset, elem);
485             return;
486         default:
487             throw new Error("Invalid browser element");
488         }
489     } else
490         win = elem;
491     win.focus();
493     var url_s = win.location.href;
494     if (url_s.substring (0,12) != "view-source:") {
495         try {
496             open_in_browser(buffer, target, "view-source:" + url_s);
497         } catch(e) { dump_error(e); }
498     } else {
499         window.minibuffer.message ("Already viewing source");
500     }
503 interactive("view-source", function (I) {
504     var target = I.browse_target("follow");
505     var element = yield I.read_browser_object("view_source", "View source", target);
506     yield browser_element_view_source(I.buffer, target, element);
509 interactive("shell-command-on-url", function (I) {
510     var cwd = I.cwd;
511     var element = yield I.read_browser_object("shell_command_url", "URL shell command");
512     var spec = element_get_load_spec(element);
513     if (spec == null)
514         throw interactive_error("Unable to obtain URI from element");
516     var uri = load_spec_uri_string(spec);
518     var panel;
519     panel = create_info_panel(I.window, "download-panel",
520                               [["downloading",
521                                 element_get_operation_label(element, "Running on", "URI"),
522                                 load_spec_uri_string(spec)],
523                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
525     try {
526         var cmd = yield I.minibuffer.read_shell_command(
527             $cwd = cwd,
528             $initial_value = load_spec_default_shell_command(spec));
529     } finally {
530         panel.destroy();
531     }
533     shell_command_with_argument_blind(cmd, uri, $cwd = cwd);
536 function browser_element_shell_command(buffer, elem, command) {
537     var spec = element_get_load_spec(elem);
538     if (spec == null) {
539         throw interactive_error("Element has no associated URL");
540         return;
541     }
542     yield download_as_temporary(spec,
543                                 $buffer = buffer,
544                                 $shell_command = command,
545                                 $shell_command_cwd = buffer.cwd);
548 interactive("shell-command-on-file", function (I) {
549     var cwd = I.cwd;
550     var element = yield I.read_browser_object("shell_command", "Shell command");
552     var spec = element_get_load_spec(element);
553     if (spec == null)
554         throw interactive_error("Unable to obtain URI from element");
556     var uri = load_spec_uri_string(spec);
558     var panel;
559     panel = create_info_panel(I.window, "download-panel",
560                               [["downloading",
561                                 element_get_operation_label(element, "Running on"),
562                                 load_spec_uri_string(spec)],
563                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
565     try {
567         var cmd = yield I.minibuffer.read_shell_command(
568             $cwd = cwd,
569             $initial_value = load_spec_default_shell_command(spec));
570     } finally {
571         panel.destroy();
572     }
574     /* FIXME: specify cwd as well */
575     yield browser_element_shell_command(I.buffer, element, cmd);
578 interactive("bookmark", function (I) {
579     var element = yield I.read_browser_object("bookmark", "Bookmark");
580     var spec = element_get_load_spec(element);
581     if (!spec)
582         throw interactive_error("Element has no associated URI");
583     var uri_string = load_spec_uri_string(spec);
584     var panel;
585     panel = create_info_panel(I.window, "bookmark-panel",
586                               [["bookmarking",
587                                 element_get_operation_label(element, "Bookmarking"),
588                                 uri_string]]);
589     try {
590         var title = yield I.minibuffer.read($prompt = "Bookmark with title:", $initial_value = load_spec_title(spec) || "");
591     } finally {
592         panel.destroy();
593     }
594     add_bookmark(uri_string, title);
595     I.minibuffer.message("Added bookmark: " + uri_string + " - " + title);
598 interactive("save-page", function (I) {
599     check_buffer(I.buffer, content_buffer);
600     var element = yield I.read_browser_object("save_page", "Save page");
601     var spec = element_get_load_spec(element);
602     if (!spec || !load_spec_document(spec))
603         throw interactive_error("Element is not associated with a document.");
604     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
606     var panel;
607     panel = create_info_panel(I.window, "download-panel",
608                               [["downloading",
609                                 element_get_operation_label(element, "Saving"),
610                                 load_spec_uri_string(spec)],
611                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
613     try {
614         var file = yield I.minibuffer.read_file_check_overwrite(
615             $prompt = "Save page as:",
616             $history = "save",
617             $initial_value = suggested_path);
618     } finally {
619         panel.destroy();
620     }
622     save_uri(spec, file, $buffer = I.buffer);
625 interactive("save-page-as-text", function (I) {
626     check_buffer(I.buffer, content_buffer);
627     var element = yield I.read_browser_object("save_page_as_text", "Save page as text");
628     var spec = element_get_load_spec(element);
629     var doc;
630     if (!spec || !(doc = load_spec_document(spec)))
631         throw interactive_error("Element is not associated with a document.");
632     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec, "txt"), I.buffer);
634     var panel;
635     panel = create_info_panel(I.window, "download-panel",
636                               [["downloading",
637                                 element_get_operation_label(element, "Saving", "as text"),
638                                 load_spec_uri_string(spec)],
639                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
641     try {
642         var file = yield I.minibuffer.read_file_check_overwrite(
643             $prompt = "Save page as text:",
644             $history = "save",
645             $initial_value = suggested_path);
646     } finally {
647         panel.destroy();
648     }
650     save_document_as_text(doc, file, $buffer = I.buffer);
653 interactive("save-page-complete", function (I) {
654     check_buffer(I.buffer, content_buffer);
655     var element = yield I.read_browser_object("save_page_complete", "Save page complete");
656     var spec = element_get_load_spec(element);
657     var doc;
658     if (!spec || !(doc = load_spec_document(spec)))
659         throw interactive_error("Element is not associated with a document.");
660     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
662     var panel;
663     panel = create_info_panel(I.window, "download-panel",
664                               [["downloading",
665                                 element_get_operation_label(element, "Saving complete"),
666                                 load_spec_uri_string(spec)],
667                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
669     try {
670         var file = yield I.minibuffer.read_file_check_overwrite(
671             $prompt = "Save page complete:",
672             $history = "save",
673             $initial_value = suggested_path);
674         // FIXME: use proper read function
675         var dir = yield I.minibuffer.read_file(
676             $prompt = "Data Directory:",
677             $history = "save",
678             $initial_value = file.path + ".support");
679     } finally {
680         panel.destroy();
681     }
683     save_document_complete(doc, file, dir, $buffer = I.buffer);