Added support for generating URLs with fragment indentifiers (eg #foo).
[conkeror.git] / modules / element.js
blobdbdf25975e284aae10304538fc409d2d3c540577
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 @oncommand or @role='link'] | " +
132         "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
133         "//*[@contenteditable = 'true'] |" +
134         "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @role='link'] | " +
135         "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | //xhtml:button | //xhtml:select | " +
136         "//xhtml:*[@contenteditable = 'true']"),
137     $hint = "select link");
139 define_browser_object_class("mathml",
140     "Browser object class for selecting a MathML node via hinting.",
141     xpath_browser_object_handler("//m:math"),
142     $hint = "select MathML element");
144 define_browser_object_class("top",
145     "Browser object class which returns the top frame of the document.",
146     function (I, prompt) { return I.buffer.top_frame; });
148 define_browser_object_class("url",
149     "Browser object class which prompts the user for an url or webjump.",
150     function (I, prompt) {
151         var result = yield I.buffer.window.minibuffer.read_url($prompt = prompt);
152         yield co_return(result);
153     },
154     $hint = "enter URL/webjump");
156 define_browser_object_class("paste-url",
157     "Browser object which reads an url from the X Primary Selection, "+
158     "falling back on the clipboard for operating systems which lack one.",
159     function (I, prompt) {
160                 var url = read_from_x_primary_selection();
161                 // trim spaces
162                 url = url.replace(/^\s*|\s*$/,"");
163                 // add http:// if needed
164                 if (url.match(/^[^:]+\./)) {
165                         url = "http://" + url;
166                 }
167         try {
168             return make_uri(url).spec;
169         } catch (e) {
170             throw new interactive_error("error: malformed url: "+url);
171         }
172     });
174 define_browser_object_class("file",
175     "Browser object which prompts for a file name.",
176     function (I, prompt) {
177         var result = yield I.buffer.window.minibuffer.read_file(
178             $prompt = prompt,
179             $history = I.command.name+"/file",
180             $initial_value = I.local.cwd.path);
181         yield co_return(result);
182     },
183     $hint = "enter file name");
185 define_browser_object_class("alt",
186     "Browser object class which returns the alt text of an html:img, "+
187     "selected via hinting",
188     function (I, prompt) {
189         var result = yield I.buffer.window.minibuffer.read_hinted_element(
190             $buffer = I.buffer,
191             $prompt = prompt,
192             $hint_xpath_expression = "//img[@alt] | //xhtml:img[@alt]");
193         yield co_return(result.alt);
194     },
195     $hint = "select image for alt-text");
197 define_browser_object_class("title",
198     "Browser object class which returns the title attribute of an element, "+
199     "selected via hinting",
200     function (I, prompt) {
201         var result = yield I.buffer.window.minibuffer.read_hinted_element(
202             $buffer = I.buffer,
203             $prompt = prompt,
204             $hint_xpath_expression = "//*[@title] | //xhtml:*[@title]");
205         yield co_return(result.title);
206     },
207     $hint = "select element for title attribute");
209 define_browser_object_class("title-or-alt",
210     "Browser object which is the union of browser-object-alt and "+
211     "browser-object-title, with title having higher precedence in "+
212     "the case of an element that has both.",
213     function (I, prompt) {
214         var result = yield I.buffer.window.minibuffer.read_hinted_element(
215             $buffer = I.buffer,
216             $prompt = prompt,
217             $hint_xpath_expression = "//img[@alt] | //*[@title] | //xhtml:img[@alt] | //xhtml:*[@title]");
218         yield co_return(result.title ? result.title : result.alt);
219     },
220     $hint = "select element for title or alt-text");
222 define_browser_object_class("scrape-url",
223     "Browser object which lets the user choose an url from a list of "+
224     "urls scraped from the source code of the document.",
225     function (I, prompt) {
226         var completions = I.buffer.document.documentElement.innerHTML
227             .match(/http:[^\s>"]*/g)
228             .filter(remove_duplicates_filter());
229         var completer = prefix_completer($completions = completions);
230         var result = yield I.buffer.window.minibuffer.read(
231             $prompt = prompt,
232             $completer = completer,
233             $initial_value = null,
234             $auto_complete = "url",
235             $select,
236             $match_required = false);
237         yield co_return(result);
238     },
239     $hint = "choose scraped URL");
241 define_browser_object_class("up-url",
242     "Browser object which returns the url one level above the current one.",
243     function (I, prompt) {
244         var up = compute_url_up_path(I.buffer.current_uri.spec);
245         return I.buffer.current_uri.resolve(up);
246     });
248 define_browser_object_class("focused-element",
249     "Browser object which returns the focused element.",
250     function (I, prompt) { return I.buffer.focused_element; });
252 define_browser_object_class("dom-node", null,
253     xpath_browser_object_handler("//* | //xhtml:*"),
254     $hint = "select DOM node");
256 define_browser_object_class("fragment-link",
257     "Browser object class which returns a link to the specified fragment of a page",
258     function (I, prompt) {
259         var elem = yield I.buffer.window.minibuffer.read_hinted_element(
260             $buffer = I.buffer,
261             $prompt = prompt,
262             $hint_xpath_expression = "//*[@id] | //a[@name] | //xhtml:*[@id] | //xhtml:a[@name]");
263         yield co_return(page_fragment_load_spec(elem));
264     },
265     $hint = "select element to link to");
267 interactive("browser-object-text",
268     "Composable browser object which returns the text of another object.",
269     function (I) {
270         // our job here is to modify the interactive context.
271         // set I.browser_object to a browser_object which calls the
272         // original one, then returns its text.
273         var b = I.browser_object;
274         I.browser_object = function (I) {
275             I.browser_object = b;
276             var e = yield read_browser_object(I);
277             if (e instanceof Ci.nsIDOMHTMLImageElement)
278                 yield co_return(e.getAttribute("alt"));
279             yield co_return(e.textContent);
280         }
281     },
282     $prefix);
284 function read_browser_object (I) {
285     var browser_object = I.browser_object;
286     var result;
287     // literals cannot be overridden
288     if (browser_object instanceof Function) {
289         result = yield browser_object(I);
290         yield co_return(result);
291     }
292     if (! (browser_object instanceof browser_object_class))
293         yield co_return(browser_object);
295     var prompt = I.command.prompt;
296     if (! prompt) {
297         prompt = I.command.name.split(/-|_/).join(" ");
298         prompt = prompt[0].toUpperCase() + prompt.substring(1);
299     }
300     if (I.target != null)
301         prompt += TARGET_PROMPTS[I.target];
302     if (browser_object.hint)
303         prompt += " (" + browser_object.hint + ")";
304     prompt += ":";
306     result = yield browser_object.handler.call(null, I, prompt);
307     yield co_return(result);
312  * This is a simple wrapper function that sets focus to elem, and
313  * bypasses the automatic focus prevention system, which might
314  * otherwise prevent this from happening.
315  */
316 function browser_set_element_focus (buffer, elem, prevent_scroll) {
317     if (!element_dom_node_or_window_p(elem))
318         return;
319     if (prevent_scroll)
320         set_focus_no_scroll(buffer.window, elem);
321     else
322         elem.focus();
325 function browser_element_focus (buffer, elem) {
326     if (!element_dom_node_or_window_p(elem))
327         return;
329     if (elem instanceof Ci.nsIDOMXULTextBoxElement)
330         elem = elem.wrappedJSObject.inputField; // focus the input field
332     browser_set_element_focus(buffer, elem);
333     if (elem instanceof Ci.nsIDOMWindow)
334         return;
336     // If it is not a window, it must be an HTML element
337     var x = 0;
338     var y = 0;
339     if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
340         elem instanceof Ci.nsIDOMHTMLIFrameElement)
341     {
342         elem.contentWindow.focus();
343         return;
344     }
345     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
346         var coords = elem.getAttribute("coords").split(",");
347         x = Number(coords[0]);
348         y = Number(coords[1]);
349     }
351     var doc = elem.ownerDocument;
352     var evt = doc.createEvent("MouseEvents");
354     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
355     elem.dispatchEvent(evt);
358 function browser_object_follow (buffer, target, elem) {
359     // XXX: would be better to let nsILocalFile objects be load_specs
360     if (elem instanceof Ci.nsILocalFile)
361         elem = elem.path;
363     var e;
364     if (elem instanceof load_spec)
365         e = load_spec_element(elem);
366     if (! e)
367         e = elem;
369     browser_set_element_focus(buffer, e, true /* no scroll */);
371     var no_click = (((elem instanceof load_spec) &&
372                      load_spec_forced_charset(elem)) ||
373                     (e instanceof load_spec) ||
374                     (e instanceof Ci.nsIDOMWindow) ||
375                     (e instanceof Ci.nsIDOMHTMLFrameElement) ||
376                     (e instanceof Ci.nsIDOMHTMLIFrameElement) ||
377                     (e instanceof Ci.nsIDOMHTMLLinkElement) ||
378                     (e instanceof Ci.nsIDOMHTMLImageElement &&
379                      !e.hasAttribute("onmousedown") && !e.hasAttribute("onclick")));
381     if (target == FOLLOW_DEFAULT && !no_click) {
382         var x = 1, y = 1;
383         if (e instanceof Ci.nsIDOMHTMLAreaElement) {
384             var coords = e.getAttribute("coords").split(",");
385             if (coords.length >= 2) {
386                 x = Number(coords[0]) + 1;
387                 y = Number(coords[1]) + 1;
388             }
389         }
390         dom_node_click(e, x, y);
391         return;
392     }
394     var spec = load_spec(elem);
396     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
397         // it is nonsensical to follow a javascript url in a different
398         // buffer or window
399         target = FOLLOW_DEFAULT;
400     } else if (!(buffer instanceof content_buffer) &&
401         (target == FOLLOW_CURRENT_FRAME ||
402          target == FOLLOW_DEFAULT ||
403          target == OPEN_CURRENT_BUFFER))
404     {
405         target = OPEN_NEW_BUFFER;
406     }
408     switch (target) {
409     case FOLLOW_CURRENT_FRAME:
410         var current_frame = load_spec_source_frame(spec);
411         if (current_frame && current_frame != buffer.top_frame) {
412             var target_obj = get_web_navigation_for_frame(current_frame);
413             apply_load_spec(target_obj, spec);
414             break;
415         }
416     case FOLLOW_DEFAULT:
417     case OPEN_CURRENT_BUFFER:
418         buffer.load(spec);
419         break;
420     case OPEN_NEW_WINDOW:
421     case OPEN_NEW_BUFFER:
422     case OPEN_NEW_BUFFER_BACKGROUND:
423         create_buffer(buffer.window,
424                       buffer_creator(content_buffer,
425                                      $opener = buffer,
426                                      $load = spec),
427                       target);
428     }
432  * Follow a link-like element by generating fake mouse events.
433  */
434 function dom_node_click (elem, x, y) {
435     var doc = elem.ownerDocument;
436     var view = doc.defaultView;
438     var evt = doc.createEvent("MouseEvents");
439     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
440                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
441     elem.dispatchEvent(evt);
443     evt = doc.createEvent("MouseEvents");
444     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
445                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
446     elem.dispatchEvent(evt);
450 function follow (I, target) {
451     if (target == null)
452         target = FOLLOW_DEFAULT;
453     I.target = target;
454     if (target == OPEN_CURRENT_BUFFER)
455         check_buffer(I.buffer, content_buffer);
456     var element = yield read_browser_object(I);
457     try {
458         element = load_spec(element);
459         if (I.forced_charset)
460             element.forced_charset = I.forced_charset;
461     } catch (e) {}
462     browser_object_follow(I.buffer, target, element);
465 function follow_new_buffer (I) {
466     yield follow(I, OPEN_NEW_BUFFER);
469 function follow_new_buffer_background (I) {
470     yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
473 function follow_new_window (I) {
474     yield follow(I, OPEN_NEW_WINDOW);
477 function follow_current_frame (I) {
478     yield follow(I, FOLLOW_CURRENT_FRAME);
481 function follow_current_buffer (I) {
482     yield follow(I, OPEN_CURRENT_BUFFER);
486 function element_get_load_target_label (element) {
487     if (element instanceof Ci.nsIDOMWindow)
488         return "page";
489     if (element instanceof Ci.nsIDOMHTMLFrameElement)
490         return "frame";
491     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
492         return "iframe";
493     return null;
496 function element_get_operation_label (element, op_name, suffix) {
497     var target_label = element_get_load_target_label(element);
498     if (target_label != null)
499         target_label = " " + target_label;
500     else
501         target_label = "";
503     if (suffix != null)
504         suffix = " " + suffix;
505     else
506         suffix = "";
508     return op_name + target_label + suffix + ":";
512 function browser_element_copy (buffer, elem) {
513     var spec;
514     try {
515        spec = load_spec(elem);
516     } catch (e) {}
517     var text = null;
518     if (typeof elem == "string" || elem instanceof String)
519         text = elem;
520     else if (spec)
521         text = load_spec_uri_string(spec);
522     else  {
523         if (!(elem instanceof Ci.nsIDOMNode))
524             throw interactive_error("Element has no associated text to copy.");
525         switch (elem.localName) {
526         case "INPUT":
527         case "TEXTAREA":
528             text = elem.value;
529             break;
530         case "SELECT":
531             if (elem.selectedIndex >= 0)
532                 text = elem.item(elem.selectedIndex).text;
533             break;
534         default:
535             text = elem.textContent;
536             break;
537         }
538     }
539     browser_set_element_focus(buffer, elem);
540     writeToClipboard(text);
541     buffer.window.minibuffer.message("Copied: " + text);
545 define_variable("view_source_use_external_editor", false,
546     "When true, the `view-source' command will send its document to "+
547     "your external editor.");
549 define_variable("view_source_function", null,
550     "May be set to a user-defined function for viewing source code. "+
551     "The function should accept an nsILocalFile of the filename as "+
552     "its one positional argument, and it will also be called with "+
553     "the keyword `$temporary', whose value will be true if the file "+
554     "is considered temporary, and therefore the function must take "+
555     "responsibility for deleting it.");
557 function browser_object_view_source (buffer, target, elem) {
558     if (view_source_use_external_editor || view_source_function) {
559         var spec = load_spec(elem);
561         let [file, temp] = yield download_as_temporary(spec,
562                                                        $buffer = buffer,
563                                                        $action = "View source");
564         if (view_source_use_external_editor)
565             yield open_file_with_external_editor(file, $temporary = temp);
566         else
567             yield view_source_function(file, $temporary = temp);
568         return;
569     }
571     var win = null;
572     var window = buffer.window;
573     if (elem.localName) {
574         switch (elem.localName.toLowerCase()) {
575         case "frame": case "iframe":
576             win = elem.contentWindow;
577             break;
578         case "math":
579             view_mathml_source(window, charset, elem);
580             return;
581         default:
582             throw new Error("Invalid browser element");
583         }
584     } else
585         win = elem;
586     win.focus();
588     var url_s = win.location.href;
589     if (url_s.substring (0,12) != "view-source:") {
590         try {
591             browser_object_follow(buffer, target, "view-source:" + url_s);
592         } catch(e) { dump_error(e); }
593     } else {
594         window.minibuffer.message ("Already viewing source");
595     }
598 function view_source (I, target) {
599     I.target = target;
600     if (target == null)
601         target = OPEN_CURRENT_BUFFER;
602     var element = yield read_browser_object(I);
603     yield browser_object_view_source(I.buffer, target, element);
606 function view_source_new_buffer (I) {
607     yield view_source(I, OPEN_NEW_BUFFER);
610 function view_source_new_window (I) {
611     yield view_source(I, OPEN_NEW_WINDOW);
615 function browser_element_shell_command (buffer, elem, command, cwd) {
616     var spec = load_spec(elem);
617     yield download_as_temporary(spec,
618                                 $buffer = buffer,
619                                 $shell_command = command,
620                                 $shell_command_cwd = cwd);