for loop efficiency
[conkeror.git] / modules / element.js
blob191cae10652f18802adf21fece76805141cccbb5
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             for each (let x in doc.getElementsByTagName("iframe")) {
107                 let style = topwin.getComputedStyle(x, "");
108                 if (style.display == "none" || style.visibility == "hidden")
109                     continue;
110                 skip_hints = false;
111                 break;
112             }
113         }
114         if (skip_hints) {
115             // only one frame (the top-level one), no need to use the hints system
116             yield co_return(I.buffer.top_frame);
117         }
118         var result = yield I.buffer.window.minibuffer.read_hinted_element(
119             $buffer = I.buffer,
120             $prompt = prompt,
121             $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
122         yield co_return(result);
123     },
124     $hint = "select frame");
126 define_browser_object_class("links",
127     "Browser object class for selecting a hyperlink, form field, "+
128     "or link-like element, via hinting.",
129     xpath_browser_object_handler(
130         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
131         "@role='link'] | " +
132         "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
133         "//*[@contenteditable = 'true']"),
134     $hint = "select link");
136 define_browser_object_class("mathml",
137     "Browser object class for selecting a MathML node via hinting.",
138     xpath_browser_object_handler("//m:math"),
139     $hint = "select MathML element");
141 define_browser_object_class("top",
142     "Browser object class which returns the top frame of the document.",
143     function (I, prompt) { return I.buffer.top_frame; });
145 define_browser_object_class("url",
146     "Browser object class which prompts the user for an url or webjump.",
147     function (I, prompt) {
148         var result = yield I.buffer.window.minibuffer.read_url($prompt = prompt);
149         yield co_return(result);
150     },
151     $hint = "enter URL/webjump");
153 define_browser_object_class("paste-url",
154     "Browser object which reads an url from the X Primary Selection, "+
155     "falling back on the clipboard for operating systems which lack one.",
156     function (I, prompt) {
157                 var url = read_from_x_primary_selection();
158                 // trim spaces
159                 url = url.replace(/^\s*|\s*$/,"");
160                 // add http:// if needed
161                 if (url.match(/^[^:]+\./)) {
162                         url = "http://" + url;
163                 }
164         try {
165             return make_uri(url).spec;
166         } catch (e) {
167             throw new interactive_error("error: malformed url: "+url);
168         }
169     });
171 define_browser_object_class("file",
172     "Browser object which prompts for a file name.",
173     function (I, prompt) {
174         var result = yield I.buffer.window.minibuffer.read_file(
175             $prompt = prompt,
176             $history = I.command.name+"/file",
177             $initial_value = I.local.cwd.path);
178         yield co_return(result);
179     },
180     $hint = "enter file name");
182 define_browser_object_class("alt",
183     "Browser object class which returns the alt text of an html:img, "+
184     "selected via hinting",
185     function (I, prompt) {
186         var result = yield I.buffer.window.minibuffer.read_hinted_element(
187             $buffer = I.buffer,
188             $prompt = prompt,
189             $hint_xpath_expression = "//img[@alt]");
190         yield co_return(result.alt);
191     },
192     $hint = "select image for alt-text");
194 define_browser_object_class("title",
195     "Browser object class which returns the title attribute of an element, "+
196     "selected via hinting",
197     function (I, prompt) {
198         var result = yield I.buffer.window.minibuffer.read_hinted_element(
199             $buffer = I.buffer,
200             $prompt = prompt,
201             $hint_xpath_expression = "//*[@title]");
202         yield co_return(result.title);
203     },
204     $hint = "select element for title attribute");
206 define_browser_object_class("title-or-alt",
207     "Browser object which is the union of browser-object-alt and "+
208     "browser-object-title, with title having higher precedence in "+
209     "the case of an element that has both.",
210     function (I, prompt) {
211         var result = yield I.buffer.window.minibuffer.read_hinted_element(
212             $buffer = I.buffer,
213             $prompt = prompt,
214             $hint_xpath_expression = "//img[@alt] | //*[@title]");
215         yield co_return(result.title ? result.title : result.alt);
216     },
217     $hint = "select element for title or alt-text");
219 define_browser_object_class("scrape-url",
220     "Browser object which lets the user choose an url from a list of "+
221     "urls scraped from the source code of the document.",
222     function (I, prompt) {
223         var completions = I.buffer.document.documentElement.innerHTML
224             .match(/http:[^\s>"]*/g)
225             .filter(remove_duplicates_filter());
226         var completer = prefix_completer($completions = completions);
227         var result = yield I.buffer.window.minibuffer.read(
228             $prompt = prompt,
229             $completer = completer,
230             $initial_value = null,
231             $auto_complete = "url",
232             $select,
233             $match_required = false);
234         yield co_return(result);
235     },
236     $hint = "choose scraped URL");
238 define_browser_object_class("up-url",
239     "Browser object which returns the url one level above the current one.",
240     function (I, prompt) {
241         var up = compute_url_up_path(I.buffer.current_uri.spec);
242         return I.buffer.current_uri.resolve(up);
243     });
245 define_browser_object_class("focused-element",
246     "Browser object which returns the focused element.",
247     function (I, prompt) { return I.buffer.focused_element; });
249 define_browser_object_class("dom-node", null,
250     xpath_browser_object_handler("//*"),
251     $hint = "select DOM node");
253 function read_browser_object (I) {
254     var browser_object = I.browser_object;
255     // literals cannot be overridden
256     if (browser_object instanceof Function)
257         yield co_return(browser_object(I));
258     if (! (browser_object instanceof browser_object_class))
259         yield co_return(browser_object);
261     var prompt = I.command.prompt;
262     if (! prompt) {
263         prompt = I.command.name.split(/-|_/).join(" ");
264         prompt = prompt[0].toUpperCase() + prompt.substring(1);
265     }
266     if (I.target != null)
267         prompt += TARGET_PROMPTS[I.target];
268     if (browser_object.hint)
269         prompt += " (" + browser_object.hint + ")";
270     prompt += ":";
272     var result = yield browser_object.handler.call(null, I, prompt);
273     yield co_return(result);
278  * This is a simple wrapper function that sets focus to elem, and
279  * bypasses the automatic focus prevention system, which might
280  * otherwise prevent this from happening.
281  */
282 function browser_set_element_focus (buffer, elem, prevent_scroll) {
283     if (!element_dom_node_or_window_p(elem))
284         return;
285     if (prevent_scroll)
286         set_focus_no_scroll(buffer.window, elem);
287     else
288         elem.focus();
291 function browser_element_focus (buffer, elem) {
292     if (!element_dom_node_or_window_p(elem))
293         return;
295     if (elem instanceof Ci.nsIDOMXULTextBoxElement)
296         elem = elem.wrappedJSObject.inputField; // focus the input field
298     browser_set_element_focus(buffer, elem);
299     if (elem instanceof Ci.nsIDOMWindow)
300         return;
302     // If it is not a window, it must be an HTML element
303     var x = 0;
304     var y = 0;
305     if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
306         elem instanceof Ci.nsIDOMHTMLIFrameElement)
307     {
308         elem.contentWindow.focus();
309         return;
310     }
311     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
312         var coords = elem.getAttribute("coords").split(",");
313         x = Number(coords[0]);
314         y = Number(coords[1]);
315     }
317     var doc = elem.ownerDocument;
318     var evt = doc.createEvent("MouseEvents");
320     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
321     elem.dispatchEvent(evt);
324 function browser_object_follow (buffer, target, elem) {
325     // XXX: would be better to let nsILocalFile objects be load_specs
326     if (elem instanceof Ci.nsILocalFile)
327         elem = elem.path;
329     var e;
330     if (elem instanceof load_spec)
331         e = load_spec_element(elem);
332     if (! e)
333         e = elem;
335     browser_set_element_focus(buffer, e, true /* no scroll */);
337     var no_click = (((elem instanceof load_spec) &&
338                      load_spec_forced_charset(elem)) ||
339                     (e instanceof load_spec) ||
340                     (e instanceof Ci.nsIDOMWindow) ||
341                     (e instanceof Ci.nsIDOMHTMLFrameElement) ||
342                     (e instanceof Ci.nsIDOMHTMLIFrameElement) ||
343                     (e instanceof Ci.nsIDOMHTMLLinkElement) ||
344                     (e instanceof Ci.nsIDOMHTMLImageElement &&
345                      !e.hasAttribute("onmousedown") && !e.hasAttribute("onclick")));
347     if (target == FOLLOW_DEFAULT && !no_click) {
348         var x = 1, y = 1;
349         if (e instanceof Ci.nsIDOMHTMLAreaElement) {
350             var coords = e.getAttribute("coords").split(",");
351             if (coords.length >= 2) {
352                 x = Number(coords[0]) + 1;
353                 y = Number(coords[1]) + 1;
354             }
355         }
356         dom_node_click(e, x, y);
357         return;
358     }
360     var spec = load_spec(elem);
362     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
363         // it is nonsensical to follow a javascript url in a different
364         // buffer or window
365         target = FOLLOW_DEFAULT;
366     } else if (!(buffer instanceof content_buffer) &&
367         (target == FOLLOW_CURRENT_FRAME ||
368          target == FOLLOW_DEFAULT ||
369          target == OPEN_CURRENT_BUFFER))
370     {
371         target = OPEN_NEW_BUFFER;
372     }
374     switch (target) {
375     case FOLLOW_CURRENT_FRAME:
376         var current_frame = load_spec_source_frame(spec);
377         if (current_frame && current_frame != buffer.top_frame) {
378             var target_obj = get_web_navigation_for_frame(current_frame);
379             apply_load_spec(target_obj, spec);
380             break;
381         }
382     case FOLLOW_DEFAULT:
383     case OPEN_CURRENT_BUFFER:
384         buffer.load(spec);
385         break;
386     case OPEN_NEW_WINDOW:
387     case OPEN_NEW_BUFFER:
388     case OPEN_NEW_BUFFER_BACKGROUND:
389         create_buffer(buffer.window,
390                       buffer_creator(content_buffer,
391                                      $opener = buffer,
392                                      $load = spec),
393                       target);
394     }
398  * Follow a link-like element by generating fake mouse events.
399  */
400 function dom_node_click (elem, x, y) {
401     var doc = elem.ownerDocument;
402     var view = doc.defaultView;
404     var evt = doc.createEvent("MouseEvents");
405     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
406                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
407     elem.dispatchEvent(evt);
409     evt = doc.createEvent("MouseEvents");
410     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
411                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
412     elem.dispatchEvent(evt);
416 function follow (I, target) {
417     if (target == null)
418         target = FOLLOW_DEFAULT;
419     I.target = target;
420     if (target == OPEN_CURRENT_BUFFER)
421         check_buffer(I.buffer, content_buffer);
422     var element = yield read_browser_object(I);
423     try {
424         element = load_spec(element);
425         if (I.forced_charset)
426             element.forced_charset = I.forced_charset;
427     } catch (e) {}
428     browser_object_follow(I.buffer, target, element);
431 function follow_new_buffer (I) {
432     yield follow(I, OPEN_NEW_BUFFER);
435 function follow_new_buffer_background (I) {
436     yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
439 function follow_new_window (I) {
440     yield follow(I, OPEN_NEW_WINDOW);
443 function follow_current_frame (I) {
444     yield follow(I, FOLLOW_CURRENT_FRAME);
447 function follow_current_buffer (I) {
448     yield follow(I, OPEN_CURRENT_BUFFER);
452 function element_get_load_target_label (element) {
453     if (element instanceof Ci.nsIDOMWindow)
454         return "page";
455     if (element instanceof Ci.nsIDOMHTMLFrameElement)
456         return "frame";
457     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
458         return "iframe";
459     return null;
462 function element_get_operation_label (element, op_name, suffix) {
463     var target_label = element_get_load_target_label(element);
464     if (target_label != null)
465         target_label = " " + target_label;
466     else
467         target_label = "";
469     if (suffix != null)
470         suffix = " " + suffix;
471     else
472         suffix = "";
474     return op_name + target_label + suffix + ":";
478 function browser_element_copy (buffer, elem) {
479     var spec;
480     try {
481        spec = load_spec(elem);
482     } catch (e) {}
483     var text = null;
484     if (spec)
485         text = load_spec_uri_string(spec);
486     else  {
487         if (!(elem instanceof Ci.nsIDOMNode))
488             throw interactive_error("Element has no associated text to copy.");
489         switch (elem.localName) {
490         case "INPUT":
491         case "TEXTAREA":
492             text = elem.value;
493             break;
494         case "SELECT":
495             if (elem.selectedIndex >= 0)
496                 text = elem.item(elem.selectedIndex).text;
497             break;
498         default:
499             text = elem.textContent;
500             break;
501         }
502     }
503     browser_set_element_focus(buffer, elem);
504     writeToClipboard(text);
505     buffer.window.minibuffer.message("Copied: " + text);
509 define_variable("view_source_use_external_editor", false,
510     "When true, the `view-source' command will send its document to "+
511     "your external editor.");
513 define_variable("view_source_function", null,
514     "May be set to a user-defined function for viewing source code. "+
515     "The function should accept an nsILocalFile of the filename as "+
516     "its one positional argument, and it will also be called with "+
517     "the keyword `$temporary', whose value will be true if the file "+
518     "is considered temporary, and therefore the function must take "+
519     "responsibility for deleting it.");
521 function browser_object_view_source (buffer, target, elem) {
522     if (view_source_use_external_editor || view_source_function) {
523         var spec = load_spec(elem);
525         let [file, temp] = yield download_as_temporary(spec,
526                                                        $buffer = buffer,
527                                                        $action = "View source");
528         if (view_source_use_external_editor)
529             yield open_file_with_external_editor(file, $temporary = temp);
530         else
531             yield view_source_function(file, $temporary = temp);
532         return;
533     }
535     var win = null;
536     var window = buffer.window;
537     if (elem.localName) {
538         switch (elem.localName.toLowerCase()) {
539         case "frame": case "iframe":
540             win = elem.contentWindow;
541             break;
542         case "math":
543             view_mathml_source(window, charset, elem);
544             return;
545         default:
546             throw new Error("Invalid browser element");
547         }
548     } else
549         win = elem;
550     win.focus();
552     var url_s = win.location.href;
553     if (url_s.substring (0,12) != "view-source:") {
554         try {
555             browser_object_follow(buffer, target, "view-source:" + url_s);
556         } catch(e) { dump_error(e); }
557     } else {
558         window.minibuffer.message ("Already viewing source");
559     }
562 function view_source (I, target) {
563     I.target = target;
564     if (target == null)
565         target = OPEN_CURRENT_BUFFER;
566     var element = yield read_browser_object(I);
567     yield browser_object_view_source(I.buffer, target, element);
570 function view_source_new_buffer (I) {
571     yield view_source(I, OPEN_NEW_BUFFER);
574 function view_source_new_window (I) {
575     yield view_source(I, OPEN_NEW_WINDOW);
579 function browser_element_shell_command (buffer, elem, command, cwd) {
580     var spec = load_spec(elem);
581     yield download_as_temporary(spec,
582                                 $buffer = buffer,
583                                 $shell_command = command,
584                                 $shell_command_cwd = cwd);