2 * (C) Copyright 2007-2009 John J. Foerch
3 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5 * Portions of this file are derived from Vimperator,
6 * (C) Copyright 2006-2007 Martin Stubenschrott.
8 * Use, modification, and distribution are subject to the terms specified in the
14 require("mime-type-override.js");
15 require("minibuffer-read-mime-type.js");
17 var browser_object_classes
= {};
20 * browser_object_class
22 * In normal cases, make a new browser_object_class with the function,
23 * `define_browser_object_class'.
25 * name: See note on `define_browser_object_class'.
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.
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.
37 define_keywords("$hint");
38 function browser_object_class (name
, doc
, handler
) {
41 this.handler
= handler
;
43 this.hint
= arguments
.$hint
;
47 * define_browser_object_class
49 * In normal cases, make a new browser_object_class with the function,
50 * `define_browser_object_class'.
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.
56 * Other arguments are as for `browser_object_class'.
59 function define_browser_object_class (name
, doc
, handler
) {
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
; },
74 * xpath_browser_object_handler
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
81 function xpath_browser_object_handler (xpath_expression
) {
82 return function (I
, prompt
) {
83 var result
= yield I
.buffer
.window
.minibuffer
.read_hinted_element(
86 $hint_xpath_expression
= xpath_expression
);
87 yield co_return(result
);
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)
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")
116 // only one frame (the top-level one), no need to use the hints system
117 yield co_return(I
.buffer
.top_frame
);
119 var result
= yield I
.buffer
.window
.minibuffer
.read_hinted_element(
122 $hint_xpath_expression
= "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
123 yield co_return(result
);
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 "+
132 "@oncommand or @role='link' or @role='button' or @role='menuitem'] | "+
133 "//input[not(@type='hidden')] | //a[@href] | //area | "+
134 "//iframe | //textarea | //button | //select | "+
135 "//*[@contenteditable = 'true'] | "+
136 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or "+
137 "@oncommand or @role='link' or @role='button' or @role='menuitem'] | "+
138 "//xhtml:input[not(@type='hidden')] | //xhtml:a[@href] | //xhtml:area | "+
139 "//xhtml:iframe | //xhtml:textarea | //xhtml:button | //xhtml:select | " +
140 "//xhtml:*[@contenteditable = 'true'] | "+
142 $hint
= "select link");
144 define_browser_object_class("mathml",
145 "Browser object class for selecting a MathML node via hinting.",
146 xpath_browser_object_handler("//m:math"),
147 $hint
= "select MathML element");
149 define_browser_object_class("top",
150 "Browser object class which returns the top frame of the document.",
151 function (I
, prompt
) { return I
.buffer
.top_frame
; });
153 define_browser_object_class("url",
154 "Browser object class which prompts the user for an url or webjump.",
155 function (I
, prompt
) {
156 var result
= yield I
.buffer
.window
.minibuffer
.read_url($prompt
= prompt
);
157 yield co_return(result
);
159 $hint
= "enter URL/webjump");
161 define_browser_object_class("paste-url",
162 "Browser object which reads an url from the X Primary Selection, "+
163 "falling back on the clipboard for operating systems which lack one.",
164 function (I
, prompt
) {
165 var url
= read_from_x_primary_selection();
167 url
= url
.replace(/^\s*|\s*$/,"");
168 // add http:// if needed
169 if (url
.match(/^[^:]+\./)) {
170 url
= "http://" + url
;
173 return make_uri(url
).spec
;
175 throw new interactive_error("error: malformed url: "+url
);
179 define_browser_object_class("file",
180 "Browser object which prompts for a file name.",
181 function (I
, prompt
) {
182 var result
= yield I
.buffer
.window
.minibuffer
.read_file(
184 $history
= I
.command
.name
+"/file",
185 $initial_value
= I
.local
.cwd
.path
);
186 yield co_return(result
);
188 $hint
= "enter file name");
190 define_browser_object_class("alt",
191 "Browser object class which returns the alt text of an html:img, "+
192 "selected via hinting",
193 function (I
, prompt
) {
194 var result
= yield I
.buffer
.window
.minibuffer
.read_hinted_element(
197 $hint_xpath_expression
= "//img[@alt] | //xhtml:img[@alt]");
198 yield co_return(result
.alt
);
200 $hint
= "select image for alt-text");
202 define_browser_object_class("title",
203 "Browser object class which returns the title attribute of an element, "+
204 "selected via hinting",
205 function (I
, prompt
) {
206 var result
= yield I
.buffer
.window
.minibuffer
.read_hinted_element(
209 $hint_xpath_expression
= "//*[@title] | //xhtml:*[@title]");
210 yield co_return(result
.title
);
212 $hint
= "select element for title attribute");
214 define_browser_object_class("title-or-alt",
215 "Browser object which is the union of browser-object-alt and "+
216 "browser-object-title, with title having higher precedence in "+
217 "the case of an element that has both.",
218 function (I
, prompt
) {
219 var result
= yield I
.buffer
.window
.minibuffer
.read_hinted_element(
222 $hint_xpath_expression
= "//img[@alt] | //*[@title] | //xhtml:img[@alt] | //xhtml:*[@title]");
223 yield co_return(result
.title
? result
.title
: result
.alt
);
225 $hint
= "select element for title or alt-text");
227 define_browser_object_class("scrape-url",
228 "Browser object which lets the user choose an url from a list of "+
229 "urls scraped from the source code of the document.",
230 function (I
, prompt
) {
231 var completions
= I
.buffer
.document
.documentElement
.innerHTML
232 .match(/https?:[^\s<>)"]*/g)
233 .filter(remove_duplicates_filter());
234 var completer
= all_word_completer($completions
= completions
);
235 var result
= yield I
.buffer
.window
.minibuffer
.read(
237 $completer
= completer
,
238 $initial_value
= null,
239 $auto_complete
= "url",
241 $match_required
= false);
242 yield co_return(result
);
244 $hint
= "choose scraped URL");
246 define_browser_object_class("up-url",
247 "Browser object which returns the url one level above the current one.",
248 function (I
, prompt
) {
249 return compute_up_url(I
.buffer
.current_uri
);
252 define_browser_object_class("focused-element",
253 "Browser object which returns the focused element.",
254 function (I
, prompt
) { return I
.buffer
.focused_element
; });
256 define_browser_object_class("dom-node", null,
257 xpath_browser_object_handler("//* | //xhtml:*"),
258 $hint
= "select DOM node");
260 define_browser_object_class("fragment-link",
261 "Browser object class which returns a link to the specified fragment of a page",
262 function (I
, prompt
) {
263 var elem
= yield I
.buffer
.window
.minibuffer
.read_hinted_element(
266 $hint_xpath_expression
= "//*[@id] | //a[@name] | //xhtml:*[@id] | //xhtml:a[@name]");
267 yield co_return(page_fragment_load_spec(elem
));
269 $hint
= "select element to link to");
271 interactive("browser-object-text",
272 "Composable browser object which returns the text of another object.",
274 // our job here is to modify the interactive context.
275 // set I.browser_object to a browser_object which calls the
276 // original one, then returns its text.
277 var b
= I
.browser_object
;
278 I
.browser_object = function (I
) {
279 I
.browser_object
= b
;
280 var e
= yield read_browser_object(I
);
281 if (e
instanceof Ci
.nsIDOMHTMLImageElement
)
282 yield co_return(e
.getAttribute("alt"));
283 yield co_return(e
.textContent
);
288 function get_browser_object (I
) {
289 var obj
= I
.browser_object
;
292 // if there was no interactive browser-object,
293 // binding_browser_object becomes the default.
294 if (obj
=== undefined) {
295 obj
= I
.binding_browser_object
;
297 // if the command's default browser object is a non-null literal,
298 // it overrides an interactive browser-object, but not a binding
300 if (cmd
.browser_object
!= null &&
301 (! (cmd
.browser_object
instanceof browser_object_class
)) &&
302 (I
.binding_browser_object
=== undefined))
304 obj
= cmd
.browser_object
;
306 // if we still have no browser-object, look for a page-mode
307 // default, or finally the command default.
308 if (obj
=== undefined) {
310 I
.buffer
.default_browser_object_classes
[cmd
.name
]) ||
317 function read_browser_object (I
) {
318 var browser_object
= get_browser_object(I
);
319 if (browser_object
=== undefined)
320 throw interactive_error("No browser object");
323 // literals cannot be overridden
324 if (browser_object
instanceof Function
) {
325 result
= yield browser_object(I
);
326 yield co_return(result
);
328 if (! (browser_object
instanceof browser_object_class
))
329 yield co_return(browser_object
);
331 var prompt
= I
.command
.prompt
;
333 prompt
= I
.command
.name
.split(/-|_/).join(" ");
334 prompt
= prompt
[0].toUpperCase() + prompt
.substring(1);
336 if (I
.target
!= null)
337 prompt
+= TARGET_PROMPTS
[I
.target
];
338 if (browser_object
.hint
)
339 prompt
+= " (" + browser_object
.hint
+ ")";
342 result
= yield browser_object
.handler
.call(null, I
, prompt
);
343 yield co_return(result
);
348 * This is a simple wrapper function that sets focus to elem, and
349 * bypasses the automatic focus prevention system, which might
350 * otherwise prevent this from happening.
352 function browser_set_element_focus (buffer
, elem
, prevent_scroll
) {
353 if (! dom_node_or_window_p(elem
))
358 set_focus_no_scroll(buffer
.window
, elem
);
363 function browser_element_focus (buffer
, elem
) {
364 if (! dom_node_or_window_p(elem
))
367 if (elem
instanceof Ci
.nsIDOMXULTextBoxElement
) {
368 if (elem
.wrappedJSObject
)
369 elem
= elem
.wrappedJSObject
.inputField
; // focus the input field
371 elem
= elem
.inputField
;
374 browser_set_element_focus(buffer
, elem
);
375 if (elem
instanceof Ci
.nsIDOMWindow
)
378 // If it is not a window, it must be an HTML element
381 if (elem
instanceof Ci
.nsIDOMHTMLFrameElement
||
382 elem
instanceof Ci
.nsIDOMHTMLIFrameElement
)
384 elem
.contentWindow
.focus();
387 if (elem
instanceof Ci
.nsIDOMHTMLAreaElement
) {
388 var coords
= elem
.getAttribute("coords").split(",");
389 x
= Number(coords
[0]);
390 y
= Number(coords
[1]);
393 var doc
= elem
.ownerDocument
;
394 var evt
= doc
.createEvent("MouseEvents");
396 evt
.initMouseEvent("mouseover", true, true, doc
.defaultView
, 1, x
, y
, 0, 0, 0, 0, 0, 0, 0, null);
397 elem
.dispatchEvent(evt
);
400 function browser_object_follow (buffer
, target
, elem
) {
401 // XXX: would be better to let nsILocalFile objects be load_specs
402 if (elem
instanceof Ci
.nsILocalFile
)
406 if (elem
instanceof load_spec
)
407 e
= load_spec_element(elem
);
411 browser_set_element_focus(buffer
, e
, true /* no scroll */);
413 var no_click
= (((elem
instanceof load_spec
) &&
414 load_spec_forced_charset(elem
)) ||
415 (e
instanceof load_spec
) ||
416 (e
instanceof Ci
.nsIDOMWindow
) ||
417 (e
instanceof Ci
.nsIDOMHTMLFrameElement
) ||
418 (e
instanceof Ci
.nsIDOMHTMLIFrameElement
) ||
419 (e
instanceof Ci
.nsIDOMHTMLLinkElement
) ||
420 (e
instanceof Ci
.nsIDOMHTMLImageElement
&&
421 !e
.hasAttribute("onmousedown") && !e
.hasAttribute("onclick")));
423 if (target
== FOLLOW_DEFAULT
&& !no_click
) {
425 if (e
instanceof Ci
.nsIDOMHTMLAreaElement
) {
426 var coords
= e
.getAttribute("coords").split(",");
427 if (coords
.length
>= 2) {
428 x
= Number(coords
[0]) + 1;
429 y
= Number(coords
[1]) + 1;
432 dom_node_click(e
, x
, y
);
436 var spec
= load_spec(elem
);
438 if (load_spec_uri_string(spec
).match(/^\s*javascript:/)) {
439 // it is nonsensical to follow a javascript url in a different
441 target
= FOLLOW_DEFAULT
;
442 } else if (!(buffer
instanceof content_buffer
) &&
443 (target
== FOLLOW_CURRENT_FRAME
||
444 target
== FOLLOW_DEFAULT
||
445 target
== OPEN_CURRENT_BUFFER
))
447 target
= OPEN_NEW_BUFFER
;
451 case FOLLOW_CURRENT_FRAME
:
452 var current_frame
= load_spec_source_frame(spec
);
453 if (current_frame
&& current_frame
!= buffer
.top_frame
) {
454 var target_obj
= get_web_navigation_for_frame(current_frame
);
455 apply_load_spec(target_obj
, spec
);
459 case OPEN_CURRENT_BUFFER
:
462 case OPEN_NEW_WINDOW
:
463 case OPEN_NEW_BUFFER
:
464 case OPEN_NEW_BUFFER_BACKGROUND
:
465 if (dom_node_or_window_p(e
))
469 create_buffer(buffer
.window
,
470 buffer_creator(content_buffer
,
478 * Follow a link-like element by generating fake mouse events.
480 function dom_node_click (elem
, x
, y
) {
481 var doc
= elem
.ownerDocument
;
482 var view
= doc
.defaultView
;
484 var evt
= doc
.createEvent("MouseEvents");
485 evt
.initMouseEvent("mousedown", true, true, view
, 1, x
, y
, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
486 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
487 elem
.dispatchEvent(evt
);
489 evt
= doc
.createEvent("MouseEvents");
490 evt
.initMouseEvent("click", true, true, view
, 1, x
, y
, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
491 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
492 elem
.dispatchEvent(evt
);
494 evt
= doc
.createEvent("MouseEvents");
495 evt
.initMouseEvent("mouseup", true, true, view
, 1, x
, y
, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
496 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
497 elem
.dispatchEvent(evt
);
501 function follow (I
, target
) {
503 target
= FOLLOW_DEFAULT
;
505 if (target
== OPEN_CURRENT_BUFFER
)
506 check_buffer(I
.buffer
, content_buffer
);
507 var element
= yield read_browser_object(I
);
509 element
= load_spec(element
);
510 if (I
.forced_charset
)
511 element
.forced_charset
= I
.forced_charset
;
513 browser_object_follow(I
.buffer
, target
, element
);
516 function follow_new_buffer (I
) {
517 yield follow(I
, OPEN_NEW_BUFFER
);
520 function follow_new_buffer_background (I
) {
521 yield follow(I
, OPEN_NEW_BUFFER_BACKGROUND
);
524 function follow_new_window (I
) {
525 yield follow(I
, OPEN_NEW_WINDOW
);
528 function follow_current_frame (I
) {
529 yield follow(I
, FOLLOW_CURRENT_FRAME
);
532 function follow_current_buffer (I
) {
533 yield follow(I
, OPEN_CURRENT_BUFFER
);
537 function element_get_load_target_label (element
) {
538 if (element
instanceof Ci
.nsIDOMWindow
)
540 if (element
instanceof Ci
.nsIDOMHTMLFrameElement
)
542 if (element
instanceof Ci
.nsIDOMHTMLIFrameElement
)
547 function element_get_operation_label (element
, op_name
, suffix
) {
548 var target_label
= element_get_load_target_label(element
);
549 if (target_label
!= null)
550 target_label
= " " + target_label
;
555 suffix
= " " + suffix
;
559 return op_name
+ target_label
+ suffix
+ ":";
563 function browser_element_text (buffer
, elem
) {
565 var spec
= load_spec(elem
);
568 if (typeof elem
== "string" || elem
instanceof String
)
571 text
= load_spec_uri_string(spec
);
573 if (!(elem
instanceof Ci
.nsIDOMNode
))
574 throw interactive_error("Element has no associated text to copy.");
575 var tag
= elem
.localName
.toLowerCase();
576 if ((tag
== "input" || tag
== "button") &&
577 elem
.type
== "submit" && elem
.form
&& elem
.form
.action
)
579 text
= elem
.form
.action
;
580 } else if (tag
== "input" || tag
== "textarea") {
582 } else if (tag
== "select") {
583 if (elem
.selectedIndex
>= 0)
584 text
= elem
.item(elem
.selectedIndex
).text
;
586 text
= elem
.textContent
;
593 define_variable("copy_append_separator", "\n",
594 "String used to separate old and new text when text is appended to clipboard");
596 function copy_text (I
) {
597 var element
= yield read_browser_object(I
);
598 browser_set_element_focus(I
.buffer
, element
);
599 var text
= browser_element_text(I
.buffer
, element
);
600 writeToClipboard(text
);
601 I
.buffer
.window
.minibuffer
.message("Copied: " + text
);
604 function copy_text_append (I
) {
605 var element
= yield read_browser_object(I
);
606 browser_set_element_focus(I
.buffer
, element
);
607 var new_text
= browser_element_text(I
.buffer
, element
);
608 var text
= read_from_clipboard() + copy_append_separator
+ new_text
;
609 writeToClipboard(text
);
610 I
.buffer
.window
.minibuffer
.message("Copied: ..." + new_text
);
614 define_variable("view_source_use_external_editor", false,
615 "When true, the `view-source' command will send its document to "+
616 "your external editor.");
618 define_variable("view_source_function", null,
619 "May be set to a user-defined function for viewing source code. "+
620 "The function should accept an nsILocalFile of the filename as "+
621 "its one positional argument, and it will also be called with "+
622 "the keyword `$temporary', whose value will be true if the file "+
623 "is considered temporary, and therefore the function must take "+
624 "responsibility for deleting it.");
626 function browser_object_view_source (buffer
, target
, elem
) {
627 if (view_source_use_external_editor
|| view_source_function
) {
628 var spec
= load_spec(elem
);
630 let [file
, temp
] = yield download_as_temporary(spec
,
632 $action
= "View source");
633 if (view_source_use_external_editor
)
634 yield open_file_with_external_editor(file
, $temporary
= temp
);
636 yield view_source_function(file
, $temporary
= temp
);
641 var window
= buffer
.window
;
642 if (elem
.localName
) {
643 switch (elem
.localName
.toLowerCase()) {
644 case "frame": case "iframe":
645 win
= elem
.contentWindow
;
648 view_mathml_source(window
, charset
, elem
);
651 throw new Error("Invalid browser element");
657 var url_s
= win
.location
.href
;
658 if (url_s
.substring (0,12) != "view-source:") {
660 browser_object_follow(buffer
, target
, "view-source:" + url_s
);
661 } catch(e
) { dump_error(e
); }
664 browser_object_follow(buffer
, target
, url_s
.replace(/^view-source\:/, ''));
665 } catch(e
) { dump_error(e
); }
669 function view_source (I
, target
) {
672 target
= OPEN_CURRENT_BUFFER
;
673 var element
= yield read_browser_object(I
);
674 yield browser_object_view_source(I
.buffer
, target
, element
);
677 function view_source_new_buffer (I
) {
678 yield view_source(I
, OPEN_NEW_BUFFER
);
681 function view_source_new_window (I
) {
682 yield view_source(I
, OPEN_NEW_WINDOW
);
686 function browser_element_shell_command (buffer
, elem
, command
, cwd
) {
687 var spec
= load_spec(elem
);
688 yield download_as_temporary(spec
,
690 $shell_command
= command
,
691 $shell_command_cwd
= cwd
);