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