gmane page-mode: binding for browser-object-links
[conkeror.git] / modules / element.js
blob57205cde627dc6652a4e92f1163bfa82bc2856ec
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 (ctx) { ctx.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         if (doc.getElementsByTagName("frame").length == 0 &&
61             doc.getElementsByTagName("iframe").length == 0)
62         {
63             // only one frame (the top-level one), no need to use the hints system
64             yield co_return(I.buffer.top_frame);
65         }
66         var result = yield I.buffer.window.minibuffer.read_hinted_element(
67             $buffer = I.buffer,
68             $prompt = prompt,
69             $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
70         yield co_return(result);
71     });
73 define_browser_object_class(
74     "links", "link", null,
75     xpath_browser_object_handler (
76         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
77         "@role='link'] | " +
78         "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
79         "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand] | " +
80         "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
81         "//xhtml:button | //xhtml:select"));
83 define_browser_object_class(
84     "mathml", "MathML element", null,
85     xpath_browser_object_handler ("//m:math"));
87 define_browser_object_class(
88     "top", null, null,
89     function (I, prompt) { yield co_return(I.buffer.top_frame); });
91 define_browser_object_class(
92     "url", null, null,
93     function (I, prompt) {
94         var result = yield I.buffer.window.minibuffer.read_url($prompt = prompt);
95         yield co_return(result);
96     });
98 define_browser_object_class(
99     "pasteurl", null, null,
100     function (I, url) {
101         let url = read_from_x_primary_selection();
102         yield co_return(url);
103     });
105 define_browser_object_class(
106     "file", null, null,
107     function (I, prompt) {
108         var result = yield I.buffer.window.minibuffer.read_file(
109             $prompt = prompt,
110             $history = I.command.name+"/file",
111             $initial_value = I.buffer.cwd.path);
112         yield co_return (result);
113     });
115 define_browser_object_class(
116     "alt", "Image Alt-text", null,
117     function (I, prompt) {
118         var result = yield I.buffer.window.minibuffer.read_hinted_element(
119             $buffer = I.buffer,
120             $prompt = prompt,
121             $hint_xpath_expression = "//img[@alt]");
122         yield (co_return (result.alt));
123     });
125 define_browser_object_class(
126     "title", "Element Title", null,
127     function (I, prompt) {
128         var result = yield I.buffer.window.minibuffer.read_hinted_element(
129             $buffer = I.buffer,
130             $prompt = prompt,
131             $hint_xpath_expression = "//*[@title]");
132         yield (co_return (result.title));
133     });
135 define_browser_object_class(
136     "title-or-alt", "Element Title or Alt-text", null,
137     function (I, prompt) {
138         var result = yield I.buffer.window.minibuffer.read_hinted_element(
139             $buffer = I.buffer,
140             $prompt = prompt,
141             $hint_xpath_expression = "//img[@alt] | //*[@title]");
142         yield (co_return (result.title ? result.title : result.alt));
143     });
145 define_browser_object_class(
146     "scrape-url", "url",
147     "Scrapes urls from the source code of the top-level document of buffer.",
148     function (I, prompt) {
149         var completions = I.buffer.document.documentElement.innerHTML
150             .match(/http:[^\s>"]*/g)
151             .filter(remove_duplicates_filter());
152         var completer = prefix_completer($completions = completions);
153         var result = yield I.buffer.window.minibuffer.read(
154             $prompt = prompt,
155             $completer = completer,
156             $initial_value = null,
157             $auto_complete = "url",
158             $select,
159             $match_required = false);
160         yield co_return(result);
161     });
163 define_browser_object_class(
164     "up-url", "Up Url", null,
165     function (I, prompt) {
166         var up = compute_url_up_path(I.buffer.current_URI.spec);
167         return I.buffer.current_URI.resolve(up);
168     });
170 define_browser_object_class(
171     "focused-element", "Focused element", null,
172     function (I, prompt) {
173         return I.buffer.focused_element;
174     });
176 function read_browser_object (I)
178     var browser_object = I.browser_object;
179     // literals cannot be overridden
180     if (browser_object instanceof Function)
181         yield co_return(browser_object());
182     if (! (browser_object instanceof browser_object_class))
183         yield co_return(browser_object);
185     var prompt = I.command.prompt;
186     if (! prompt) {
187         prompt = I.command.name.split(/-|_/).join(" ");
188         prompt = prompt[0].toUpperCase() + prompt.substring(1);
189     }
190     if (I.target != null)
191         prompt += TARGET_PROMPTS[I.target];
192     if (browser_object.label)
193         prompt += " (select " + browser_object.label + ")";
194     prompt += ":";
196     var result = yield browser_object.handler.call(null, I, prompt);
197     yield co_return(result);
201 function is_dom_node_or_window(elem) {
202     if (elem instanceof Ci.nsIDOMNode)
203         return true;
204     if (elem instanceof Ci.nsIDOMWindow)
205         return true;
206     return false;
210  * This is a simple wrapper function that sets focus to elem, and
211  * bypasses the automatic focus prevention system, which might
212  * otherwise prevent this from happening.
213  */
214 function browser_set_element_focus(buffer, elem, prevent_scroll) {
215     if (!is_dom_node_or_window(elem))
216         return;
218     buffer.last_user_input_received = Date.now();
219     if (prevent_scroll)
220         set_focus_no_scroll(buffer.window, elem);
221     else
222         elem.focus();
225 function browser_element_focus(buffer, elem)
227     if (!is_dom_node_or_window(elem))
228         return;
230     if (elem instanceof Ci.nsIDOMXULTextBoxElement)  {
231         // Focus the input field instead
232         elem = elem.wrappedJSObject.inputField;
233     }
235     browser_set_element_focus(buffer, elem);
236     if (elem instanceof Ci.nsIDOMWindow) {
237         return;
238     }
239     // If it is not a window, it must be an HTML element
240     var x = 0;
241     var y = 0;
242     if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
243         elem.contentWindow.focus();
244         return;
245     }
246     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
247         var coords = elem.getAttribute("coords").split(",");
248         x = Number(coords[0]);
249         y = Number(coords[1]);
250     }
252     var doc = elem.ownerDocument;
253     var evt = doc.createEvent("MouseEvents");
255     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
256     elem.dispatchEvent(evt);
259 function browser_object_follow(buffer, target, elem)
261     // XXX: would be better to let nsILocalFile objects be load_specs
262     if (elem instanceof Ci.nsILocalFile)
263         elem = elem.path;
265     var e;
266     if (elem instanceof load_spec)
267         e = load_spec_element(elem);
268     if (! e)
269         e = elem;
271     browser_set_element_focus(buffer, e, true /* no scroll */);
273     var no_click = ((e instanceof load_spec) ||
274                     (e instanceof Ci.nsIDOMWindow) ||
275                     (e instanceof Ci.nsIDOMHTMLFrameElement) ||
276                     (e instanceof Ci.nsIDOMHTMLIFrameElement) ||
277                     (e instanceof Ci.nsIDOMHTMLLinkElement) ||
278                     (e instanceof Ci.nsIDOMHTMLImageElement &&
279                      !e.hasAttribute("onmousedown") && !e.hasAttribute("onclick")));
281     if (target == FOLLOW_DEFAULT && !no_click) {
282         var x = 1, y = 1;
283         if (e instanceof Ci.nsIDOMHTMLAreaElement) {
284             var coords = e.getAttribute("coords").split(",");
285             if (coords.length >= 2) {
286                 x = Number(coords[0]) + 1;
287                 y = Number(coords[1]) + 1;
288             }
289         }
290         dom_node_click(e, x, y);
291         return;
292     }
294     var spec = load_spec(elem);
296     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
297         // it is nonsensical to follow a javascript url in a different
298         // buffer or window
299         target = FOLLOW_DEFAULT;
300     } else if (!(buffer instanceof content_buffer) &&
301         (target == FOLLOW_CURRENT_FRAME ||
302          target == FOLLOW_DEFAULT ||
303          target == OPEN_CURRENT_BUFFER))
304     {
305         target = OPEN_NEW_BUFFER;
306     }
308     switch (target) {
309     case FOLLOW_CURRENT_FRAME:
310         var current_frame = load_spec_source_frame(spec);
311         if (current_frame && current_frame != buffer.top_frame) {
312             var target_obj = get_web_navigation_for_frame(current_frame);
313             apply_load_spec(target_obj, spec);
314             break;
315         }
316     case FOLLOW_DEFAULT:
317     case OPEN_CURRENT_BUFFER:
318         buffer.load(spec);
319         break;
320     case OPEN_NEW_WINDOW:
321     case OPEN_NEW_BUFFER:
322     case OPEN_NEW_BUFFER_BACKGROUND:
323         create_buffer(buffer.window,
324                       buffer_creator(content_buffer,
325                                      $load = spec,
326                                      $configuration = buffer.configuration),
327                       target);
328     }
332  * Follow a link-like element by generating fake mouse events.
333  */
334 function dom_node_click (elem, x, y) {
335     var doc = elem.ownerDocument;
336     var view = doc.defaultView;
338     var evt = doc.createEvent("MouseEvents");
339     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
340                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
341     elem.dispatchEvent(evt);
343     evt = doc.createEvent("MouseEvents");
344     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
345                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
346     elem.dispatchEvent(evt);
350 function follow (I, target) {
351     if (target == null)
352         target = FOLLOW_DEFAULT;
353     I.target = target;
354     if (target == OPEN_CURRENT_BUFFER)
355         check_buffer (I.buffer, content_buffer);
356     var element = yield read_browser_object(I);
357     try {
358         element = load_spec(element);
359         if (I.forced_charset)
360             element.forced_charset = I.forced_charset;
361     } catch (e) {}
362     browser_object_follow(I.buffer, target, element);
365 function follow_new_buffer (I) {
366     yield follow(I, OPEN_NEW_BUFFER);
369 function follow_new_buffer_background (I) {
370     yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
373 function follow_new_window (I) {
374     yield follow(I, OPEN_NEW_WINDOW);
377 function follow_current_frame (I) {
378     yield follow(I, FOLLOW_CURRENT_FRAME);
381 function follow_current_buffer (I) {
382     yield follow(I, OPEN_CURRENT_BUFFER);
386 function element_get_load_target_label(element) {
387     if (element instanceof Ci.nsIDOMWindow)
388         return "page";
389     if (element instanceof Ci.nsIDOMHTMLFrameElement)
390         return "frame";
391     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
392         return "iframe";
393     return null;
396 function element_get_operation_label(element, op_name, suffix) {
397     var target_label = element_get_load_target_label(element);
398     if (target_label != null)
399         target_label = " " + target_label;
400     else
401         target_label = "";
403     if (suffix != null)
404         suffix = " " + suffix;
405     else
406         suffix = "";
408     return op_name + target_label + suffix + ":";
412 function browser_element_copy(buffer, elem)
414     var spec;
415     try {
416        spec = load_spec(elem);
417     } catch (e) {}
418     var text = null;
419     if (spec)
420         text = load_spec_uri_string(spec);
421     else  {
422         if (!(elem instanceof Ci.nsIDOMNode))
423             throw interactive_error("Element has no associated text to copy.");
424         switch (elem.localName) {
425         case "INPUT":
426         case "TEXTAREA":
427             text = elem.value;
428             break;
429         case "SELECT":
430             if (elem.selectedIndex >= 0)
431                 text = elem.item(elem.selectedIndex).text;
432             break;
433         default:
434             text = elem.textContent;
435             break;
436         }
437     }
438     browser_set_element_focus(buffer, elem);
439     writeToClipboard (text);
440     buffer.window.minibuffer.message ("Copied: " + text);
444 var view_source_use_external_editor = false, view_source_function = null;
445 function browser_object_view_source(buffer, target, elem)
447     if (view_source_use_external_editor || view_source_function)
448     {
449         var spec = load_spec(elem);
451         let [file, temp] = yield download_as_temporary(spec,
452                                                        $buffer = buffer,
453                                                        $action = "View source");
454         if (view_source_use_external_editor)
455             yield open_file_with_external_editor(file, $temporary = temp);
456         else
457             yield view_source_function(file, $temporary = temp);
458         return;
459     }
461     var win = null;
462     var window = buffer.window;
463     if (elem.localName) {
464         switch (elem.localName.toLowerCase()) {
465         case "frame": case "iframe":
466             win = elem.contentWindow;
467             break;
468         case "math":
469             view_mathml_source (window, charset, elem);
470             return;
471         default:
472             throw new Error("Invalid browser element");
473         }
474     } else
475         win = elem;
476     win.focus();
478     var url_s = win.location.href;
479     if (url_s.substring (0,12) != "view-source:") {
480         try {
481             browser_object_follow(buffer, target, "view-source:" + url_s);
482         } catch(e) { dump_error(e); }
483     } else {
484         window.minibuffer.message ("Already viewing source");
485     }
488 function view_source (I, target) {
489     I.target = target;
490     var element = yield read_browser_object(I);
491     yield browser_object_view_source(I.buffer, (target == null ? OPEN_CURRENT_BUFFER : target), element);
494 function view_source_new_buffer (I) {
495     yield view_source(I, OPEN_NEW_BUFFER);
498 function view_source_new_window (I) {
499     yield view_source(I, OPEN_NEW_WINDOW);
503 function browser_element_shell_command(buffer, elem, command) {
504     var spec = load_spec(elem);
505     yield download_as_temporary(spec,
506                                 $buffer = buffer,
507                                 $shell_command = command,
508                                 $shell_command_cwd = buffer.cwd);