2 * (C) Copyright 2007-2008 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 * handler is a coroutine called as: handler(buffer, prompt)
22 define_keywords("$doc", "$action", "$label", "$handler", "$xpath_expression");
23 function define_browser_object_class(name) {
24 keywords(arguments, $xpath_expression = undefined);
25 var handler = arguments.$handler;
26 let xpath_expression = arguments.$xpath_expression;
27 if (handler === undefined && xpath_expression != undefined) {
28 handler = function (buf, prompt) {
29 var result = yield buf.window.minibuffer.read_hinted_element(
32 $hint_xpath_expression = xpath_expression);
33 yield co_return(result);
36 var base_obj = browser_object_classes[name];
38 base_obj = browser_object_classes[name] = {};
40 if (arguments.$action) {
41 name = name + "/" + arguments.$action;
42 obj = browser_object_classes[name];
44 obj = browser_object_classes[name] = {__proto__: base_obj};
47 if (arguments.$label !== undefined)
48 obj.label = arguments.$label;
49 if (arguments.$doc !== undefined)
50 obj.doc = arguments.$doc;
51 if (handler !== undefined)
52 obj.handler = handler;
54 "browser-object-class-"+name,
55 "A prefix command to specify that the following command operate "+
56 "on objects of type: "+name+".",
57 function (ctx) { ctx._browser_object_class = name; },
61 define_browser_object_class("images",
63 $xpath_expression = "//img | //xhtml:img");
65 define_browser_object_class("frames", $label = "frame", $handler = function (buf, prompt) {
66 check_buffer(buf, content_buffer);
67 var doc = buf.document;
68 if (doc.getElementsByTagName("frame").length == 0 &&
69 doc.getElementsByTagName("iframe").length == 0)
71 // only one frame (the top-level one), no need to use the hints system
72 yield co_return(buf.top_frame);
75 var result = yield buf.window.minibuffer.read_hinted_element(
78 $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
79 yield co_return(result);
82 define_browser_object_class(
83 "links", $label = "link",
85 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
87 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | //label | " +
88 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand] | " +
89 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
90 "//xhtml:button | //xhtml:select");
92 define_browser_object_class("mathml", $label = "MathML element", $xpath_expression = "//m:math");
94 define_browser_object_class("top", $handler = function (buf, prompt) { yield co_return(buf.top_frame); });
96 define_browser_object_class("url", $handler = function (buf, prompt) {
97 check_buffer (buf, content_buffer);
98 var result = yield buf.window.minibuffer.read_url ($prompt = prompt);
99 yield co_return (result);
103 "default_browser_object_classes",
106 follow_top: "frames",
110 view_source: "frames",
113 save_page_complete: "top",
114 save_page_as_text: "frames",
117 "Specifies the default object class for each operation.\n" +
118 "This variable should be an object literal with string-valued properties that specify one of the defined browser object classes. If a property named after the operation is not present, the \"default\" property is consulted instead.");
120 interactive_context.prototype.browser_object_class = function (action_name) {
122 this._browser_object_class ||
123 this.get("default_browser_object_classes")[action_name] ||
124 this.get("default_browser_object_classes")["default"];
128 function lookup_browser_object_class(class_name, action) {
130 if (action != null) {
131 obj = browser_object_classes[class_name + "/" + action];
135 return browser_object_classes[class_name];
138 interactive_context.prototype.read_browser_object = function(action, action_name, target)
140 var object_class_name = this.browser_object_class(action);
141 var object_class = lookup_browser_object_class(object_class_name, action);
143 var prompt = action_name;
144 var label = object_class.label || object_class_name;
146 prompt += TARGET_PROMPTS[target];
147 prompt += " (select " + label + "):";
149 var result = yield object_class.handler.call(null, this.buffer, prompt);
150 yield co_return(result);
154 function is_dom_node_or_window(elem) {
155 if (elem instanceof Ci.nsIDOMNode)
157 if (elem instanceof Ci.nsIDOMWindow)
163 * This is a simple wrapper function that sets focus to elem, and
164 * bypasses the automatic focus prevention system, which might
165 * otherwise prevent this from happening.
167 function browser_set_element_focus(buffer, elem, prevent_scroll) {
168 if (!is_dom_node_or_window(elem))
171 buffer.last_user_input_received = Date.now();
173 set_focus_no_scroll(buffer.window, elem);
178 function browser_element_focus(buffer, elem)
180 if (!is_dom_node_or_window(elem))
183 if (elem instanceof Ci.nsIDOMXULTextBoxElement) {
184 // Focus the input field instead
185 elem = elem.wrappedJSObject.inputField;
188 browser_set_element_focus(buffer, elem);
189 if (elem instanceof Ci.nsIDOMWindow) {
192 // If it is not a window, it must be an HTML element
195 if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
196 elem.contentWindow.focus();
199 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
200 var coords = elem.getAttribute("coords").split(",");
201 x = Number(coords[0]);
202 y = Number(coords[1]);
205 var doc = elem.ownerDocument;
206 var evt = doc.createEvent("MouseEvents");
207 var doc = elem.ownerDocument;
209 evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
210 elem.dispatchEvent(evt);
213 function browser_element_follow(buffer, target, elem)
215 browser_set_element_focus(buffer, elem, true /* no scroll */);
217 var no_click = (is_load_spec(elem) ||
218 (elem instanceof Ci.nsIDOMWindow) ||
219 (elem instanceof Ci.nsIDOMHTMLFrameElement) ||
220 (elem instanceof Ci.nsIDOMHTMLIFrameElement) ||
221 (elem instanceof Ci.nsIDOMHTMLLinkElement) ||
222 (elem instanceof Ci.nsIDOMHTMLImageElement &&
223 !elem.hasAttribute("onmousedown") && !elem.hasAttribute("onclick")));
225 if (target == FOLLOW_DEFAULT && !no_click) {
227 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
228 var coords = elem.getAttribute("coords").split(",");
229 if (coords.length >= 2) {
230 x = Number(coords[0]) + 1;
231 y = Number(coords[1]) + 1;
234 browser_follow_link_with_click(buffer, elem, x, y);
238 var spec = element_get_load_spec(elem);
240 throw interactive_error("Element has no associated URL");
244 if (load_spec_uri_string(spec).match(/^\s*javascript:/)) {
245 // This URL won't work
246 throw interactive_error("Can't load javascript URL");
249 if (!(buffer instanceof content_buffer) &&
250 (target == FOLLOW_CURRENT_FRAME ||
251 target == FOLLOW_DEFAULT ||
252 target == FOLLOW_TOP_FRAME ||
253 target == OPEN_CURRENT_BUFFER))
254 target = OPEN_NEW_BUFFER;
257 case FOLLOW_CURRENT_FRAME:
258 var current_frame = load_spec_source_frame(spec);
259 if (current_frame && current_frame != buffer.top_frame) {
260 var target_obj = get_web_navigation_for_frame(current_frame);
261 apply_load_spec(target_obj, spec);
265 case FOLLOW_TOP_FRAME:
266 case OPEN_CURRENT_BUFFER:
269 case OPEN_NEW_WINDOW:
270 case OPEN_NEW_BUFFER:
271 case OPEN_NEW_BUFFER_BACKGROUND:
272 create_buffer(buffer.window,
273 buffer_creator(content_buffer,
275 $configuration = buffer.configuration),
281 * Follow a link-like element by generating fake mouse events.
283 function browser_follow_link_with_click(buffer, elem, x, y) {
284 var doc = elem.ownerDocument;
285 var view = doc.defaultView;
287 var evt = doc.createEvent("MouseEvents");
288 evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
289 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
290 elem.dispatchEvent(evt);
292 evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
293 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
294 elem.dispatchEvent(evt);
297 function element_get_load_spec(elem) {
299 if (is_load_spec(elem))
304 if (elem instanceof Ci.nsIDOMWindow)
305 spec = load_spec({document: elem.document});
307 else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
308 elem instanceof Ci.nsIDOMHTMLIFrameElement)
309 spec = load_spec({document: elem.contentDocument});
315 if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
316 elem instanceof Ci.nsIDOMHTMLAreaElement ||
317 elem instanceof Ci.nsIDOMHTMLLinkElement) {
318 if (!elem.hasAttribute("href"))
319 return null; // nothing can be done, as no nesting within these elements is allowed
321 title = elem.title || elem.textContent;
323 else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
325 title = elem.title || elem.alt;
329 while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
330 node = node.parentNode;
331 if (node && !node.hasAttribute("href"))
339 if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
340 url = linkNode.getAttributeNS(XLINK_NS, "href");
343 node = node.parentNode;
346 url = makeURLAbsolute(node.baseURI, url);
347 title = node.title || node.textContent;
350 if (url && url.length > 0) {
351 if (title && title.length == 0)
353 spec = load_spec({uri: url, source_frame: elem.ownerDocument.defaultView, title: title});
359 interactive("follow", null, function (I) {
360 var target = I.browse_target("follow");
361 var element = yield I.read_browser_object("follow", "Follow", target);
362 browser_element_follow(I.buffer, target, element);
365 interactive("follow-top", null, function (I) {
366 var target = I.browse_target("follow-top");
367 var element = yield I.read_browser_object("follow_top", "Follow", target);
368 browser_element_follow(I.buffer, target, element);
371 interactive("focus", null, function (I) {
372 var element = yield I.read_browser_object("focus", "Focus");
373 browser_element_focus(I.buffer, element);
376 function element_get_load_target_label(element) {
377 if (element instanceof Ci.nsIDOMWindow)
379 if (element instanceof Ci.nsIDOMHTMLFrameElement)
381 if (element instanceof Ci.nsIDOMHTMLIFrameElement)
386 function element_get_operation_label(element, op_name, suffix) {
387 var target_label = element_get_load_target_label(element);
388 if (target_label != null)
389 target_label = " " + target_label;
394 suffix = " " + suffix;
398 return op_name + target_label + suffix + ":";
401 interactive("save", null, function (I) {
402 var element = yield I.read_browser_object("save", "Save");
404 var spec = element_get_load_spec(element);
406 throw interactive_error("Element has no associated URI");
409 panel = create_info_panel(I.window, "download-panel",
411 element_get_operation_label(element, "Saving"),
412 load_spec_uri_string(spec)],
413 ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
416 var file = yield I.minibuffer.read_file_check_overwrite(
417 $prompt = "Save as:",
418 $initial_value = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer),
430 function browser_element_copy(buffer, elem)
432 var spec = element_get_load_spec(elem);
435 text = load_spec_uri_string(spec);
437 if (!(elem instanceof Ci.nsIDOMNode))
438 throw interactive_error("Element has no associated text to copy.");
439 switch (elem.localName) {
445 if (elem.selectedIndex >= 0)
446 text = elem.item(elem.selectedIndex).text;
449 text = elem.textContent;
453 browser_set_element_focus(buffer, elem);
454 writeToClipboard (text);
455 buffer.window.minibuffer.message ("Copied: " + text);
459 interactive("copy", null, function (I) {
460 var element = yield I.read_browser_object("copy", "Copy");
461 browser_element_copy(I.buffer, element);
464 var view_source_use_external_editor = false, view_source_function = null;
465 function browser_element_view_source(buffer, target, elem)
467 if (view_source_use_external_editor || view_source_function)
469 var spec = element_get_load_spec(elem);
471 throw interactive_error("Element has no associated URL");
475 let [file, temp] = yield download_as_temporary(spec,
477 $action = "View source");
478 if (view_source_use_external_editor)
479 yield open_file_with_external_editor(file, $temporary = temp);
481 yield view_source_function(file, $temporary = temp);
486 var window = buffer.window;
487 if (elem.localName) {
488 switch (elem.localName.toLowerCase()) {
489 case "frame": case "iframe":
490 win = elem.contentWindow;
493 view_mathml_source (window, charset, elem);
496 throw new Error("Invalid browser element");
502 var url_s = win.location.href;
503 if (url_s.substring (0,12) != "view-source:") {
505 open_in_browser(buffer, target, "view-source:" + url_s);
506 } catch(e) { dump_error(e); }
508 window.minibuffer.message ("Already viewing source");
512 interactive("view-source", null, function (I) {
513 var target = I.browse_target("follow");
514 var element = yield I.read_browser_object("view_source", "View source", target);
515 yield browser_element_view_source(I.buffer, target, element);
518 interactive("shell-command-on-url", null, function (I) {
520 var element = yield I.read_browser_object("shell_command_url", "URL shell command");
521 var spec = element_get_load_spec(element);
523 throw interactive_error("Unable to obtain URI from element");
525 var uri = load_spec_uri_string(spec);
528 panel = create_info_panel(I.window, "download-panel",
530 element_get_operation_label(element, "Running on", "URI"),
531 load_spec_uri_string(spec)],
532 ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
535 var cmd = yield I.minibuffer.read_shell_command(
537 $initial_value = load_spec_default_shell_command(spec));
542 shell_command_with_argument_blind(cmd, uri, $cwd = cwd);
545 function browser_element_shell_command(buffer, elem, command) {
546 var spec = element_get_load_spec(elem);
548 throw interactive_error("Element has no associated URL");
551 yield download_as_temporary(spec,
553 $shell_command = command,
554 $shell_command_cwd = buffer.cwd);
557 interactive("shell-command-on-file", null, function (I) {
559 var element = yield I.read_browser_object("shell_command", "Shell command");
561 var spec = element_get_load_spec(element);
563 throw interactive_error("Unable to obtain URI from element");
565 var uri = load_spec_uri_string(spec);
568 panel = create_info_panel(I.window, "download-panel",
570 element_get_operation_label(element, "Running on"),
571 load_spec_uri_string(spec)],
572 ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
576 var cmd = yield I.minibuffer.read_shell_command(
578 $initial_value = load_spec_default_shell_command(spec));
583 /* FIXME: specify cwd as well */
584 yield browser_element_shell_command(I.buffer, element, cmd);
587 interactive("bookmark", null, function (I) {
588 var element = yield I.read_browser_object("bookmark", "Bookmark");
589 var spec = element_get_load_spec(element);
591 throw interactive_error("Element has no associated URI");
592 var uri_string = load_spec_uri_string(spec);
594 panel = create_info_panel(I.window, "bookmark-panel",
596 element_get_operation_label(element, "Bookmarking"),
599 var title = yield I.minibuffer.read($prompt = "Bookmark with title:", $initial_value = load_spec_title(spec) || "");
603 add_bookmark(uri_string, title);
604 I.minibuffer.message("Added bookmark: " + uri_string + " - " + title);
607 interactive("save-page", null, function (I) {
608 check_buffer(I.buffer, content_buffer);
609 var element = yield I.read_browser_object("save_page", "Save page");
610 var spec = element_get_load_spec(element);
611 if (!spec || !load_spec_document(spec))
612 throw interactive_error("Element is not associated with a document.");
613 var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
616 panel = create_info_panel(I.window, "download-panel",
618 element_get_operation_label(element, "Saving"),
619 load_spec_uri_string(spec)],
620 ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
623 var file = yield I.minibuffer.read_file_check_overwrite(
624 $prompt = "Save page as:",
626 $initial_value = suggested_path);
631 save_uri(spec, file, $buffer = I.buffer);
634 interactive("save-page-as-text", null, function (I) {
635 check_buffer(I.buffer, content_buffer);
636 var element = yield I.read_browser_object("save_page_as_text", "Save page as text");
637 var spec = element_get_load_spec(element);
639 if (!spec || !(doc = load_spec_document(spec)))
640 throw interactive_error("Element is not associated with a document.");
641 var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec, "txt"), I.buffer);
644 panel = create_info_panel(I.window, "download-panel",
646 element_get_operation_label(element, "Saving", "as text"),
647 load_spec_uri_string(spec)],
648 ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
651 var file = yield I.minibuffer.read_file_check_overwrite(
652 $prompt = "Save page as text:",
654 $initial_value = suggested_path);
659 save_document_as_text(doc, file, $buffer = I.buffer);
662 interactive("save-page-complete", null, function (I) {
663 check_buffer(I.buffer, content_buffer);
664 var element = yield I.read_browser_object("save_page_complete", "Save page complete");
665 var spec = element_get_load_spec(element);
667 if (!spec || !(doc = load_spec_document(spec)))
668 throw interactive_error("Element is not associated with a document.");
669 var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
672 panel = create_info_panel(I.window, "download-panel",
674 element_get_operation_label(element, "Saving complete"),
675 load_spec_uri_string(spec)],
676 ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
679 var file = yield I.minibuffer.read_file_check_overwrite(
680 $prompt = "Save page complete:",
682 $initial_value = suggested_path);
683 // FIXME: use proper read function
684 var dir = yield I.minibuffer.read_file(
685 $prompt = "Data Directory:",
687 $initial_value = file.path + ".support");
692 save_document_complete(doc, file, dir, $buffer = I.buffer);
695 default_browse_targets["view-as-mime-type"] = [FOLLOW_CURRENT_FRAME, OPEN_CURRENT_BUFFER,
696 OPEN_NEW_BUFFER, OPEN_NEW_WINDOW];
697 interactive("view-as-mime-type",
698 "Display a browser object in the browser using the specified MIME type.",
700 var element = yield I.read_browser_object("view_as_mime_type", "View in browser as mime type");
701 var spec = element_get_load_spec(element);
703 var target = I.browse_target("view-as-mime-type");
706 throw interactive_error("Element is not associated with a URI");
708 if (!can_override_mime_type_for_uri(load_spec_uri(spec)))
709 throw interactive_error("Overriding the MIME type is not currently supported for non-HTTP URLs.");
713 var mime_type = load_spec_mime_type(spec);
714 panel = create_info_panel(I.window, "download-panel",
716 element_get_operation_label(element, "View in browser"),
717 load_spec_uri_string(spec)],
718 ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
722 let suggested_type = mime_type;
723 if (gecko_viewable_mime_type_list.indexOf(suggested_type) == -1)
724 suggested_type = "text/plain";
725 mime_type = yield I.minibuffer.read_gecko_viewable_mime_type(
726 $prompt = "View internally as",
727 $initial_value = suggested_type,
729 override_mime_type_for_next_load(load_spec_uri(spec), mime_type);
730 browser_element_follow(I.buffer, target, spec);