browser_object_frames: auto-select top frame if there are no visible frames
[conkeror.git] / modules / element.js
bloba725203a6580105d750336c3053d66e76a47fdd1
1 /**
2  * (C) Copyright 2007-2008 John J. Foerch
3  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
4  *
5  * Portions of this file are derived from Vimperator,
6  * (C) Copyright 2006-2007 Martin Stubenschrott.
7  *
8  * Use, modification, and distribution are subject to the terms specified in the
9  * COPYING file.
10 **/
12 require("hints.js");
13 require("save.js");
14 require("mime-type-override.js");
15 require("minibuffer-read-mime-type.js");
17 var browser_object_classes = {};
19 /**
20  * handler is a coroutine called as: handler(buffer, prompt)
21  */
22 function browser_object_class (name, label, doc, handler) {
23     this.name = name;
24     this.handler = handler;
25     if (doc) this.doc = doc;
26     if (label) this.label = label;
29 function define_browser_object_class (name, label, doc, handler) {
30     var varname = 'browser_object_'+name.replace('-','_','g');
31     var ob = conkeror[varname] =
32         new browser_object_class(name, label, doc, handler);
33     interactive(
34         "browser-object-"+name,
35         "A prefix command to specify that the following command operate "+
36             "on objects of type: "+name+".",
37         function (I) { I.browser_object = ob; },
38         $prefix = true);
39     return ob;
42 function xpath_browser_object_handler (xpath_expression) {
43     return function (I, prompt) {
44         var result = yield I.buffer.window.minibuffer.read_hinted_element(
45             $buffer = I.buffer,
46             $prompt = prompt,
47             $hint_xpath_expression = xpath_expression);
48         yield co_return(result);
49     };
52 define_browser_object_class(
53     "images", "image", null,
54     xpath_browser_object_handler("//img | //xhtml:img"));
56 define_browser_object_class(
57     "frames","frame", null,
58     function (I, prompt) {
59         var doc = I.buffer.document;
60         // Check for any frames or visible iframes
61         var skip_hints = true;
62         if (doc.getElementsByTagName("frame").length > 0)
63             skip_hints = false;
64         else {
65             let topwin = I.buffer.top_frame;
66             for each (let x in doc.getElementsByTagName("iframe"))
67             {
68                 let style = topwin.getComputedStyle(x, "");
69                 if (style.display == "none" || style.visibility == "hidden")
70                     continue;
71                 skip_hints = false;
72                 break;
73             }
74         }
75         if (skip_hints) {
76             // only one frame (the top-level one), no need to use the hints system
77             yield co_return(I.buffer.top_frame);
78         }
79         var result = yield I.buffer.window.minibuffer.read_hinted_element(
80             $buffer = I.buffer,
81             $prompt = prompt,
82             $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
83         yield co_return(result);
84     });
86 define_browser_object_class(
87     "links", "link", null,
88     xpath_browser_object_handler (
89         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
90         "@role='link'] | " +
91         "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
92         "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand] | " +
93         "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
94         "//xhtml:button | //xhtml:select"));
96 define_browser_object_class(
97     "mathml", "MathML element", null,
98     xpath_browser_object_handler("//m:math"));
100 define_browser_object_class(
101     "top", null, null,
102     function (I, prompt) { yield co_return(I.buffer.top_frame); });
104 define_browser_object_class(
105     "url", null, null,
106     function (I, prompt) {
107         var result = yield I.buffer.window.minibuffer.read_url($prompt = prompt);
108         yield co_return(result);
109     });
111 define_browser_object_class(
112     "pasteurl", null, null,
113     function (I, url) {
114         let url = read_from_x_primary_selection();
115         yield co_return(url);
116     });
118 define_browser_object_class(
119     "file", null, null,
120     function (I, prompt) {
121         var result = yield I.buffer.window.minibuffer.read_file(
122             $prompt = prompt,
123             $history = I.command.name+"/file",
124             $initial_value = I.local.cwd.path);
125         yield co_return(result);
126     });
128 define_browser_object_class(
129     "alt", "Image Alt-text", null,
130     function (I, prompt) {
131         var result = yield I.buffer.window.minibuffer.read_hinted_element(
132             $buffer = I.buffer,
133             $prompt = prompt,
134             $hint_xpath_expression = "//img[@alt]");
135         yield co_return(result.alt);
136     });
138 define_browser_object_class(
139     "title", "Element Title", null,
140     function (I, prompt) {
141         var result = yield I.buffer.window.minibuffer.read_hinted_element(
142             $buffer = I.buffer,
143             $prompt = prompt,
144             $hint_xpath_expression = "//*[@title]");
145         yield co_return(result.title);
146     });
148 define_browser_object_class(
149     "title-or-alt", "Element Title or Alt-text", null,
150     function (I, prompt) {
151         var result = yield I.buffer.window.minibuffer.read_hinted_element(
152             $buffer = I.buffer,
153             $prompt = prompt,
154             $hint_xpath_expression = "//img[@alt] | //*[@title]");
155         yield co_return(result.title ? result.title : result.alt);
156     });
158 define_browser_object_class(
159     "scrape-url", "url",
160     "Scrapes urls from the source code of the top-level document of buffer.",
161     function (I, prompt) {
162         var completions = I.buffer.document.documentElement.innerHTML
163             .match(/http:[^\s>"]*/g)
164             .filter(remove_duplicates_filter());
165         var completer = prefix_completer($completions = completions);
166         var result = yield I.buffer.window.minibuffer.read(
167             $prompt = prompt,
168             $completer = completer,
169             $initial_value = null,
170             $auto_complete = "url",
171             $select,
172             $match_required = false);
173         yield co_return(result);
174     });
176 define_browser_object_class(
177     "up-url", "Up Url", null,
178     function (I, prompt) {
179         var up = compute_url_up_path(I.buffer.current_URI.spec);
180         return I.buffer.current_URI.resolve(up);
181     });
183 define_browser_object_class(
184     "focused-element", "Focused element", null,
185     function (I, prompt) {
186         return I.buffer.focused_element;
187     });
189 function read_browser_object (I) {
190     var browser_object = I.browser_object;
191     // literals cannot be overridden
192     if (browser_object instanceof Function)
193         yield co_return(browser_object());
194     if (! (browser_object instanceof browser_object_class))
195         yield co_return(browser_object);
197     var prompt = I.command.prompt;
198     if (! prompt) {
199         prompt = I.command.name.split(/-|_/).join(" ");
200         prompt = prompt[0].toUpperCase() + prompt.substring(1);
201     }
202     if (I.target != null)
203         prompt += TARGET_PROMPTS[I.target];
204     if (browser_object.label)
205         prompt += " (select " + browser_object.label + ")";
206     prompt += ":";
208     var result = yield browser_object.handler.call(null, I, prompt);
209     yield co_return(result);
214  * This is a simple wrapper function that sets focus to elem, and
215  * bypasses the automatic focus prevention system, which might
216  * otherwise prevent this from happening.
217  */
218 function browser_set_element_focus (buffer, elem, prevent_scroll) {
219     if (!element_dom_node_or_window_p(elem))
220         return;
222     buffer.last_user_input_received = Date.now();
223     if (prevent_scroll)
224         set_focus_no_scroll(buffer.window, elem);
225     else
226         elem.focus();
229 function browser_element_focus (buffer, elem) {
230     if (!element_dom_node_or_window_p(elem))
231         return;
233     if (elem instanceof Ci.nsIDOMXULTextBoxElement)
234         elem = elem.wrappedJSObject.inputField; // focus the input field
236     browser_set_element_focus(buffer, elem);
237     if (elem instanceof Ci.nsIDOMWindow)
238         return;
240     // If it is not a window, it must be an HTML element
241     var x = 0;
242     var y = 0;
243     if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
244         elem instanceof Ci.nsIDOMHTMLIFrameElement)
245     {
246         elem.contentWindow.focus();
247         return;
248     }
249     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
250         var coords = elem.getAttribute("coords").split(",");
251         x = Number(coords[0]);
252         y = Number(coords[1]);
253     }
255     var doc = elem.ownerDocument;
256     var evt = doc.createEvent("MouseEvents");
258     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
259     elem.dispatchEvent(evt);
262 function browser_object_follow (buffer, target, elem) {
263     // XXX: would be better to let nsILocalFile objects be load_specs
264     if (elem instanceof Ci.nsILocalFile)
265         elem = elem.path;
267     var e;
268     if (elem instanceof load_spec)
269         e = load_spec_element(elem);
270     if (! e)
271         e = elem;
273     browser_set_element_focus(buffer, e, true /* no scroll */);
275     var no_click = ((e instanceof load_spec) ||
276                     (e instanceof Ci.nsIDOMWindow) ||
277                     (e instanceof Ci.nsIDOMHTMLFrameElement) ||
278                     (e instanceof Ci.nsIDOMHTMLIFrameElement) ||
279                     (e instanceof Ci.nsIDOMHTMLLinkElement) ||
280                     (e instanceof Ci.nsIDOMHTMLImageElement &&
281                      !e.hasAttribute("onmousedown") && !e.hasAttribute("onclick")));
283     if (target == FOLLOW_DEFAULT && !no_click) {
284         var x = 1, y = 1;
285         if (e instanceof Ci.nsIDOMHTMLAreaElement) {
286             var coords = e.getAttribute("coords").split(",");
287             if (coords.length >= 2) {
288                 x = Number(coords[0]) + 1;
289                 y = Number(coords[1]) + 1;
290             }
291         }
292         dom_node_click(e, x, y);
293         return;
294     }
296     var spec = load_spec(elem);
298     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
299         // it is nonsensical to follow a javascript url in a different
300         // buffer or window
301         target = FOLLOW_DEFAULT;
302     } else if (!(buffer instanceof content_buffer) &&
303         (target == FOLLOW_CURRENT_FRAME ||
304          target == FOLLOW_DEFAULT ||
305          target == OPEN_CURRENT_BUFFER))
306     {
307         target = OPEN_NEW_BUFFER;
308     }
310     switch (target) {
311     case FOLLOW_CURRENT_FRAME:
312         var current_frame = load_spec_source_frame(spec);
313         if (current_frame && current_frame != buffer.top_frame) {
314             var target_obj = get_web_navigation_for_frame(current_frame);
315             apply_load_spec(target_obj, spec);
316             break;
317         }
318     case FOLLOW_DEFAULT:
319     case OPEN_CURRENT_BUFFER:
320         buffer.load(spec);
321         break;
322     case OPEN_NEW_WINDOW:
323     case OPEN_NEW_BUFFER:
324     case OPEN_NEW_BUFFER_BACKGROUND:
325         create_buffer(buffer.window,
326                       buffer_creator(content_buffer,
327                                      $opener = buffer,
328                                      $load = spec),
329                       target);
330     }
334  * Follow a link-like element by generating fake mouse events.
335  */
336 function dom_node_click (elem, x, y) {
337     var doc = elem.ownerDocument;
338     var view = doc.defaultView;
340     var evt = doc.createEvent("MouseEvents");
341     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
342                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
343     elem.dispatchEvent(evt);
345     evt = doc.createEvent("MouseEvents");
346     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
347                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
348     elem.dispatchEvent(evt);
352 function follow (I, target) {
353     if (target == null)
354         target = FOLLOW_DEFAULT;
355     I.target = target;
356     if (target == OPEN_CURRENT_BUFFER)
357         check_buffer(I.buffer, content_buffer);
358     var element = yield read_browser_object(I);
359     try {
360         element = load_spec(element);
361         if (I.forced_charset)
362             element.forced_charset = I.forced_charset;
363     } catch (e) {}
364     browser_object_follow(I.buffer, target, element);
367 function follow_new_buffer (I) {
368     yield follow(I, OPEN_NEW_BUFFER);
371 function follow_new_buffer_background (I) {
372     yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
375 function follow_new_window (I) {
376     yield follow(I, OPEN_NEW_WINDOW);
379 function follow_current_frame (I) {
380     yield follow(I, FOLLOW_CURRENT_FRAME);
383 function follow_current_buffer (I) {
384     yield follow(I, OPEN_CURRENT_BUFFER);
388 function element_get_load_target_label (element) {
389     if (element instanceof Ci.nsIDOMWindow)
390         return "page";
391     if (element instanceof Ci.nsIDOMHTMLFrameElement)
392         return "frame";
393     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
394         return "iframe";
395     return null;
398 function element_get_operation_label (element, op_name, suffix) {
399     var target_label = element_get_load_target_label(element);
400     if (target_label != null)
401         target_label = " " + target_label;
402     else
403         target_label = "";
405     if (suffix != null)
406         suffix = " " + suffix;
407     else
408         suffix = "";
410     return op_name + target_label + suffix + ":";
414 function browser_element_copy (buffer, elem) {
415     var spec;
416     try {
417        spec = load_spec(elem);
418     } catch (e) {}
419     var text = null;
420     if (spec)
421         text = load_spec_uri_string(spec);
422     else  {
423         if (!(elem instanceof Ci.nsIDOMNode))
424             throw interactive_error("Element has no associated text to copy.");
425         switch (elem.localName) {
426         case "INPUT":
427         case "TEXTAREA":
428             text = elem.value;
429             break;
430         case "SELECT":
431             if (elem.selectedIndex >= 0)
432                 text = elem.item(elem.selectedIndex).text;
433             break;
434         default:
435             text = elem.textContent;
436             break;
437         }
438     }
439     browser_set_element_focus(buffer, elem);
440     writeToClipboard(text);
441     buffer.window.minibuffer.message("Copied: " + text);
445 define_variable("view_source_use_external_editor", false,
446     "When true, the `view-source' command will send its document to "+
447     "your external editor.");
449 define_variable("view_source_function", null,
450     "May be set to a user-defined function for viewing source code. "+
451     "The function should accept an nsILocalFile of the filename as "+
452     "its one positional argument, and it will also be called with "+
453     "the keyword `$temporary', whose value will be true if the file "+
454     "is considered temporary, and therefore the function must take "+
455     "responsibility for deleting it.");
457 function browser_object_view_source (buffer, target, elem) {
458     if (view_source_use_external_editor || view_source_function) {
459         var spec = load_spec(elem);
461         let [file, temp] = yield download_as_temporary(spec,
462                                                        $buffer = buffer,
463                                                        $action = "View source");
464         if (view_source_use_external_editor)
465             yield open_file_with_external_editor(file, $temporary = temp);
466         else
467             yield view_source_function(file, $temporary = temp);
468         return;
469     }
471     var win = null;
472     var window = buffer.window;
473     if (elem.localName) {
474         switch (elem.localName.toLowerCase()) {
475         case "frame": case "iframe":
476             win = elem.contentWindow;
477             break;
478         case "math":
479             view_mathml_source(window, charset, elem);
480             return;
481         default:
482             throw new Error("Invalid browser element");
483         }
484     } else
485         win = elem;
486     win.focus();
488     var url_s = win.location.href;
489     if (url_s.substring (0,12) != "view-source:") {
490         try {
491             browser_object_follow(buffer, target, "view-source:" + url_s);
492         } catch(e) { dump_error(e); }
493     } else {
494         window.minibuffer.message ("Already viewing source");
495     }
498 function view_source (I, target) {
499     I.target = target;
500     if (target == null)
501         target = OPEN_CURRENT_BUFFER;
502     var element = yield read_browser_object(I);
503     yield browser_object_view_source(I.buffer, target, element);
506 function view_source_new_buffer (I) {
507     yield view_source(I, OPEN_NEW_BUFFER);
510 function view_source_new_window (I) {
511     yield view_source(I, OPEN_NEW_WINDOW);
515 function browser_element_shell_command (buffer, elem, command, cwd) {
516     var spec = load_spec(elem);
517     yield download_as_temporary(spec,
518                                 $buffer = buffer,
519                                 $shell_command = command,
520                                 $shell_command_cwd = cwd);