Access hints_default_object_classes and hints_xpath_expressions as buffer-local variables
[conkeror.git] / modules / element.js
blob395609ab5be7e1592da56de19aaa553b6ae9817c
1 require("hints.js");
2 require("save.js");
4 function is_dom_node_or_window(elem) {
5     if (elem instanceof Ci.nsIDOMNode)
6         return true;
7     if (elem instanceof Ci.nsIDOMWindow)
8         return true;
9     return false;
12 /**
13  * This is a simple wrapper function that sets focus to elem, and
14  * bypasses the automatic focus prevention system, which might
15  * otherwise prevent this from happening.
16  */
17 function browser_set_element_focus(buffer, elem, prevent_scroll) {
18     if (!is_dom_node_or_window(elem))
19         return;
21     buffer.last_user_input_received = Date.now();
22     if (prevent_scroll)
23         set_focus_no_scroll(buffer.window, elem);
24     else
25         elem.focus();
28 function browser_element_focus(buffer, elem)
30     if (!is_dom_node_or_window(elem))
31         return;
33     if (elem instanceof Ci.nsIDOMXULTextBoxElement)  {
34         // Focus the input field instead
35         elem = elem.wrappedJSObject.inputField;
36     }
38     browser_set_element_focus(buffer, elem);
39     if (elem instanceof Ci.nsIDOMWindow) {
40         return;
41     }
42     // If it is not a window, it must be an HTML element
43     var x = 0;
44     var y = 0;
45     if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
46         elem.contentWindow.focus();
47         return;
48     }
49     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
50         var coords = elem.getAttribute("coords").split(",");
51         x = Number(coords[0]);
52         y = Number(coords[1]);
53     }
55     var doc = elem.ownerDocument;
56     var evt = doc.createEvent("MouseEvents");
57     var doc = elem.ownerDocument;
59     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
60     elem.dispatchEvent(evt);
63 function browser_element_follow(buffer, target, elem)
65     browser_set_element_focus(buffer, elem, true /* no scroll */);
67     var load_spec = null;
68     var current_frame = null;
69     var no_click = false;
70     if (elem instanceof media_spec) {
71         no_click = true;
72         current_frame = elem.frame;
73     } else if (elem instanceof Ci.nsIDOMWindow) {
74         current_frame = elem;
75         no_click = true;
76     } else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
77                elem instanceof Ci.nsIDOMHTMLIFrameElement ||
78                elem instanceof Ci.nsIDOMHTMLLinkElement) {
79         no_click = true;
80     } else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
81         if (!elem.hasAttribute("onmousedown") && !elem.hasAttribute("onclick"))
82             no_click = true;
83     }
85     if (target == FOLLOW_DEFAULT && !no_click) {
86         var x = 1, y = 1;
87         if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
88             var coords = elem.getAttribute("coords").split(",");
89             if (coords.length >= 2) {
90                 x = Number(coords[0]) + 1;
91                 y = Number(coords[1]) + 1;
92             }
93         }
94         browser_follow_link_with_click(buffer, elem, x, y);
95         return;
96     }
98     var load_spec = element_get_load_spec(elem);
99     if (load_spec == null) {
100         throw interactive_error("Element has no associated URL");
101         return;
102     }
104     if (load_spec.url.match(/^\s*javascript:/)) {
105         // This URL won't work
106         throw interactive_error("Can't load javascript URL");
107     }
109     var target_obj = null;
110     switch (target) {
111     case FOLLOW_CURRENT_FRAME:
112         if (current_frame == null) {
113             if (elem.ownerDocument)
114                 current_frame = elem.ownerDocument.defaultView;
115             else
116                 current_frame = buffer.top_frame;
117         }
118         if (current_frame != buffer.top_frame) {
119             target_obj = get_web_navigation_for_window(current_frame);
120             apply_load_spec(target_obj, load_spec);
121             break;
122         }
123     case FOLLOW_DEFAULT:
124     case FOLLOW_TOP_FRAME:
125     case OPEN_CURRENT_BUFFER:
126         buffer.load(load_spec);
127         break;
128     case OPEN_NEW_WINDOW:
129     case OPEN_NEW_BUFFER:
130     case OPEN_NEW_BUFFER_BACKGROUND:
131         create_buffer(buffer.window,
132                       buffer_creator(content_buffer,
133                                      $load = load_spec,
134                                      $configuration = buffer.configuration),
135                       target);
136     }
140  * Follow a link-like element by generating fake mouse events.
141  */
142 function browser_follow_link_with_click(buffer, elem, x, y) {
143     var doc = elem.ownerDocument;
144     var view = doc.defaultView;
146     var evt = doc.createEvent("MouseEvents");
147     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
148                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
149     elem.dispatchEvent(evt);
151     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
152                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
153     elem.dispatchEvent(evt);
156 function element_get_url(elem) {
157     var url = null;
159     if (elem instanceof media_spec) {
160         if (elem instanceof media_spec_simple_uri)
161             return elem.uri;
162         return null;
163     }
164     else if (elem instanceof Ci.nsIDOMWindow)
165         url = elem.location.href;
167     else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
168         elem instanceof Ci.nsIDOMHTMLIFrameElement)
169         url = elem.contentWindow.location.href;
171     else if (elem instanceof Ci.nsIDOMHTMLImageElement)
172         url = elem.src;
174     else if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
175              elem instanceof Ci.nsIDOMHTMLAreaElement ||
176              elem instanceof Ci.nsIDOMHTMLLinkElement) {
177         if (elem.hasAttribute("href"))
178             url = elem.href;
179         else
180             return null; // nothing can be done
181     } else {
182         var node = elem;
183         while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
184             node = node.parentNode;
185         if (node && !node.hasAttribute("href"))
186             node = null;
187         if (!node) {
188             // Try simple XLink
189             node = elem;
190             while (node) {
191                 if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
192                     url = linkNode.getAttributeNS(XLINK_NS, "href");
193                     break;
194                 }
195                 node = node.parentNode;
196             }
197             if (url)
198                 url = makeURLAbsolute(node.baseURI, url);
199         } else
200             url = node.href;
201     }
202     if (url && url.length == 0)
203         url = null;
204     return url;
207 function element_get_load_spec(elem) {
208     var load_spec = null;
210     if (elem instanceof media_spec) {
211         if (elem instanceof media_spec_simple_uri) {
212             load_spec = {url: elem.uri, referrer: make_uri(elem.frame.location.href),
213                          filename: elem.filename,
214                          mime_type: elem.mime_type };
215         }
216     }
217     else if (elem instanceof Ci.nsIDOMWindow)
218         load_spec = document_load_spec(elem.document);
220     else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
221              elem instanceof Ci.nsIDOMHTMLIFrameElement)
222         load_spec = document_load_spec(elem.contentDocument);
224     else {
225         var url = null;
227         if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
228             elem instanceof Ci.nsIDOMHTMLAreaElement ||
229             elem instanceof Ci.nsIDOMHTMLLinkElement) {
230             if (!elem.hasAttribute("href"))
231                 return null; // nothing can be done, as no nesting within these elements is allowed
232             url = elem.href;
233         }
234         else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
235             url = elem.src;
236         }
237         else {
238             var node = elem;
239             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
240                 node = node.parentNode;
241             if (node && !node.hasAttribute("href"))
242                 node = null;
243             else
244                 url = node.href;
245             if (!node) {
246                 // Try simple XLink
247                 node = elem;
248                 while (node) {
249                     if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
250                         url = linkNode.getAttributeNS(XLINK_NS, "href");
251                         break;
252                     }
253                     node = node.parentNode;
254                 }
255                 if (url)
256                     url = makeURLAbsolute(node.baseURI, url);
257             }
258         }
259         if (url && url.length > 0) {
260             var referrer = null;
261             try {
262                 referrer = makeURL(elem.ownerDocument.location.href);
263             } catch (e) {}
264             load_spec = {url: url, referrer: referrer};
265         }
266     }
267     return load_spec;
270 define_variable(
271     "hints_default_object_classes",
272     {
273         follow: "links",
274         follow_top: "frames",
275         focus: "frames",
276         save: "links",
277         copy: "links",
278         view_source: "frames",
279         bookmark: "frames",
280         save_page: "frames",
281         save_page_complete: "top",
282         save_page_as_text: "frames",
283         def: "links"
284     },
285     "Specifies the default object class for each operation.\n" +
286         "This variable should be an object literal with string-valued properties that specify one of the supported object classes (\"links\", \"frames\", \"top\", or \"images\") defined in `hints_xpath_expression'.  If a property named after the operation is not present, the \"def\" property is consulted instead.");
288 interactive_context.prototype.hints_object_class = function (action_name) {
289     var cls =
290         this._hints_object_class ||
291         this.get("hints_default_object_classes")[action_name] ||
292         this.get("hints_default_object_classes")["def"];
293     return cls;
296 function resolve_hints_xpath_expression(buffer, object_class, action_name) {
297     var db = buffer.get("hints_xpath_expressions")[object_class];
298     return db[action_name] || db["def"];
301 interactive_context.prototype.hints_xpath_expression = function (action_name) {
302     return resolve_hints_xpath_expression(this.buffer, this.hints_object_class(action_name), action_name);
305 function hints_object_class_selector(name) {
306     return function (ctx, active_keymap, overlay_keymap) {
307         ctx._hints_object_class = name;
308         ctx.overlay_keymap = overlay_keymap || active_keymap;
309     }
312 interactive_context.prototype.read_hinted_element_with_prompt = function(action, action_name, default_class, target)
314     var object_class = this.hints_object_class(action);
316     var prompt = action_name;
317     if (target != null)
318         prompt += TARGET_PROMPTS[target];
319     if (object_class != default_class)
320         prompt += " (" + object_class + ")";
321     prompt += ":";
322     
323     var result = yield this.minibuffer.read_hinted_element(
324         $buffer = this.buffer,
325         $prompt = prompt,
326         $object_class = object_class,
327         $action = action);
329     yield co_return(result);
332 interactive("follow", function (I) {
333     var target = I.browse_target("follow");
334     var element = yield I.read_hinted_element_with_prompt("follow", "Follow", "links", target);
335     browser_element_follow(I.buffer, target, element);
338 interactive("follow-top", function (I) {
339     var target = I.browse_target("follow-top");
340     var element = yield I.read_hinted_element_with_prompt("follow_top", "Follow", "links", target);
341     browser_element_follow(I.buffer, target, element);
344 interactive("focus", function (I) {
345     var element = yield I.read_hinted_element_with_prompt("focus", "Focus");
346     browser_element_focus(I.buffer, element);
349 function element_get_load_target_label(element) {
350     if (element instanceof Ci.nsIDOMWindow)
351         return "page";
352     if (element instanceof Ci.nsIDOMHTMLFrameElement)
353         return "frame";
354     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
355         return "iframe";
356     if (element instanceof media_spec)
357         return  "media";
358     return null;
361 function element_get_operation_label(element, op_name, suffix) {
362     var target_label = element_get_load_target_label(element);
363     if (target_label != null)
364         target_label = " " + target_label;
365     else
366         target_label = "";
368     if (suffix != null)
369         suffix = " " + suffix;
370     else
371         suffix = "";
373     return op_name + target_label + suffix + ":";
376 interactive("save", function (I) {
377     var element = yield I.read_hinted_element_with_prompt("save", "Save", "links");
379     var load_spec = element_get_load_spec(element);
380     if (load_spec == null)
381         throw interactive_error("Element has no associated URI");
383     var panel;
384     panel = create_info_panel(I.window, "download-panel",
385                               [["downloading",
386                                 element_get_operation_label(element, "Saving"),
387                                 uri_string_from_load_spec(load_spec)],
388                                ["mime-type", "Mime type:", mime_type_from_load_spec(load_spec)]]);
390     try {
391         var file = yield I.minibuffer.read_file_check_overwrite(
392             $prompt = "Save as:",
393             $initial_value = suggest_save_path_from_file_name(suggest_file_name(load_spec), I.buffer),
394             $history = "save");
396     } finally {
397         panel.destroy();
398     }
400     save_uri(load_spec, file,
401              $buffer = I.buffer,
402              $use_cache = false);
405 function browser_element_copy(buffer, elem)
407     var text = element_get_url(elem);
408     if (text == null) {
409         if (!(elem instanceof Ci.nsIDOMNode))
410             throw interactive_error("Element has no associated text to copy.");
411         switch (elem.localName) {
412         case "INPUT":
413         case "TEXTAREA":
414             text = elem.value;
415             break;
416         case "SELECT":
417             if (elem.selectedIndex >= 0)
418                 text = elem.item(elem.selectedIndex).text;
419             break;
420         }
421     }
422     if (text == null)
423         text = elem.textContent;
424     writeToClipboard (text);
425     buffer.window.minibuffer.message ("Copied: " + text);
429 interactive("copy", function (I) {
430     var element = yield I.read_hinted_element_with_prompt("copy", "Copy", "links");
431     browser_element_copy(I.buffer, element);
434 var view_source_use_external_editor = false, view_source_function = null;
435 function browser_element_view_source(buffer, target, elem)
437     if (view_source_use_external_editor || view_source_function)
438     {
439         var load_spec = element_get_load_spec(elem);
440         if (load_spec == null) {
441             throw interactive_error("Element has no associated URL");
442             return;
443         }
445         let [file, temp] = yield download_as_temporary(load_spec,
446                                                        $buffer = buffer,
447                                                        $action = "View source");
448         if (view_source_use_external_editor)
449             yield open_file_with_external_editor(file, $temporary = temp);
450         else
451             yield view_source_function(file, $temporary = temp);
452         return;
453     }
455     var win = null;
456     var window = buffer.window;
457     if (elem.localName) {
458         switch (elem.localName.toLowerCase()) {
459         case "frame": case "iframe":
460             win = elem.contentWindow;
461             break;
462         case "math":
463             view_mathml_source (window, charset, elem);
464             return;
465         default:
466             throw new Error("Invalid browser element");
467         }
468     } else
469         win = elem;
470     win.focus();
472     var url_s = win.location.href;
473     if (url_s.substring (0,12) != "view-source:") {
474         try {
475             open_in_browser(buffer, target, "view-source:" + url_s);
476         } catch(e) { dump_error(e); }
477     } else {
478         window.minibuffer.message ("Already viewing source");
479     }
482 interactive("view-source", function (I) {
483     var target = I.browse_target("follow");
484     var element = yield I.read_hinted_element_with_prompt("view_source", "View source", "frames", target);
485     yield browser_element_view_source(I.buffer, target, element);
488 interactive("shell-command-on-url", function (I) {
489     var cwd = I.cwd;
490     var element = yield I.read_hinted_element_with_prompt("shell_command_url", "URL shell command target", "links");
491     var load_spec = element_get_load_spec(element);
492     if (load_spec == null)
493         throw interactive_error("Unable to obtain URI from element");
495     var uri = uri_string_from_load_spec(load_spec);
497     var panel;
498     panel = create_info_panel(I.window, "download-panel",
499                               [["downloading",
500                                 element_get_operation_label(element, "Running on", "URI"),
501                                 uri_string_from_load_spec(load_spec)],
502                                ["mime-type", "Mime type:", mime_type_from_load_spec(load_spec)]]);
504     try {
505         var cmd = yield I.minibuffer.read_shell_command(
506             $cwd = cwd,
507             $initial_value = default_shell_command_from_load_spec(load_spec));
508     } finally {
509         panel.destroy();
510     }
512     shell_command_with_argument_blind(cmd, uri, $cwd = cwd);
515 function browser_element_shell_command(buffer, elem, command) {
516     var load_spec = element_get_load_spec(elem);
517     if (load_spec == null) {
518         throw interactive_error("Element has no associated URL");
519         return;
520     }
521     yield download_as_temporary(load_spec,
522                                 $buffer = buffer,
523                                 $shell_command = command,
524                                 $shell_command_cwd = buffer.cwd);
527 interactive("shell-command-on-file", function (I) {
528     var cwd = I.cwd;
529     var element = yield I.read_hinted_element_with_prompt("shell_command", "Shell command target", "links");
531     var load_spec = element_get_load_spec(element);
532     if (load_spec == null)
533         throw interactive_error("Unable to obtain URI from element");
535     var uri = uri_string_from_load_spec(load_spec);
537     var panel;
538     panel = create_info_panel(I.window, "download-panel",
539                               [["downloading",
540                                 element_get_operation_label(element, "Running on"),
541                                 uri_string_from_load_spec(load_spec)],
542                                ["mime-type", "Mime type:", mime_type_from_load_spec(load_spec)]]);
544     try {
546         var cmd = yield I.minibuffer.read_shell_command(
547             $cwd = cwd,
548             $initial_value = default_shell_command_from_load_spec(load_spec));
549     } finally {
550         panel.destroy();
551     }
553     /* FIXME: specify cwd as well */
554     yield browser_element_shell_command(I.buffer, element, cmd);
557 interactive("bookmark", function (I) {
558     var element = yield I.read_hinted_element_with_prompt("bookmark", "Bookmark", "frames");
559     let [uri, suggested_title] = get_element_bookmark_info(element);
561     var panel;
562     panel = create_info_panel(I.window, "bookmark-panel",
563                               [["bookmarking",
564                                 element_get_operation_label(element, "Bookmarking"),
565                                 uri]]);
566     try {
567         var title = yield I.minibuffer.read($prompt = "Bookmark with title:", $initial_value = suggested_title);
568     } finally {
569         panel.destroy();
570     }
571     add_bookmark(uri, title);
572     I.minibuffer.message("Added bookmark: " + uri + " - " + title);
575 interactive("save-page", function (I) {
576     check_buffer(I.buffer, content_buffer);
577     var element = yield I.read_hinted_element_with_prompt("save_page", "Save page", "frames");
578     var load_spec = element_get_load_spec(element);
579     if (load_spec == null || typeof(load_spec) != "object" || load_spec.document == null)
580         throw interactive_error("Element is not associated with a document.");
581     var doc = load_spec.document;
582     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(doc), I.buffer);
584     var panel;
585     panel = create_info_panel(I.window, "download-panel",
586                               [["downloading",
587                                 element_get_operation_label(element, "Saving"),
588                                 uri_string_from_load_spec(load_spec)],
589                                ["mime-type", "Mime type:", mime_type_from_load_spec(load_spec)]]);
591     try {
592         var file = yield I.minibuffer.read_file_check_overwrite(
593             $prompt = "Save page as:",
594             $history = "save",
595             $initial_value = suggested_path);
596     } finally {
597         panel.destroy();
598     }
600     save_uri(document_load_spec(doc), file, $buffer = I.buffer);
603 interactive("save-page-as-text", function (I) {
604     check_buffer(I.buffer, content_buffer);
605     var element = yield I.read_hinted_element_with_prompt("save_page_as_text", "Save page as text", "frames");
606     var load_spec = element_get_load_spec(element);
607     if (load_spec == null || typeof(load_spec) != "object" || load_spec.document == null)
608         throw interactive_error("Element is not associated with a document.");
609     var doc = load_spec.document;
610     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(doc, "txt"), I.buffer);
612     var panel;
613     panel = create_info_panel(I.window, "download-panel",
614                               [["downloading",
615                                 element_get_operation_label(element, "Saving", "as text"),
616                                 uri_string_from_load_spec(load_spec)],
617                                ["mime-type", "Mime type:", mime_type_from_load_spec(load_spec)]]);
619     try {
620         var file = yield I.minibuffer.read_file_check_overwrite(
621             $prompt = "Save page as text:",
622             $history = "save",
623             $initial_value = suggested_path);
624     } finally {
625         panel.destroy();
626     }
628     save_document_as_text(doc, file, $buffer = I.buffer);
631 interactive("save-page-complete", function (I) {
632     check_buffer(I.buffer, content_buffer);
633     var element = yield I.read_hinted_element_with_prompt("save_page_complete", "Save page complete", "frames");
634     var load_spec = element_get_load_spec(element);
635     if (load_spec == null || typeof(load_spec) != "object" || load_spec.document == null)
636         throw interactive_error("Element is not associated with a document.");
637     var doc = load_spec.document;
638     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(doc), I.buffer);
640     var panel;
641     panel = create_info_panel(I.window, "download-panel",
642                               [["downloading",
643                                 element_get_operation_label(element, "Saving complete"),
644                                 uri_string_from_load_spec(load_spec)],
645                                ["mime-type", "Mime type:", mime_type_from_load_spec(load_spec)]]);
647     try {
648         var file = yield I.minibuffer.read_file_check_overwrite(
649             $prompt = "Save page complete:",
650             $history = "save",
651             $initial_value = suggested_path);
652         // FIXME: use proper read function
653         var dir = yield I.minibuffer.read_file(
654             $prompt = "Data Directory:",
655             $history = "save",
656             $initial_value = file.path + ".support");
657     } finally {
658         panel.destroy();
659     }
661     save_document_complete(doc, file, dir, $buffer = I.buffer);