Add initial OpenSearch search engine support
[conkeror.git] / modules / element.js
blobcb035f5129fe0d712efe4ba25ab7e99fd50a80d5
1 require("hints.js");
2 require("save.js");
4 var browser_object_classes = {};
6 /**
7  * handler is a coroutine called as: handler(buffer, prompt)
8  */
9 define_keywords("$doc", "$action", "$label", "$handler", "$xpath_expression");
10 function define_browser_object_class(name) {
11     keywords(arguments, $xpath_expression = undefined);
12     var handler = arguments.$handler;
13     let xpath_expression = arguments.$xpath_expression;
14     if (handler === undefined && xpath_expression != undefined) {
15         handler = function (buf, prompt) {
16             var result = yield buf.window.minibuffer.read_hinted_element(
17                 $buffer = buf,
18                 $prompt = prompt,
19                 $hint_xpath_expression = xpath_expression);
20             yield co_return(result);
21         };
22     }
23     var base_obj = browser_object_classes[name];
24     if (base_obj == null)
25         base_obj = browser_object_classes[name] = {};
26     var obj;
27     if (arguments.$action) {
28         name = name + "/" + arguments.$action;
29         obj = browser_object_classes[name];
30         if (obj == null)
31             obj = browser_object_classes[name] = {__proto__: base_obj};
32     } else
33         obj = base_obj;
34     if (arguments.$label !== undefined)
35         obj.label = arguments.$label;
36     if (arguments.$doc !== undefined)
37         obj.doc = arguments.$doc;
38     if (handler !== undefined)
39         obj.handler = handler;
42 define_browser_object_class("images", $xpath_expression = "//img | //xhtml:img");
44 define_browser_object_class("frames", $handler = function (buf, prompt) {
45     check_buffer(buf, content_buffer);
46     var doc = buf.document;
47     if (doc.getElementsByTagName("frame").length == 0 &&
48         doc.getElementsByTagName("iframe").length == 0)
49     {
50         // only one frame (the top-level one), no need to use the hints system
51         yield co_return(buf.top_frame);
52     }
54     var result = yield buf.window.minibuffer.read_hinted_element(
55         $buffer = buf,
56         $prompt = prompt,
57         $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
58     yield co_return(result);
59 });
61 define_browser_object_class(
62     "links",
63     $xpath_expression = 
64         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
65         "@class='lk' or @class='s' or @role='link'] | " +
66         "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
67         "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
68         "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
69         "//xhtml:button | //xhtml:select");
71 define_browser_object_class("mathml", $label = "MathML", $xpath_expression = "//m:math");
73 define_browser_object_class("top", $handler = function (buf, prompt) { yield co_return(buf.top_frame); });
76 define_variable(
77     "default_browser_object_classes",
78     {
79         follow: "links",
80         follow_top: "frames",
81         focus: "frames",
82         save: "links",
83         copy: "links",
84         view_source: "frames",
85         bookmark: "frames",
86         save_page: "frames",
87         save_page_complete: "top",
88         save_page_as_text: "frames",
89         default: "links"
90     },
91     "Specifies the default object class for each operation.\n" +
92         "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.");
94 interactive_context.prototype.browser_object_class = function (action_name) {
95     var cls =
96         this._browser_object_class ||
97         this.get("default_browser_object_classes")[action_name] ||
98         this.get("default_browser_object_classes")["default"];
99     return cls;
102 function browser_object_class_selector(name) {
103     return function (ctx, active_keymap, overlay_keymap) {
104         ctx._browser_object_class = name;
105         ctx.overlay_keymap = overlay_keymap || active_keymap;
106     }
109 function lookup_browser_object_class(class_name, action) {
110     var obj;
111     if (action != null) {
112         obj = browser_object_classes[class_name + "/" + action];
113         if (obj)
114             return obj;
115     }
116     return browser_object_classes[class_name];
119 interactive_context.prototype.read_browser_object = function(action, action_name, default_class, target)
121     var object_class_name = this.browser_object_class(action);
122     var object_class = lookup_browser_object_class(object_class_name, action);
124     var prompt = action_name;
125     if (target != null)
126         prompt += TARGET_PROMPTS[target];
127     if (object_class_name != default_class) {
128         var label = object_class.label || object_class_name;
129         prompt += " (" + label + ")";
130     }
131     prompt += ":";
133     var result = yield object_class.handler.call(null, this.buffer, prompt);
134     yield co_return(result);
138 function is_dom_node_or_window(elem) {
139     if (elem instanceof Ci.nsIDOMNode)
140         return true;
141     if (elem instanceof Ci.nsIDOMWindow)
142         return true;
143     return false;
147  * This is a simple wrapper function that sets focus to elem, and
148  * bypasses the automatic focus prevention system, which might
149  * otherwise prevent this from happening.
150  */
151 function browser_set_element_focus(buffer, elem, prevent_scroll) {
152     if (!is_dom_node_or_window(elem))
153         return;
155     buffer.last_user_input_received = Date.now();
156     if (prevent_scroll)
157         set_focus_no_scroll(buffer.window, elem);
158     else
159         elem.focus();
162 function browser_element_focus(buffer, elem)
164     if (!is_dom_node_or_window(elem))
165         return;
167     if (elem instanceof Ci.nsIDOMXULTextBoxElement)  {
168         // Focus the input field instead
169         elem = elem.wrappedJSObject.inputField;
170     }
172     browser_set_element_focus(buffer, elem);
173     if (elem instanceof Ci.nsIDOMWindow) {
174         return;
175     }
176     // If it is not a window, it must be an HTML element
177     var x = 0;
178     var y = 0;
179     if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
180         elem.contentWindow.focus();
181         return;
182     }
183     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
184         var coords = elem.getAttribute("coords").split(",");
185         x = Number(coords[0]);
186         y = Number(coords[1]);
187     }
189     var doc = elem.ownerDocument;
190     var evt = doc.createEvent("MouseEvents");
191     var doc = elem.ownerDocument;
193     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
194     elem.dispatchEvent(evt);
197 function browser_element_follow(buffer, target, elem)
199     browser_set_element_focus(buffer, elem, true /* no scroll */);
201     var no_click = (is_load_spec(elem) ||
202                     (elem instanceof Ci.nsIDOMWindow) ||
203                     (elem instanceof Ci.nsIDOMHTMLFrameElement) ||
204                     (elem instanceof Ci.nsIDOMHTMLIFrameElement) ||
205                     (elem instanceof Ci.nsIDOMHTMLLinkElement) ||
206                     (elem instanceof Ci.nsIDOMHTMLImageElement &&
207                      !elem.hasAttribute("onmousedown") && !elem.hasAttribute("onclick")));
209     if (target == FOLLOW_DEFAULT && !no_click) {
210         var x = 1, y = 1;
211         if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
212             var coords = elem.getAttribute("coords").split(",");
213             if (coords.length >= 2) {
214                 x = Number(coords[0]) + 1;
215                 y = Number(coords[1]) + 1;
216             }
217         }
218         browser_follow_link_with_click(buffer, elem, x, y);
219         return;
220     }
222     var spec = element_get_load_spec(elem);
223     if (spec == null) {
224         throw interactive_error("Element has no associated URL");
225         return;
226     }
228     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
229         // This URL won't work
230         throw interactive_error("Can't load javascript URL");
231     }
233     switch (target) {
234     case FOLLOW_CURRENT_FRAME:
235         var current_frame = load_spec_source_frame(spec);
236         if (current_frame && current_frame != buffer.top_frame) {
237             var target_obj = get_web_navigation_for_frame(current_frame);
238             apply_load_spec(target_obj, spec);
239             break;
240         }
241     case FOLLOW_DEFAULT:
242     case FOLLOW_TOP_FRAME:
243     case OPEN_CURRENT_BUFFER:
244         buffer.load(spec);
245         break;
246     case OPEN_NEW_WINDOW:
247     case OPEN_NEW_BUFFER:
248     case OPEN_NEW_BUFFER_BACKGROUND:
249         create_buffer(buffer.window,
250                       buffer_creator(content_buffer,
251                                      $load = spec,
252                                      $configuration = buffer.configuration),
253                       target);
254     }
258  * Follow a link-like element by generating fake mouse events.
259  */
260 function browser_follow_link_with_click(buffer, elem, x, y) {
261     var doc = elem.ownerDocument;
262     var view = doc.defaultView;
264     var evt = doc.createEvent("MouseEvents");
265     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
266                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
267     elem.dispatchEvent(evt);
269     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
270                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
271     elem.dispatchEvent(evt);
274 function element_get_load_spec(elem) {
276     if (is_load_spec(elem))
277         return elem;
279     var spec = null;
281     if (elem instanceof Ci.nsIDOMWindow)
282         spec = load_spec({document: elem.document});
284     else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
285              elem instanceof Ci.nsIDOMHTMLIFrameElement)
286         spec = load_spec({document: elem.contentDocument});
288     else {
289         var url = null;
290         var title = null;
292         if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
293             elem instanceof Ci.nsIDOMHTMLAreaElement ||
294             elem instanceof Ci.nsIDOMHTMLLinkElement) {
295             if (!elem.hasAttribute("href"))
296                 return null; // nothing can be done, as no nesting within these elements is allowed
297             url = elem.href;
298             title = elem.title || elem.textContent;
299         }
300         else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
301             url = elem.src;
302             title = elem.title || elem.alt;
303         }
304         else {
305             var node = elem;
306             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
307                 node = node.parentNode;
308             if (node && !node.hasAttribute("href"))
309                 node = null;
310             else
311                 url = node.href;
312             if (!node) {
313                 // Try simple XLink
314                 node = elem;
315                 while (node) {
316                     if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
317                         url = linkNode.getAttributeNS(XLINK_NS, "href");
318                         break;
319                     }
320                     node = node.parentNode;
321                 }
322                 if (url)
323                     url = makeURLAbsolute(node.baseURI, url);
324                 title = node.title || node.textContent;
325             }
326         }
327         if (url && url.length > 0) {
328             if (title && title.length == 0)
329                 title = null;
330             spec = load_spec({uri: url, source_frame: elem.ownerDocument.defaultView, title: title});
331         }
332     }
333     return spec;
336 interactive("follow", function (I) {
337     var target = I.browse_target("follow");
338     var element = yield I.read_browser_object("follow", "Follow", "links", target);
339     browser_element_follow(I.buffer, target, element);
342 interactive("follow-top", function (I) {
343     var target = I.browse_target("follow-top");
344     var element = yield I.read_browser_object("follow_top", "Follow", "links", target);
345     browser_element_follow(I.buffer, target, element);
348 interactive("focus", function (I) {
349     var element = yield I.read_browser_object("focus", "Focus");
350     browser_element_focus(I.buffer, element);
353 function element_get_load_target_label(element) {
354     if (element instanceof Ci.nsIDOMWindow)
355         return "page";
356     if (element instanceof Ci.nsIDOMHTMLFrameElement)
357         return "frame";
358     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
359         return "iframe";
360     return null;
363 function element_get_operation_label(element, op_name, suffix) {
364     var target_label = element_get_load_target_label(element);
365     if (target_label != null)
366         target_label = " " + target_label;
367     else
368         target_label = "";
370     if (suffix != null)
371         suffix = " " + suffix;
372     else
373         suffix = "";
375     return op_name + target_label + suffix + ":";
378 interactive("save", function (I) {
379     var element = yield I.read_browser_object("save", "Save", "links");
381     var spec = element_get_load_spec(element);
382     if (spec == null)
383         throw interactive_error("Element has no associated URI");
385     var panel;
386     panel = create_info_panel(I.window, "download-panel",
387                               [["downloading",
388                                 element_get_operation_label(element, "Saving"),
389                                 load_spec_uri_string(spec)],
390                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
392     try {
393         var file = yield I.minibuffer.read_file_check_overwrite(
394             $prompt = "Save as:",
395             $initial_value = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer),
396             $history = "save");
398     } finally {
399         panel.destroy();
400     }
402     save_uri(spec, file,
403              $buffer = I.buffer,
404              $use_cache = false);
407 function browser_element_copy(buffer, elem)
409     var spec = element_get_load_spec(elem);
410     var text = null;
411     if (spec)
412         text = load_spec_uri_string(spec);
413     else  {
414         if (!(elem instanceof Ci.nsIDOMNode))
415             throw interactive_error("Element has no associated text to copy.");
416         switch (elem.localName) {
417         case "INPUT":
418         case "TEXTAREA":
419             text = elem.value;
420             break;
421         case "SELECT":
422             if (elem.selectedIndex >= 0)
423                 text = elem.item(elem.selectedIndex).text;
424             break;
425         default:
426             text = elem.textContent;
427             break;
428         }
429     }
430     writeToClipboard (text);
431     buffer.window.minibuffer.message ("Copied: " + text);
435 interactive("copy", function (I) {
436     var element = yield I.read_browser_object("copy", "Copy", "links");
437     browser_element_copy(I.buffer, element);
440 var view_source_use_external_editor = false, view_source_function = null;
441 function browser_element_view_source(buffer, target, elem)
443     if (view_source_use_external_editor || view_source_function)
444     {
445         var spec = element_get_load_spec(elem);
446         if (spec == null) {
447             throw interactive_error("Element has no associated URL");
448             return;
449         }
451         let [file, temp] = yield download_as_temporary(spec,
452                                                        $buffer = buffer,
453                                                        $action = "View source");
454         if (view_source_use_external_editor)
455             yield open_file_with_external_editor(file, $temporary = temp);
456         else
457             yield view_source_function(file, $temporary = temp);
458         return;
459     }
461     var win = null;
462     var window = buffer.window;
463     if (elem.localName) {
464         switch (elem.localName.toLowerCase()) {
465         case "frame": case "iframe":
466             win = elem.contentWindow;
467             break;
468         case "math":
469             view_mathml_source (window, charset, elem);
470             return;
471         default:
472             throw new Error("Invalid browser element");
473         }
474     } else
475         win = elem;
476     win.focus();
478     var url_s = win.location.href;
479     if (url_s.substring (0,12) != "view-source:") {
480         try {
481             open_in_browser(buffer, target, "view-source:" + url_s);
482         } catch(e) { dump_error(e); }
483     } else {
484         window.minibuffer.message ("Already viewing source");
485     }
488 interactive("view-source", function (I) {
489     var target = I.browse_target("follow");
490     var element = yield I.read_browser_object("view_source", "View source", "frames", target);
491     yield browser_element_view_source(I.buffer, target, element);
494 interactive("shell-command-on-url", function (I) {
495     var cwd = I.cwd;
496     var element = yield I.read_browser_object("shell_command_url", "URL shell command target", "links");
497     var spec = element_get_load_spec(element);
498     if (spec == null)
499         throw interactive_error("Unable to obtain URI from element");
501     var uri = load_spec_uri_string(spec);
503     var panel;
504     panel = create_info_panel(I.window, "download-panel",
505                               [["downloading",
506                                 element_get_operation_label(element, "Running on", "URI"),
507                                 load_spec_uri_string(spec)],
508                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
510     try {
511         var cmd = yield I.minibuffer.read_shell_command(
512             $cwd = cwd,
513             $initial_value = load_spec_default_shell_command(spec));
514     } finally {
515         panel.destroy();
516     }
518     shell_command_with_argument_blind(cmd, uri, $cwd = cwd);
521 function browser_element_shell_command(buffer, elem, command) {
522     var spec = element_get_load_spec(elem);
523     if (spec == null) {
524         throw interactive_error("Element has no associated URL");
525         return;
526     }
527     yield download_as_temporary(spec,
528                                 $buffer = buffer,
529                                 $shell_command = command,
530                                 $shell_command_cwd = buffer.cwd);
533 interactive("shell-command-on-file", function (I) {
534     var cwd = I.cwd;
535     var element = yield I.read_browser_object("shell_command", "Shell command target", "links");
537     var spec = element_get_load_spec(element);
538     if (spec == null)
539         throw interactive_error("Unable to obtain URI from element");
541     var uri = load_spec_uri_string(spec);
543     var panel;
544     panel = create_info_panel(I.window, "download-panel",
545                               [["downloading",
546                                 element_get_operation_label(element, "Running on"),
547                                 load_spec_uri_string(spec)],
548                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
550     try {
552         var cmd = yield I.minibuffer.read_shell_command(
553             $cwd = cwd,
554             $initial_value = load_spec_default_shell_command(spec));
555     } finally {
556         panel.destroy();
557     }
559     /* FIXME: specify cwd as well */
560     yield browser_element_shell_command(I.buffer, element, cmd);
563 interactive("bookmark", function (I) {
564     var element = yield I.read_browser_object("bookmark", "Bookmark", "frames");
565     var spec = element_get_load_spec(element);
566     if (!spec)
567         throw interactive_error("Element has no associated URI");
568     var uri_string = load_spec_uri_string(spec);
569     var panel;
570     panel = create_info_panel(I.window, "bookmark-panel",
571                               [["bookmarking",
572                                 element_get_operation_label(element, "Bookmarking"),
573                                 uri_string]]);
574     try {
575         var title = yield I.minibuffer.read($prompt = "Bookmark with title:", $initial_value = load_spec_title(spec) || "");
576     } finally {
577         panel.destroy();
578     }
579     add_bookmark(uri_string, title);
580     I.minibuffer.message("Added bookmark: " + uri_string + " - " + title);
583 interactive("save-page", function (I) {
584     check_buffer(I.buffer, content_buffer);
585     var element = yield I.read_browser_object("save_page", "Save page", "frames");
586     var spec = element_get_load_spec(element);
587     if (!spec || !load_spec_document(spec))
588         throw interactive_error("Element is not associated with a document.");
589     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
591     var panel;
592     panel = create_info_panel(I.window, "download-panel",
593                               [["downloading",
594                                 element_get_operation_label(element, "Saving"),
595                                 load_spec_uri_string(spec)],
596                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
598     try {
599         var file = yield I.minibuffer.read_file_check_overwrite(
600             $prompt = "Save page as:",
601             $history = "save",
602             $initial_value = suggested_path);
603     } finally {
604         panel.destroy();
605     }
607     save_uri(spec, file, $buffer = I.buffer);
610 interactive("save-page-as-text", function (I) {
611     check_buffer(I.buffer, content_buffer);
612     var element = yield I.read_browser_object("save_page_as_text", "Save page as text", "frames");
613     var spec = element_get_load_spec(element);
614     var doc;
615     if (!spec || !(doc = load_spec_document(spec)))
616         throw interactive_error("Element is not associated with a document.");
617     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec, "txt"), I.buffer);
619     var panel;
620     panel = create_info_panel(I.window, "download-panel",
621                               [["downloading",
622                                 element_get_operation_label(element, "Saving", "as text"),
623                                 load_spec_uri_string(spec)],
624                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
626     try {
627         var file = yield I.minibuffer.read_file_check_overwrite(
628             $prompt = "Save page as text:",
629             $history = "save",
630             $initial_value = suggested_path);
631     } finally {
632         panel.destroy();
633     }
635     save_document_as_text(doc, file, $buffer = I.buffer);
638 interactive("save-page-complete", function (I) {
639     check_buffer(I.buffer, content_buffer);
640     var element = yield I.read_browser_object("save_page_complete", "Save page complete", "frames");
641     var spec = element_get_load_spec(element);
642     var doc;
643     if (!spec || !(doc = load_spec_document(spec)))
644         throw interactive_error("Element is not associated with a document.");
645     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
646     
647     var panel;
648     panel = create_info_panel(I.window, "download-panel",
649                               [["downloading",
650                                 element_get_operation_label(element, "Saving complete"),
651                                 load_spec_uri_string(spec)],
652                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
654     try {
655         var file = yield I.minibuffer.read_file_check_overwrite(
656             $prompt = "Save page complete:",
657             $history = "save",
658             $initial_value = suggested_path);
659         // FIXME: use proper read function
660         var dir = yield I.minibuffer.read_file(
661             $prompt = "Data Directory:",
662             $history = "save",
663             $initial_value = file.path + ".support");
664     } finally {
665         panel.destroy();
666     }
668     save_document_complete(doc, file, dir, $buffer = I.buffer);