rename browser-follow-next/previous to follow-next/previous
[conkeror.git] / modules / element.js
blobcffd6e1c5286396e9c6797a3d5912dd284f8910f
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", $xpath_expression = "//img | //xhtml:img");
55 define_browser_object_class("frames", $handler = function (buf, prompt) {
56     check_buffer(buf, content_buffer);
57     var doc = buf.document;
58     if (doc.getElementsByTagName("frame").length == 0 &&
59         doc.getElementsByTagName("iframe").length == 0)
60     {
61         // only one frame (the top-level one), no need to use the hints system
62         yield co_return(buf.top_frame);
63     }
65     var result = yield buf.window.minibuffer.read_hinted_element(
66         $buffer = buf,
67         $prompt = prompt,
68         $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
69     yield co_return(result);
70 });
72 define_browser_object_class(
73     "links",
74     $xpath_expression =
75         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
76         "@class='lk' or @class='s' or @role='link'] | " +
77         "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
78         "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
79         "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
80         "//xhtml:button | //xhtml:select");
82 define_browser_object_class("mathml", $label = "MathML", $xpath_expression = "//m:math");
84 define_browser_object_class("top", $handler = function (buf, prompt) { yield co_return(buf.top_frame); });
87 define_variable(
88     "default_browser_object_classes",
89     {
90         follow: "links",
91         follow_top: "frames",
92         focus: "frames",
93         save: "links",
94         copy: "links",
95         view_source: "frames",
96         bookmark: "frames",
97         save_page: "frames",
98         save_page_complete: "top",
99         save_page_as_text: "frames",
100         default: "links"
101     },
102     "Specifies the default object class for each operation.\n" +
103         "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.");
105 interactive_context.prototype.browser_object_class = function (action_name) {
106     var cls =
107         this._browser_object_class ||
108         this.get("default_browser_object_classes")[action_name] ||
109         this.get("default_browser_object_classes")["default"];
110     return cls;
113 function browser_object_class_selector(name) {
114     return function (ctx, active_keymap, overlay_keymap) {
115         ctx._browser_object_class = name;
116         ctx.overlay_keymap = overlay_keymap || active_keymap;
117     }
120 function lookup_browser_object_class(class_name, action) {
121     var obj;
122     if (action != null) {
123         obj = browser_object_classes[class_name + "/" + action];
124         if (obj)
125             return obj;
126     }
127     return browser_object_classes[class_name];
130 interactive_context.prototype.read_browser_object = function(action, action_name, default_class, target)
132     var object_class_name = this.browser_object_class(action);
133     var object_class = lookup_browser_object_class(object_class_name, action);
135     var prompt = action_name;
136     if (target != null)
137         prompt += TARGET_PROMPTS[target];
138     if (object_class_name != default_class) {
139         var label = object_class.label || object_class_name;
140         prompt += " (" + label + ")";
141     }
142     prompt += ":";
144     var result = yield object_class.handler.call(null, this.buffer, prompt);
145     yield co_return(result);
149 function is_dom_node_or_window(elem) {
150     if (elem instanceof Ci.nsIDOMNode)
151         return true;
152     if (elem instanceof Ci.nsIDOMWindow)
153         return true;
154     return false;
158  * This is a simple wrapper function that sets focus to elem, and
159  * bypasses the automatic focus prevention system, which might
160  * otherwise prevent this from happening.
161  */
162 function browser_set_element_focus(buffer, elem, prevent_scroll) {
163     if (!is_dom_node_or_window(elem))
164         return;
166     buffer.last_user_input_received = Date.now();
167     if (prevent_scroll)
168         set_focus_no_scroll(buffer.window, elem);
169     else
170         elem.focus();
173 function browser_element_focus(buffer, elem)
175     if (!is_dom_node_or_window(elem))
176         return;
178     if (elem instanceof Ci.nsIDOMXULTextBoxElement)  {
179         // Focus the input field instead
180         elem = elem.wrappedJSObject.inputField;
181     }
183     browser_set_element_focus(buffer, elem);
184     if (elem instanceof Ci.nsIDOMWindow) {
185         return;
186     }
187     // If it is not a window, it must be an HTML element
188     var x = 0;
189     var y = 0;
190     if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
191         elem.contentWindow.focus();
192         return;
193     }
194     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
195         var coords = elem.getAttribute("coords").split(",");
196         x = Number(coords[0]);
197         y = Number(coords[1]);
198     }
200     var doc = elem.ownerDocument;
201     var evt = doc.createEvent("MouseEvents");
202     var doc = elem.ownerDocument;
204     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
205     elem.dispatchEvent(evt);
208 function browser_element_follow(buffer, target, elem)
210     browser_set_element_focus(buffer, elem, true /* no scroll */);
212     var no_click = (is_load_spec(elem) ||
213                     (elem instanceof Ci.nsIDOMWindow) ||
214                     (elem instanceof Ci.nsIDOMHTMLFrameElement) ||
215                     (elem instanceof Ci.nsIDOMHTMLIFrameElement) ||
216                     (elem instanceof Ci.nsIDOMHTMLLinkElement) ||
217                     (elem instanceof Ci.nsIDOMHTMLImageElement &&
218                      !elem.hasAttribute("onmousedown") && !elem.hasAttribute("onclick")));
220     if (target == FOLLOW_DEFAULT && !no_click) {
221         var x = 1, y = 1;
222         if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
223             var coords = elem.getAttribute("coords").split(",");
224             if (coords.length >= 2) {
225                 x = Number(coords[0]) + 1;
226                 y = Number(coords[1]) + 1;
227             }
228         }
229         browser_follow_link_with_click(buffer, elem, x, y);
230         return;
231     }
233     var spec = element_get_load_spec(elem);
234     if (spec == null) {
235         throw interactive_error("Element has no associated URL");
236         return;
237     }
239     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
240         // This URL won't work
241         throw interactive_error("Can't load javascript URL");
242     }
244     switch (target) {
245     case FOLLOW_CURRENT_FRAME:
246         var current_frame = load_spec_source_frame(spec);
247         if (current_frame && current_frame != buffer.top_frame) {
248             var target_obj = get_web_navigation_for_frame(current_frame);
249             apply_load_spec(target_obj, spec);
250             break;
251         }
252     case FOLLOW_DEFAULT:
253     case FOLLOW_TOP_FRAME:
254     case OPEN_CURRENT_BUFFER:
255         buffer.load(spec);
256         break;
257     case OPEN_NEW_WINDOW:
258     case OPEN_NEW_BUFFER:
259     case OPEN_NEW_BUFFER_BACKGROUND:
260         create_buffer(buffer.window,
261                       buffer_creator(content_buffer,
262                                      $load = spec,
263                                      $configuration = buffer.configuration),
264                       target);
265     }
269  * Follow a link-like element by generating fake mouse events.
270  */
271 function browser_follow_link_with_click(buffer, elem, x, y) {
272     var doc = elem.ownerDocument;
273     var view = doc.defaultView;
275     var evt = doc.createEvent("MouseEvents");
276     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
277                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
278     elem.dispatchEvent(evt);
280     evt.initMouseEvent("click", 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);
285 function element_get_load_spec(elem) {
287     if (is_load_spec(elem))
288         return elem;
290     var spec = null;
292     if (elem instanceof Ci.nsIDOMWindow)
293         spec = load_spec({document: elem.document});
295     else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
296              elem instanceof Ci.nsIDOMHTMLIFrameElement)
297         spec = load_spec({document: elem.contentDocument});
299     else {
300         var url = null;
301         var title = null;
303         if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
304             elem instanceof Ci.nsIDOMHTMLAreaElement ||
305             elem instanceof Ci.nsIDOMHTMLLinkElement) {
306             if (!elem.hasAttribute("href"))
307                 return null; // nothing can be done, as no nesting within these elements is allowed
308             url = elem.href;
309             title = elem.title || elem.textContent;
310         }
311         else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
312             url = elem.src;
313             title = elem.title || elem.alt;
314         }
315         else {
316             var node = elem;
317             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
318                 node = node.parentNode;
319             if (node && !node.hasAttribute("href"))
320                 node = null;
321             else
322                 url = node.href;
323             if (!node) {
324                 // Try simple XLink
325                 node = elem;
326                 while (node) {
327                     if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
328                         url = linkNode.getAttributeNS(XLINK_NS, "href");
329                         break;
330                     }
331                     node = node.parentNode;
332                 }
333                 if (url)
334                     url = makeURLAbsolute(node.baseURI, url);
335                 title = node.title || node.textContent;
336             }
337         }
338         if (url && url.length > 0) {
339             if (title && title.length == 0)
340                 title = null;
341             spec = load_spec({uri: url, source_frame: elem.ownerDocument.defaultView, title: title});
342         }
343     }
344     return spec;
347 interactive("follow", function (I) {
348     var target = I.browse_target("follow");
349     var element = yield I.read_browser_object("follow", "Follow", "links", target);
350     browser_element_follow(I.buffer, target, element);
353 interactive("follow-top", function (I) {
354     var target = I.browse_target("follow-top");
355     var element = yield I.read_browser_object("follow_top", "Follow", "links", target);
356     browser_element_follow(I.buffer, target, element);
359 interactive("focus", function (I) {
360     var element = yield I.read_browser_object("focus", "Focus");
361     browser_element_focus(I.buffer, element);
364 function element_get_load_target_label(element) {
365     if (element instanceof Ci.nsIDOMWindow)
366         return "page";
367     if (element instanceof Ci.nsIDOMHTMLFrameElement)
368         return "frame";
369     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
370         return "iframe";
371     return null;
374 function element_get_operation_label(element, op_name, suffix) {
375     var target_label = element_get_load_target_label(element);
376     if (target_label != null)
377         target_label = " " + target_label;
378     else
379         target_label = "";
381     if (suffix != null)
382         suffix = " " + suffix;
383     else
384         suffix = "";
386     return op_name + target_label + suffix + ":";
389 interactive("save", function (I) {
390     var element = yield I.read_browser_object("save", "Save", "links");
392     var spec = element_get_load_spec(element);
393     if (spec == null)
394         throw interactive_error("Element has no associated URI");
396     var panel;
397     panel = create_info_panel(I.window, "download-panel",
398                               [["downloading",
399                                 element_get_operation_label(element, "Saving"),
400                                 load_spec_uri_string(spec)],
401                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
403     try {
404         var file = yield I.minibuffer.read_file_check_overwrite(
405             $prompt = "Save as:",
406             $initial_value = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer),
407             $history = "save");
409     } finally {
410         panel.destroy();
411     }
413     save_uri(spec, file,
414              $buffer = I.buffer,
415              $use_cache = false);
418 function browser_element_copy(buffer, elem)
420     var spec = element_get_load_spec(elem);
421     var text = null;
422     if (spec)
423         text = load_spec_uri_string(spec);
424     else  {
425         if (!(elem instanceof Ci.nsIDOMNode))
426             throw interactive_error("Element has no associated text to copy.");
427         switch (elem.localName) {
428         case "INPUT":
429         case "TEXTAREA":
430             text = elem.value;
431             break;
432         case "SELECT":
433             if (elem.selectedIndex >= 0)
434                 text = elem.item(elem.selectedIndex).text;
435             break;
436         default:
437             text = elem.textContent;
438             break;
439         }
440     }
441     writeToClipboard (text);
442     buffer.window.minibuffer.message ("Copied: " + text);
446 interactive("copy", function (I) {
447     var element = yield I.read_browser_object("copy", "Copy", "links");
448     browser_element_copy(I.buffer, element);
451 var view_source_use_external_editor = false, view_source_function = null;
452 function browser_element_view_source(buffer, target, elem)
454     if (view_source_use_external_editor || view_source_function)
455     {
456         var spec = element_get_load_spec(elem);
457         if (spec == null) {
458             throw interactive_error("Element has no associated URL");
459             return;
460         }
462         let [file, temp] = yield download_as_temporary(spec,
463                                                        $buffer = buffer,
464                                                        $action = "View source");
465         if (view_source_use_external_editor)
466             yield open_file_with_external_editor(file, $temporary = temp);
467         else
468             yield view_source_function(file, $temporary = temp);
469         return;
470     }
472     var win = null;
473     var window = buffer.window;
474     if (elem.localName) {
475         switch (elem.localName.toLowerCase()) {
476         case "frame": case "iframe":
477             win = elem.contentWindow;
478             break;
479         case "math":
480             view_mathml_source (window, charset, elem);
481             return;
482         default:
483             throw new Error("Invalid browser element");
484         }
485     } else
486         win = elem;
487     win.focus();
489     var url_s = win.location.href;
490     if (url_s.substring (0,12) != "view-source:") {
491         try {
492             open_in_browser(buffer, target, "view-source:" + url_s);
493         } catch(e) { dump_error(e); }
494     } else {
495         window.minibuffer.message ("Already viewing source");
496     }
499 interactive("view-source", function (I) {
500     var target = I.browse_target("follow");
501     var element = yield I.read_browser_object("view_source", "View source", "frames", target);
502     yield browser_element_view_source(I.buffer, target, element);
505 interactive("shell-command-on-url", function (I) {
506     var cwd = I.cwd;
507     var element = yield I.read_browser_object("shell_command_url", "URL shell command target", "links");
508     var spec = element_get_load_spec(element);
509     if (spec == null)
510         throw interactive_error("Unable to obtain URI from element");
512     var uri = load_spec_uri_string(spec);
514     var panel;
515     panel = create_info_panel(I.window, "download-panel",
516                               [["downloading",
517                                 element_get_operation_label(element, "Running on", "URI"),
518                                 load_spec_uri_string(spec)],
519                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
521     try {
522         var cmd = yield I.minibuffer.read_shell_command(
523             $cwd = cwd,
524             $initial_value = load_spec_default_shell_command(spec));
525     } finally {
526         panel.destroy();
527     }
529     shell_command_with_argument_blind(cmd, uri, $cwd = cwd);
532 function browser_element_shell_command(buffer, elem, command) {
533     var spec = element_get_load_spec(elem);
534     if (spec == null) {
535         throw interactive_error("Element has no associated URL");
536         return;
537     }
538     yield download_as_temporary(spec,
539                                 $buffer = buffer,
540                                 $shell_command = command,
541                                 $shell_command_cwd = buffer.cwd);
544 interactive("shell-command-on-file", function (I) {
545     var cwd = I.cwd;
546     var element = yield I.read_browser_object("shell_command", "Shell command target", "links");
548     var spec = element_get_load_spec(element);
549     if (spec == null)
550         throw interactive_error("Unable to obtain URI from element");
552     var uri = load_spec_uri_string(spec);
554     var panel;
555     panel = create_info_panel(I.window, "download-panel",
556                               [["downloading",
557                                 element_get_operation_label(element, "Running on"),
558                                 load_spec_uri_string(spec)],
559                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
561     try {
563         var cmd = yield I.minibuffer.read_shell_command(
564             $cwd = cwd,
565             $initial_value = load_spec_default_shell_command(spec));
566     } finally {
567         panel.destroy();
568     }
570     /* FIXME: specify cwd as well */
571     yield browser_element_shell_command(I.buffer, element, cmd);
574 interactive("bookmark", function (I) {
575     var element = yield I.read_browser_object("bookmark", "Bookmark", "frames");
576     var spec = element_get_load_spec(element);
577     if (!spec)
578         throw interactive_error("Element has no associated URI");
579     var uri_string = load_spec_uri_string(spec);
580     var panel;
581     panel = create_info_panel(I.window, "bookmark-panel",
582                               [["bookmarking",
583                                 element_get_operation_label(element, "Bookmarking"),
584                                 uri_string]]);
585     try {
586         var title = yield I.minibuffer.read($prompt = "Bookmark with title:", $initial_value = load_spec_title(spec) || "");
587     } finally {
588         panel.destroy();
589     }
590     add_bookmark(uri_string, title);
591     I.minibuffer.message("Added bookmark: " + uri_string + " - " + title);
594 interactive("save-page", function (I) {
595     check_buffer(I.buffer, content_buffer);
596     var element = yield I.read_browser_object("save_page", "Save page", "frames");
597     var spec = element_get_load_spec(element);
598     if (!spec || !load_spec_document(spec))
599         throw interactive_error("Element is not associated with a document.");
600     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
602     var panel;
603     panel = create_info_panel(I.window, "download-panel",
604                               [["downloading",
605                                 element_get_operation_label(element, "Saving"),
606                                 load_spec_uri_string(spec)],
607                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
609     try {
610         var file = yield I.minibuffer.read_file_check_overwrite(
611             $prompt = "Save page as:",
612             $history = "save",
613             $initial_value = suggested_path);
614     } finally {
615         panel.destroy();
616     }
618     save_uri(spec, file, $buffer = I.buffer);
621 interactive("save-page-as-text", function (I) {
622     check_buffer(I.buffer, content_buffer);
623     var element = yield I.read_browser_object("save_page_as_text", "Save page as text", "frames");
624     var spec = element_get_load_spec(element);
625     var doc;
626     if (!spec || !(doc = load_spec_document(spec)))
627         throw interactive_error("Element is not associated with a document.");
628     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec, "txt"), I.buffer);
630     var panel;
631     panel = create_info_panel(I.window, "download-panel",
632                               [["downloading",
633                                 element_get_operation_label(element, "Saving", "as text"),
634                                 load_spec_uri_string(spec)],
635                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
637     try {
638         var file = yield I.minibuffer.read_file_check_overwrite(
639             $prompt = "Save page as text:",
640             $history = "save",
641             $initial_value = suggested_path);
642     } finally {
643         panel.destroy();
644     }
646     save_document_as_text(doc, file, $buffer = I.buffer);
649 interactive("save-page-complete", function (I) {
650     check_buffer(I.buffer, content_buffer);
651     var element = yield I.read_browser_object("save_page_complete", "Save page complete", "frames");
652     var spec = element_get_load_spec(element);
653     var doc;
654     if (!spec || !(doc = load_spec_document(spec)))
655         throw interactive_error("Element is not associated with a document.");
656     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
658     var panel;
659     panel = create_info_panel(I.window, "download-panel",
660                               [["downloading",
661                                 element_get_operation_label(element, "Saving complete"),
662                                 load_spec_uri_string(spec)],
663                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
665     try {
666         var file = yield I.minibuffer.read_file_check_overwrite(
667             $prompt = "Save page complete:",
668             $history = "save",
669             $initial_value = suggested_path);
670         // FIXME: use proper read function
671         var dir = yield I.minibuffer.read_file(
672             $prompt = "Data Directory:",
673             $history = "save",
674             $initial_value = file.path + ".support");
675     } finally {
676         panel.destroy();
677     }
679     save_document_complete(doc, file, dir, $buffer = I.buffer);