element_dom_node_or_window_p: renamed to dom_node_or_window_p
[conkeror.git] / modules / element.js
blob2c963bc30cf54f55dd991ea6e9b175e5a8424087
1 /**
2  * (C) Copyright 2007-2009 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 in_module(null);
14 require("hints.js");
15 require("save.js");
16 require("mime-type-override.js");
17 require("minibuffer-read-mime-type.js");
19 var browser_object_classes = {};
21 /**
22  * browser_object_class
23  *
24  *   In normal cases, make a new browser_object_class with the function,
25  * `define_browser_object_class'.
26  *
27  * name: See note on `define_browser_object_class'.
28  *
29  * doc: a docstring
30  *
31  * handler: a coroutine called as: handler(I, prompt).  `I' is a normal
32  *          interactive context.  `prompt' is there to pass along as the
33  *          $prompt of various minibuffer read procedures, if needed.
34  *
35  * $hint: short string (usually verb and noun) to describe the UI
36  *        of the browser object class to the user.  Only used by
37  *        browser object classes which make use of the minibuffer.
38  */
39 define_keywords("$hint");
40 function browser_object_class (name, doc, handler) {
41     keywords(arguments);
42     this.name = name;
43     this.handler = handler;
44     this.doc = doc;
45     this.hint = arguments.$hint;
48 /**
49  * define_browser_object_class
50  *
51  *   In normal cases, make a new browser_object_class with the function,
52  * `define_browser_object_class'.
53  *
54  * name: the name of the browser object class.  multiword names should be
55  *       hyphenated.  From this name, a variable browser_object_NAME and
56  *       an interactive command browser-object-NAME will be generated.
57  *
58  *   Other arguments are as for `browser_object_class'.
59  */
60 // keywords: $hint
61 function define_browser_object_class (name, doc, handler) {
62     keywords(arguments);
63     var varname = 'browser_object_'+name.replace('-','_','g');
64     var ob = conkeror[varname] =
65         new browser_object_class(name, doc, handler,
66                                  forward_keywords(arguments));
67     interactive("browser-object-"+name,
68         "A prefix command to specify that the following command operate "+
69         "on objects of type: "+name+".",
70         function (I) { I.browser_object = ob; },
71         $prefix = true);
72     return ob;
75 /**
76  * xpath_browser_object_handler
77  *
78  *   This generates a function of the type needed for a handler of a
79  * browser object class.  The handler uses `read_hinted_element' of
80  * hints.js to let the user pick a DOM node from those matched by
81  * `xpath_expression'.
82  */
83 function xpath_browser_object_handler (xpath_expression) {
84     return function (I, prompt) {
85         var result = yield I.buffer.window.minibuffer.read_hinted_element(
86             $buffer = I.buffer,
87             $prompt = prompt,
88             $hint_xpath_expression = xpath_expression);
89         yield co_return(result);
90     };
93 define_browser_object_class("images",
94     "Browser object class for selecting an html:img via hinting.",
95     xpath_browser_object_handler("//img | //xhtml:img"),
96     $hint = "select image");
98 define_browser_object_class("frames",
99     "Browser object class for selecting a frame or iframe via hinting.",
100     function (I, prompt) {
101         var doc = I.buffer.document;
102         // Check for any frames or visible iframes
103         var skip_hints = true;
104         if (doc.getElementsByTagName("frame").length > 0)
105             skip_hints = false;
106         else {
107             let topwin = I.buffer.top_frame;
108             let iframes = doc.getElementsByTagName("iframe");
109             for (var i = 0, nframes = iframes.length; i < nframes; i++) {
110                 let style = topwin.getComputedStyle(iframes[i], "");
111                 if (style.display == "none" || style.visibility == "hidden")
112                     continue;
113                 skip_hints = false;
114                 break;
115             }
116         }
117         if (skip_hints) {
118             // only one frame (the top-level one), no need to use the hints system
119             yield co_return(I.buffer.top_frame);
120         }
121         var result = yield I.buffer.window.minibuffer.read_hinted_element(
122             $buffer = I.buffer,
123             $prompt = prompt,
124             $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
125         yield co_return(result);
126     },
127     $hint = "select frame");
129 define_browser_object_class("links",
130     "Browser object class for selecting a hyperlink, form field, "+
131     "or link-like element, via hinting.",
132     xpath_browser_object_handler(
133         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or "+
134             "@oncommand or @role='link' or @role='button' or @role='menuitem'] | "+
135         "//input[not(@type='hidden')] | //a[@href] | //area | "+
136         "//iframe | //textarea | //button | //select | "+
137         "//*[@contenteditable = 'true'] | "+
138         "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or "+
139                   "@oncommand or @role='link' or @role='button' or @role='menuitem'] | "+
140         "//xhtml:input[not(@type='hidden')] | //xhtml:a[@href] | //xhtml:area | "+
141         "//xhtml:iframe | //xhtml:textarea | //xhtml:button | //xhtml:select | " +
142         "//xhtml:*[@contenteditable = 'true'] | "+
143         "//svg:a"),
144     $hint = "select link");
146 define_browser_object_class("mathml",
147     "Browser object class for selecting a MathML node via hinting.",
148     xpath_browser_object_handler("//m:math"),
149     $hint = "select MathML element");
151 define_browser_object_class("top",
152     "Browser object class which returns the top frame of the document.",
153     function (I, prompt) { return I.buffer.top_frame; });
155 define_browser_object_class("url",
156     "Browser object class which prompts the user for an url or webjump.",
157     function (I, prompt) {
158         var result = yield I.buffer.window.minibuffer.read_url($prompt = prompt);
159         yield co_return(result);
160     },
161     $hint = "enter URL/webjump");
163 define_browser_object_class("paste-url",
164     "Browser object which reads an url from the X Primary Selection, "+
165     "falling back on the clipboard for operating systems which lack one.",
166     function (I, prompt) {
167                 var url = read_from_x_primary_selection();
168                 // trim spaces
169                 url = url.replace(/^\s*|\s*$/,"");
170                 // add http:// if needed
171                 if (url.match(/^[^:]+\./)) {
172                         url = "http://" + url;
173                 }
174         try {
175             return make_uri(url).spec;
176         } catch (e) {
177             throw new interactive_error("error: malformed url: "+url);
178         }
179     });
181 define_browser_object_class("file",
182     "Browser object which prompts for a file name.",
183     function (I, prompt) {
184         var result = yield I.buffer.window.minibuffer.read_file(
185             $prompt = prompt,
186             $history = I.command.name+"/file",
187             $initial_value = I.local.cwd.path);
188         yield co_return(result);
189     },
190     $hint = "enter file name");
192 define_browser_object_class("alt",
193     "Browser object class which returns the alt text of an html:img, "+
194     "selected via hinting",
195     function (I, prompt) {
196         var result = yield I.buffer.window.minibuffer.read_hinted_element(
197             $buffer = I.buffer,
198             $prompt = prompt,
199             $hint_xpath_expression = "//img[@alt] | //xhtml:img[@alt]");
200         yield co_return(result.alt);
201     },
202     $hint = "select image for alt-text");
204 define_browser_object_class("title",
205     "Browser object class which returns the title attribute of an element, "+
206     "selected via hinting",
207     function (I, prompt) {
208         var result = yield I.buffer.window.minibuffer.read_hinted_element(
209             $buffer = I.buffer,
210             $prompt = prompt,
211             $hint_xpath_expression = "//*[@title] | //xhtml:*[@title]");
212         yield co_return(result.title);
213     },
214     $hint = "select element for title attribute");
216 define_browser_object_class("title-or-alt",
217     "Browser object which is the union of browser-object-alt and "+
218     "browser-object-title, with title having higher precedence in "+
219     "the case of an element that has both.",
220     function (I, prompt) {
221         var result = yield I.buffer.window.minibuffer.read_hinted_element(
222             $buffer = I.buffer,
223             $prompt = prompt,
224             $hint_xpath_expression = "//img[@alt] | //*[@title] | //xhtml:img[@alt] | //xhtml:*[@title]");
225         yield co_return(result.title ? result.title : result.alt);
226     },
227     $hint = "select element for title or alt-text");
229 define_browser_object_class("scrape-url",
230     "Browser object which lets the user choose an url from a list of "+
231     "urls scraped from the source code of the document.",
232     function (I, prompt) {
233         var completions = I.buffer.document.documentElement.innerHTML
234             .match(/https?:[^\s<>)"]*/g)
235             .filter(remove_duplicates_filter());
236         var completer = all_word_completer($completions = completions);
237         var result = yield I.buffer.window.minibuffer.read(
238             $prompt = prompt,
239             $completer = completer,
240             $initial_value = null,
241             $auto_complete = "url",
242             $select,
243             $match_required = false);
244         yield co_return(result);
245     },
246     $hint = "choose scraped URL");
248 define_browser_object_class("up-url",
249     "Browser object which returns the url one level above the current one.",
250     function (I, prompt) {
251         var up = compute_url_up_path(I.buffer.current_uri.spec);
252         return I.buffer.current_uri.resolve(up);
253     });
255 define_browser_object_class("focused-element",
256     "Browser object which returns the focused element.",
257     function (I, prompt) { return I.buffer.focused_element; });
259 define_browser_object_class("dom-node", null,
260     xpath_browser_object_handler("//* | //xhtml:*"),
261     $hint = "select DOM node");
263 define_browser_object_class("fragment-link",
264     "Browser object class which returns a link to the specified fragment of a page",
265     function (I, prompt) {
266         var elem = yield I.buffer.window.minibuffer.read_hinted_element(
267             $buffer = I.buffer,
268             $prompt = prompt,
269             $hint_xpath_expression = "//*[@id] | //a[@name] | //xhtml:*[@id] | //xhtml:a[@name]");
270         yield co_return(page_fragment_load_spec(elem));
271     },
272     $hint = "select element to link to");
274 interactive("browser-object-text",
275     "Composable browser object which returns the text of another object.",
276     function (I) {
277         // our job here is to modify the interactive context.
278         // set I.browser_object to a browser_object which calls the
279         // original one, then returns its text.
280         var b = I.browser_object;
281         I.browser_object = function (I) {
282             I.browser_object = b;
283             var e = yield read_browser_object(I);
284             if (e instanceof Ci.nsIDOMHTMLImageElement)
285                 yield co_return(e.getAttribute("alt"));
286             yield co_return(e.textContent);
287         };
288     },
289     $prefix);
291 function get_browser_object (I) {
292     var obj = I.browser_object;
293     var cmd = I.command;
295     // if there was no interactive browser-object,
296     // binding_browser_object becomes the default.
297     if (obj === undefined) {
298         obj = I.binding_browser_object;
299     }
300     // if the command's default browser object is a non-null literal,
301     // it overrides an interactive browser-object, but not a binding
302     // browser object.
303     if (cmd.browser_object != null &&
304         (! (cmd.browser_object instanceof browser_object_class)) &&
305         (I.binding_browser_object === undefined))
306     {
307         obj = cmd.browser_object;
308     }
309     // if we still have no browser-object, look for a page-mode
310     // default, or finally the command default.
311     if (obj === undefined) {
312         obj = (I.buffer &&
313                I.buffer.default_browser_object_classes[cmd.name]) ||
314             cmd.browser_object;
315     }
317     return obj;
320 function read_browser_object (I) {
321     var browser_object = get_browser_object(I);
322     if (browser_object === undefined)
323         throw interactive_error("No browser object");
325     var result;
326     // literals cannot be overridden
327     if (browser_object instanceof Function) {
328         result = yield browser_object(I);
329         yield co_return(result);
330     }
331     if (! (browser_object instanceof browser_object_class))
332         yield co_return(browser_object);
334     var prompt = I.command.prompt;
335     if (! prompt) {
336         prompt = I.command.name.split(/-|_/).join(" ");
337         prompt = prompt[0].toUpperCase() + prompt.substring(1);
338     }
339     if (I.target != null)
340         prompt += TARGET_PROMPTS[I.target];
341     if (browser_object.hint)
342         prompt += " (" + browser_object.hint + ")";
343     prompt += ":";
345     result = yield browser_object.handler.call(null, I, prompt);
346     yield co_return(result);
351  * This is a simple wrapper function that sets focus to elem, and
352  * bypasses the automatic focus prevention system, which might
353  * otherwise prevent this from happening.
354  */
355 function browser_set_element_focus (buffer, elem, prevent_scroll) {
356     if (! dom_node_or_window_p(elem))
357         return;
358     if (! elem.focus)
359         return;
360     if (prevent_scroll)
361         set_focus_no_scroll(buffer.window, elem);
362     else
363         elem.focus();
366 function browser_element_focus (buffer, elem) {
367     if (! dom_node_or_window_p(elem))
368         return;
370     if (elem instanceof Ci.nsIDOMXULTextBoxElement)
371         elem = elem.wrappedJSObject.inputField; // focus the input field
373     browser_set_element_focus(buffer, elem);
374     if (elem instanceof Ci.nsIDOMWindow)
375         return;
377     // If it is not a window, it must be an HTML element
378     var x = 0;
379     var y = 0;
380     if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
381         elem instanceof Ci.nsIDOMHTMLIFrameElement)
382     {
383         elem.contentWindow.focus();
384         return;
385     }
386     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
387         var coords = elem.getAttribute("coords").split(",");
388         x = Number(coords[0]);
389         y = Number(coords[1]);
390     }
392     var doc = elem.ownerDocument;
393     var evt = doc.createEvent("MouseEvents");
395     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
396     elem.dispatchEvent(evt);
399 function browser_object_follow (buffer, target, elem) {
400     // XXX: would be better to let nsILocalFile objects be load_specs
401     if (elem instanceof Ci.nsILocalFile)
402         elem = elem.path;
404     var e;
405     if (elem instanceof load_spec)
406         e = load_spec_element(elem);
407     if (! e)
408         e = elem;
410     browser_set_element_focus(buffer, e, true /* no scroll */);
412     var no_click = (((elem instanceof load_spec) &&
413                      load_spec_forced_charset(elem)) ||
414                     (e instanceof load_spec) ||
415                     (e instanceof Ci.nsIDOMWindow) ||
416                     (e instanceof Ci.nsIDOMHTMLFrameElement) ||
417                     (e instanceof Ci.nsIDOMHTMLIFrameElement) ||
418                     (e instanceof Ci.nsIDOMHTMLLinkElement) ||
419                     (e instanceof Ci.nsIDOMHTMLImageElement &&
420                      !e.hasAttribute("onmousedown") && !e.hasAttribute("onclick")));
422     if (target == FOLLOW_DEFAULT && !no_click) {
423         var x = 1, y = 1;
424         if (e instanceof Ci.nsIDOMHTMLAreaElement) {
425             var coords = e.getAttribute("coords").split(",");
426             if (coords.length >= 2) {
427                 x = Number(coords[0]) + 1;
428                 y = Number(coords[1]) + 1;
429             }
430         }
431         dom_node_click(e, x, y);
432         return;
433     }
435     var spec = load_spec(elem);
437     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
438         // it is nonsensical to follow a javascript url in a different
439         // buffer or window
440         target = FOLLOW_DEFAULT;
441     } else if (!(buffer instanceof content_buffer) &&
442         (target == FOLLOW_CURRENT_FRAME ||
443          target == FOLLOW_DEFAULT ||
444          target == OPEN_CURRENT_BUFFER))
445     {
446         target = OPEN_NEW_BUFFER;
447     }
449     switch (target) {
450     case FOLLOW_CURRENT_FRAME:
451         var current_frame = load_spec_source_frame(spec);
452         if (current_frame && current_frame != buffer.top_frame) {
453             var target_obj = get_web_navigation_for_frame(current_frame);
454             apply_load_spec(target_obj, spec);
455             break;
456         }
457     case FOLLOW_DEFAULT:
458     case OPEN_CURRENT_BUFFER:
459         buffer.load(spec);
460         break;
461     case OPEN_NEW_WINDOW:
462     case OPEN_NEW_BUFFER:
463     case OPEN_NEW_BUFFER_BACKGROUND:
464         if (dom_node_or_window_p(e))
465             var opener = buffer;
466         else
467             opener = null;
468         create_buffer(buffer.window,
469                       buffer_creator(content_buffer,
470                                      $opener = opener,
471                                      $load = spec),
472                       target);
473     }
477  * Follow a link-like element by generating fake mouse events.
478  */
479 function dom_node_click (elem, x, y) {
480     var doc = elem.ownerDocument;
481     var view = doc.defaultView;
483     var evt = doc.createEvent("MouseEvents");
484     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
485                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
486     elem.dispatchEvent(evt);
488     evt = doc.createEvent("MouseEvents");
489     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
490                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
491     elem.dispatchEvent(evt);
493     evt = doc.createEvent("MouseEvents");
494     evt.initMouseEvent("mouseup", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
495                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
496     elem.dispatchEvent(evt);
500 function follow (I, target) {
501     if (target == null)
502         target = FOLLOW_DEFAULT;
503     I.target = target;
504     if (target == OPEN_CURRENT_BUFFER)
505         check_buffer(I.buffer, content_buffer);
506     var element = yield read_browser_object(I);
507     try {
508         element = load_spec(element);
509         if (I.forced_charset)
510             element.forced_charset = I.forced_charset;
511     } catch (e) {}
512     browser_object_follow(I.buffer, target, element);
515 function follow_new_buffer (I) {
516     yield follow(I, OPEN_NEW_BUFFER);
519 function follow_new_buffer_background (I) {
520     yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
523 function follow_new_window (I) {
524     yield follow(I, OPEN_NEW_WINDOW);
527 function follow_current_frame (I) {
528     yield follow(I, FOLLOW_CURRENT_FRAME);
531 function follow_current_buffer (I) {
532     yield follow(I, OPEN_CURRENT_BUFFER);
536 function element_get_load_target_label (element) {
537     if (element instanceof Ci.nsIDOMWindow)
538         return "page";
539     if (element instanceof Ci.nsIDOMHTMLFrameElement)
540         return "frame";
541     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
542         return "iframe";
543     return null;
546 function element_get_operation_label (element, op_name, suffix) {
547     var target_label = element_get_load_target_label(element);
548     if (target_label != null)
549         target_label = " " + target_label;
550     else
551         target_label = "";
553     if (suffix != null)
554         suffix = " " + suffix;
555     else
556         suffix = "";
558     return op_name + target_label + suffix + ":";
562 function browser_element_copy (buffer, elem) {
563     try {
564        var spec = load_spec(elem);
565     } catch (e) {}
566     var text = null;
567     if (typeof elem == "string" || elem instanceof String)
568         text = elem;
569     else if (spec)
570         text = load_spec_uri_string(spec);
571     else {
572         if (!(elem instanceof Ci.nsIDOMNode))
573             throw interactive_error("Element has no associated text to copy.");
574         var tag = elem.localName.toLowerCase();
575         if ((tag == "input" || tag == "button") &&
576             elem.type == "submit" && elem.form && elem.form.action)
577         {
578             text = elem.form.action;
579         } else if (tag == "input" || tag == "textarea") {
580             text = elem.value;
581         } else if (tag == "select") {
582             if (elem.selectedIndex >= 0)
583                 text = elem.item(elem.selectedIndex).text;
584         } else {
585             text = elem.textContent;
586         }
587     }
588     browser_set_element_focus(buffer, elem);
589     writeToClipboard(text);
590     buffer.window.minibuffer.message("Copied: " + text);
594 define_variable("view_source_use_external_editor", false,
595     "When true, the `view-source' command will send its document to "+
596     "your external editor.");
598 define_variable("view_source_function", null,
599     "May be set to a user-defined function for viewing source code. "+
600     "The function should accept an nsILocalFile of the filename as "+
601     "its one positional argument, and it will also be called with "+
602     "the keyword `$temporary', whose value will be true if the file "+
603     "is considered temporary, and therefore the function must take "+
604     "responsibility for deleting it.");
606 function browser_object_view_source (buffer, target, elem) {
607     if (view_source_use_external_editor || view_source_function) {
608         var spec = load_spec(elem);
610         let [file, temp] = yield download_as_temporary(spec,
611                                                        $buffer = buffer,
612                                                        $action = "View source");
613         if (view_source_use_external_editor)
614             yield open_file_with_external_editor(file, $temporary = temp);
615         else
616             yield view_source_function(file, $temporary = temp);
617         return;
618     }
620     var win = null;
621     var window = buffer.window;
622     if (elem.localName) {
623         switch (elem.localName.toLowerCase()) {
624         case "frame": case "iframe":
625             win = elem.contentWindow;
626             break;
627         case "math":
628             view_mathml_source(window, charset, elem);
629             return;
630         default:
631             throw new Error("Invalid browser element");
632         }
633     } else
634         win = elem;
635     win.focus();
637     var url_s = win.location.href;
638     if (url_s.substring (0,12) != "view-source:") {
639         try {
640             browser_object_follow(buffer, target, "view-source:" + url_s);
641         } catch(e) { dump_error(e); }
642     } else {
643         try {
644             browser_object_follow(buffer, target, url_s.replace(/^view-source\:/, ''));
645         } catch(e) { dump_error(e); }
646     }
649 function view_source (I, target) {
650     I.target = target;
651     if (target == null)
652         target = OPEN_CURRENT_BUFFER;
653     var element = yield read_browser_object(I);
654     yield browser_object_view_source(I.buffer, target, element);
657 function view_source_new_buffer (I) {
658     yield view_source(I, OPEN_NEW_BUFFER);
661 function view_source_new_window (I) {
662     yield view_source(I, OPEN_NEW_WINDOW);
666 function browser_element_shell_command (buffer, elem, command, cwd) {
667     var spec = load_spec(elem);
668     yield download_as_temporary(spec,
669                                 $buffer = buffer,
670                                 $shell_command = command,
671                                 $shell_command_cwd = cwd);
674 provide("element");