Don't override OPEN_CURRENT_BUFFER with default target.
[conkeror.git] / modules / element.js
blobb0c25b6a7518495f6c6b7bcfee9ba27b3e84084b
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_class = ob; },
38         $prefix = true);
39     return ob;
42 function xpath_browser_object_handler (xpath_expression) {
43     return function (buf, prompt) {
44         var result = yield buf.window.minibuffer.read_hinted_element(
45             $buffer = buf,
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 (buf, prompt) {
59         check_buffer(buf, content_buffer);
60         var doc = buf.document;
61         if (doc.getElementsByTagName("frame").length == 0 &&
62             doc.getElementsByTagName("iframe").length == 0)
63         {
64             // only one frame (the top-level one), no need to use the hints system
65             yield co_return(buf.top_frame);
66         }
67         var result = yield buf.window.minibuffer.read_hinted_element(
68             $buffer = buf,
69             $prompt = prompt,
70             $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
71         yield co_return(result);
72     });
74 define_browser_object_class(
75     "links", "link", null,
76     xpath_browser_object_handler (
77         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
78         "@role='link'] | " +
79         "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
80         "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand] | " +
81         "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
82         "//xhtml:button | //xhtml:select"));
84 define_browser_object_class(
85     "mathml", "MathML element", null,
86     xpath_browser_object_handler ("//m:math"));
88 define_browser_object_class(
89     "top", null, null,
90     function (buf, prompt) { yield co_return(buf.top_frame); });
92 define_browser_object_class(
93     "url", null, null,
94     function (buf, prompt) {
95         check_buffer (buf, content_buffer);
96         var result = yield buf.window.minibuffer.read_url ($prompt = prompt);
97         yield co_return (result);
98     });
100 define_browser_object_class(
101     "alt", "Image Alt-text", null,
102     function (buf, prompt) {
103         var result = yield buf.window.minibuffer.read_hinted_element(
104             $buffer = buf,
105             $prompt = prompt,
106             $hint_xpath_expression = "//img[@alt]");
107         yield (co_return (result.alt));
108     });
110 define_browser_object_class(
111     "title", "Element Title", null,
112     function (buf, prompt) {
113         var result = yield buf.window.minibuffer.read_hinted_element(
114             $buffer = buf,
115             $prompt = prompt,
116             $hint_xpath_expression = "//*[@title]");
117         yield (co_return (result.title));
118     });
120 define_browser_object_class(
121     "title-or-alt", "Element Title or Alt-text", null,
122     function (buf, prompt) {
123         var result = yield buf.window.minibuffer.read_hinted_element(
124             $buffer = buf,
125             $prompt = prompt,
126             $hint_xpath_expression = "//img[@alt] | //*[@title]");
127         yield (co_return (result.title ? result.title : result.alt));
128     });
131 interactive_context.prototype.read_browser_object = function(action, target)
133     var browser_object = this.browser_object; //default
134     // literals cannot be overridden
135     if (browser_object instanceof Function)
136         yield co_return(browser_object());
137     if (! (browser_object instanceof browser_object_class))
138         yield co_return(browser_object);
140     var object_class = this._browser_object_class; //override
141     if (! object_class)
142         object_class = browser_object;
143     var prompt = action.split(/-|_/).join(" ");
144     prompt = prompt[0].toUpperCase() + prompt.substring(1);
145     if (target != null)
146         prompt += TARGET_PROMPTS[target];
147     if (object_class.label)
148         prompt += " (select " + object_class.label + ")";
149     prompt += ":";
151     var result = yield object_class.handler.call(null, this.buffer, prompt);
152     yield co_return(result);
156 function is_dom_node_or_window(elem) {
157     if (elem instanceof Ci.nsIDOMNode)
158         return true;
159     if (elem instanceof Ci.nsIDOMWindow)
160         return true;
161     return false;
165  * This is a simple wrapper function that sets focus to elem, and
166  * bypasses the automatic focus prevention system, which might
167  * otherwise prevent this from happening.
168  */
169 function browser_set_element_focus(buffer, elem, prevent_scroll) {
170     if (!is_dom_node_or_window(elem))
171         return;
173     buffer.last_user_input_received = Date.now();
174     if (prevent_scroll)
175         set_focus_no_scroll(buffer.window, elem);
176     else
177         elem.focus();
180 function browser_element_focus(buffer, elem)
182     if (!is_dom_node_or_window(elem))
183         return;
185     if (elem instanceof Ci.nsIDOMXULTextBoxElement)  {
186         // Focus the input field instead
187         elem = elem.wrappedJSObject.inputField;
188     }
190     browser_set_element_focus(buffer, elem);
191     if (elem instanceof Ci.nsIDOMWindow) {
192         return;
193     }
194     // If it is not a window, it must be an HTML element
195     var x = 0;
196     var y = 0;
197     if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
198         elem.contentWindow.focus();
199         return;
200     }
201     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
202         var coords = elem.getAttribute("coords").split(",");
203         x = Number(coords[0]);
204         y = Number(coords[1]);
205     }
207     var doc = elem.ownerDocument;
208     var evt = doc.createEvent("MouseEvents");
209     var doc = elem.ownerDocument;
211     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
212     elem.dispatchEvent(evt);
215 function browser_object_follow(buffer, target, elem)
217     browser_set_element_focus(buffer, elem, true /* no scroll */);
219     var no_click = (is_load_spec(elem) ||
220                     (elem instanceof Ci.nsIDOMWindow) ||
221                     (elem instanceof Ci.nsIDOMHTMLFrameElement) ||
222                     (elem instanceof Ci.nsIDOMHTMLIFrameElement) ||
223                     (elem instanceof Ci.nsIDOMHTMLLinkElement) ||
224                     (elem instanceof Ci.nsIDOMHTMLImageElement &&
225                      !elem.hasAttribute("onmousedown") && !elem.hasAttribute("onclick")));
227     if (target == FOLLOW_DEFAULT && !no_click) {
228         var x = 1, y = 1;
229         if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
230             var coords = elem.getAttribute("coords").split(",");
231             if (coords.length >= 2) {
232                 x = Number(coords[0]) + 1;
233                 y = Number(coords[1]) + 1;
234             }
235         }
236         browser_follow_link_with_click(buffer, elem, x, y);
237         return;
238     }
240     var spec = element_get_load_spec(elem);
241     if (spec == null) {
242         throw interactive_error("Element has no associated URL");
243         return;
244     }
246     if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
247         // This URL won't work
248         throw interactive_error("Can't load javascript URL");
249     }
251     if (!(buffer instanceof content_buffer) &&
252         (target == FOLLOW_CURRENT_FRAME ||
253          target == FOLLOW_DEFAULT ||
254          target == FOLLOW_TOP_FRAME ||
255          target == OPEN_CURRENT_BUFFER))
256         target = OPEN_NEW_BUFFER;
258     switch (target) {
259     case FOLLOW_CURRENT_FRAME:
260         var current_frame = load_spec_source_frame(spec);
261         if (current_frame && current_frame != buffer.top_frame) {
262             var target_obj = get_web_navigation_for_frame(current_frame);
263             apply_load_spec(target_obj, spec);
264             break;
265         }
266     case FOLLOW_DEFAULT:
267     case FOLLOW_TOP_FRAME:
268     case OPEN_CURRENT_BUFFER:
269         buffer.load(spec);
270         break;
271     case OPEN_NEW_WINDOW:
272     case OPEN_NEW_BUFFER:
273     case OPEN_NEW_BUFFER_BACKGROUND:
274         create_buffer(buffer.window,
275                       buffer_creator(content_buffer,
276                                      $load = spec,
277                                      $configuration = buffer.configuration),
278                       target);
279     }
283  * Follow a link-like element by generating fake mouse events.
284  */
285 function browser_follow_link_with_click(buffer, elem, x, y) {
286     var doc = elem.ownerDocument;
287     var view = doc.defaultView;
289     var evt = doc.createEvent("MouseEvents");
290     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
291                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
292     elem.dispatchEvent(evt);
294     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
295                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
296     elem.dispatchEvent(evt);
299 function element_get_load_spec(elem) {
301     if (is_load_spec(elem))
302         return elem;
304     var spec = null;
306     if (elem instanceof Ci.nsIDOMWindow)
307         spec = load_spec({document: elem.document});
309     else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
310              elem instanceof Ci.nsIDOMHTMLIFrameElement)
311         spec = load_spec({document: elem.contentDocument});
313     else {
314         var url = null;
315         var title = null;
317         if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
318             elem instanceof Ci.nsIDOMHTMLAreaElement ||
319             elem instanceof Ci.nsIDOMHTMLLinkElement) {
320             if (!elem.hasAttribute("href"))
321                 return null; // nothing can be done, as no nesting within these elements is allowed
322             url = elem.href;
323             title = elem.title || elem.textContent;
324         }
325         else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
326             url = elem.src;
327             title = elem.title || elem.alt;
328         }
329         else {
330             var node = elem;
331             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
332                 node = node.parentNode;
333             if (node && !node.hasAttribute("href"))
334                 node = null;
335             else
336                 url = node.href;
337             if (!node) {
338                 // Try simple XLink
339                 node = elem;
340                 while (node) {
341                     if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
342                         url = linkNode.getAttributeNS(XLINK_NS, "href");
343                         break;
344                     }
345                     node = node.parentNode;
346                 }
347                 if (url)
348                     url = makeURLAbsolute(node.baseURI, url);
349                 title = node.title || node.textContent;
350             }
351         }
352         if (url && url.length > 0) {
353             if (title && title.length == 0)
354                 title = null;
355             spec = load_spec({uri: url, source_frame: elem.ownerDocument.defaultView, title: title});
356         }
357     }
358     return spec;
362 function follow (I, target) {
363     if (target == null)
364         target = FOLLOW_DEFAULT;
365     var element = yield I.read_browser_object(I.command, target);
366     // XXX: to follow in the current buffer requires that the current
367     // buffer be a content_buffer.  this is perhaps not the best place
368     // for this check, because FOLLOW_DEFAULT could signify new buffer
369     // or new window.
370     check_buffer (I.buffer, content_buffer);
371     browser_object_follow(I.buffer, target, element);
374 function follow_new_buffer (I) {
375     yield follow(I, OPEN_NEW_BUFFER);
378 function follow_new_buffer_background (I) {
379     yield follow(I, OPEN_NEW_BUFFER_BACKGROUND);
382 function follow_new_window (I) {
383     yield follow(I, OPEN_NEW_WINDOW);
386 function follow_top (I) {
387     yield follow(I, FOLLOW_TOP_FRAME);
390 function follow_current_frame (I) {
391     yield follow(I, FOLLOW_CURRENT_FRAME);
394 function follow_current_buffer (I) {
395     yield follow(I, OPEN_CURRENT_BUFFER);
399 function element_get_load_target_label(element) {
400     if (element instanceof Ci.nsIDOMWindow)
401         return "page";
402     if (element instanceof Ci.nsIDOMHTMLFrameElement)
403         return "frame";
404     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
405         return "iframe";
406     return null;
409 function element_get_operation_label(element, op_name, suffix) {
410     var target_label = element_get_load_target_label(element);
411     if (target_label != null)
412         target_label = " " + target_label;
413     else
414         target_label = "";
416     if (suffix != null)
417         suffix = " " + suffix;
418     else
419         suffix = "";
421     return op_name + target_label + suffix + ":";
425 function browser_element_copy(buffer, elem)
427     var spec = element_get_load_spec(elem);
428     var text = null;
429     if (spec)
430         text = load_spec_uri_string(spec);
431     else  {
432         if (!(elem instanceof Ci.nsIDOMNode))
433             throw interactive_error("Element has no associated text to copy.");
434         switch (elem.localName) {
435         case "INPUT":
436         case "TEXTAREA":
437             text = elem.value;
438             break;
439         case "SELECT":
440             if (elem.selectedIndex >= 0)
441                 text = elem.item(elem.selectedIndex).text;
442             break;
443         default:
444             text = elem.textContent;
445             break;
446         }
447     }
448     browser_set_element_focus(buffer, elem);
449     writeToClipboard (text);
450     buffer.window.minibuffer.message ("Copied: " + text);
454 var view_source_use_external_editor = false, view_source_function = null;
455 function browser_object_view_source(buffer, target, elem)
457     if (view_source_use_external_editor || view_source_function)
458     {
459         var spec = element_get_load_spec(elem);
460         if (spec == null) {
461             throw interactive_error("Element has no associated URL");
462             return;
463         }
465         let [file, temp] = yield download_as_temporary(spec,
466                                                        $buffer = buffer,
467                                                        $action = "View source");
468         if (view_source_use_external_editor)
469             yield open_file_with_external_editor(file, $temporary = temp);
470         else
471             yield view_source_function(file, $temporary = temp);
472         return;
473     }
475     var win = null;
476     var window = buffer.window;
477     if (elem.localName) {
478         switch (elem.localName.toLowerCase()) {
479         case "frame": case "iframe":
480             win = elem.contentWindow;
481             break;
482         case "math":
483             view_mathml_source (window, charset, elem);
484             return;
485         default:
486             throw new Error("Invalid browser element");
487         }
488     } else
489         win = elem;
490     win.focus();
492     var url_s = win.location.href;
493     if (url_s.substring (0,12) != "view-source:") {
494         try {
495             browser_object_follow(buffer, target, "view-source:" + url_s);
496         } catch(e) { dump_error(e); }
497     } else {
498         window.minibuffer.message ("Already viewing source");
499     }
502 function view_source (I, target) {
503     var element = yield I.read_browser_object(I.command, target);
504     yield browser_object_view_source(I.buffer, target || OPEN_CURRENT_BUFFER, element);
507 function view_source_new_buffer (I) {
508     yield view_source(I, OPEN_NEW_BUFFER);
511 function view_source_new_window (I) {
512     yield view_source(I, OPEN_NEW_WINDOW);
516 function browser_element_shell_command(buffer, elem, command) {
517     var spec = element_get_load_spec(elem);
518     if (spec == null) {
519         throw interactive_error("Element has no associated URL");
520         return;
521     }
522     yield download_as_temporary(spec,
523                                 $buffer = buffer,
524                                 $shell_command = command,
525                                 $shell_command_cwd = buffer.cwd);