new module to enable editing and deleting of bookmarks
[conkeror/arlinius.git] / modules / element.js
blob28ac598c511abd7749fdc2e3734287dc9b94d02c
1 /**
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
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 * 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'.
27 * doc: a docstring
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) {
39 keywords(arguments);
40 this.name = name;
41 this.handler = handler;
42 this.doc = doc;
43 this.hint = arguments.$hint;
46 /**
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'.
58 // keywords: $hint
59 function define_browser_object_class (name, doc, handler) {
60 keywords(arguments);
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; },
69 $prefix = true);
70 return ob;
73 /**
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
79 * `xpath_expression'.
81 function xpath_browser_object_handler (xpath_expression) {
82 return function (I, prompt) {
83 var result = yield I.buffer.window.minibuffer.read_hinted_element(
84 $buffer = I.buffer,
85 $prompt = prompt,
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)
103 skip_hints = false;
104 else {
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")
110 continue;
111 skip_hints = false;
112 break;
115 if (skip_hints) {
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(
120 $buffer = I.buffer,
121 $prompt = prompt,
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'] | "+
141 "//svg:a"),
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();
166 // trim spaces
167 url = url.replace(/^\s*|\s*$/,"");
168 // add http:// if needed
169 if (url.match(/^[^:]+\./)) {
170 url = "http://" + url;
172 try {
173 return make_uri(url).spec;
174 } catch (e) {
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(
183 $prompt = prompt,
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(
195 $buffer = I.buffer,
196 $prompt = prompt,
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(
207 $buffer = I.buffer,
208 $prompt = prompt,
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(
220 $buffer = I.buffer,
221 $prompt = prompt,
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(
236 $prompt = prompt,
237 $completer = completer,
238 $initial_value = null,
239 $auto_complete = "url",
240 $select,
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(
264 $buffer = I.buffer,
265 $prompt = prompt,
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.",
273 function (I) {
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);
286 $prefix);
288 function get_browser_object (I) {
289 var obj = I.browser_object;
290 var cmd = I.command;
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
299 // browser object.
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) {
309 obj = (I.buffer &&
310 I.buffer.default_browser_object_classes[cmd.name]) ||
311 cmd.browser_object;
314 return obj;
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");
322 var result;
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;
332 if (! 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 + ")";
340 prompt += ":";
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))
354 return;
355 if (! elem.focus)
356 return;
357 if (prevent_scroll)
358 set_focus_no_scroll(buffer.window, elem);
359 else
360 elem.focus();
363 function browser_element_focus (buffer, elem) {
364 if (! dom_node_or_window_p(elem))
365 return;
367 if (elem instanceof Ci.nsIDOMXULTextBoxElement) {
368 if (elem.wrappedJSObject)
369 elem = elem.wrappedJSObject.inputField; // focus the input field
370 else
371 elem = elem.inputField;
374 browser_set_element_focus(buffer, elem);
375 if (elem instanceof Ci.nsIDOMWindow)
376 return;
378 // If it is not a window, it must be an HTML element
379 var x = 0;
380 var y = 0;
381 if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
382 elem instanceof Ci.nsIDOMHTMLIFrameElement)
384 elem.contentWindow.focus();
385 return;
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)
403 elem = elem.path;
405 var e;
406 if (elem instanceof load_spec)
407 e = load_spec_element(elem);
408 if (! e)
409 e = 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) {
424 var x = 1, y = 1;
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);
433 return;
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
440 // buffer or window
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;
450 switch (target) {
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);
456 break;
458 case FOLLOW_DEFAULT:
459 case OPEN_CURRENT_BUFFER:
460 buffer.load(spec);
461 break;
462 case OPEN_NEW_WINDOW:
463 case OPEN_NEW_BUFFER:
464 case OPEN_NEW_BUFFER_BACKGROUND:
465 if (dom_node_or_window_p(e))
466 var opener = buffer;
467 else
468 opener = null;
469 create_buffer(buffer.window,
470 buffer_creator(content_buffer,
471 $opener = opener,
472 $load = spec),
473 target);
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) {
502 if (target == null)
503 target = FOLLOW_DEFAULT;
504 I.target = target;
505 if (target == OPEN_CURRENT_BUFFER)
506 check_buffer(I.buffer, content_buffer);
507 var element = yield read_browser_object(I);
508 try {
509 element = load_spec(element);
510 if (I.forced_charset)
511 element.forced_charset = I.forced_charset;
512 } catch (e) {}
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)
539 return "page";
540 if (element instanceof Ci.nsIDOMHTMLFrameElement)
541 return "frame";
542 if (element instanceof Ci.nsIDOMHTMLIFrameElement)
543 return "iframe";
544 return null;
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;
551 else
552 target_label = "";
554 if (suffix != null)
555 suffix = " " + suffix;
556 else
557 suffix = "";
559 return op_name + target_label + suffix + ":";
563 function browser_element_text (buffer, elem) {
564 try {
565 var spec = load_spec(elem);
566 } catch (e) {}
567 var text = null;
568 if (typeof elem == "string" || elem instanceof String)
569 text = elem;
570 else if (spec)
571 text = load_spec_uri_string(spec);
572 else {
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") {
581 text = elem.value;
582 } else if (tag == "select") {
583 if (elem.selectedIndex >= 0)
584 text = elem.item(elem.selectedIndex).text;
585 } else {
586 text = elem.textContent;
589 return text;
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,
631 $buffer = buffer,
632 $action = "View source");
633 if (view_source_use_external_editor)
634 yield open_file_with_external_editor(file, $temporary = temp);
635 else
636 yield view_source_function(file, $temporary = temp);
637 return;
640 var win = null;
641 var window = buffer.window;
642 if (elem.localName) {
643 switch (elem.localName.toLowerCase()) {
644 case "frame": case "iframe":
645 win = elem.contentWindow;
646 break;
647 case "math":
648 view_mathml_source(window, charset, elem);
649 return;
650 default:
651 throw new Error("Invalid browser element");
653 } else
654 win = elem;
655 win.focus();
657 var url_s = win.location.href;
658 if (url_s.substring (0,12) != "view-source:") {
659 try {
660 browser_object_follow(buffer, target, "view-source:" + url_s);
661 } catch(e) { dump_error(e); }
662 } else {
663 try {
664 browser_object_follow(buffer, target, url_s.replace(/^view-source\:/, ''));
665 } catch(e) { dump_error(e); }
669 function view_source (I, target) {
670 I.target = target;
671 if (target == null)
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,
689 $buffer = buffer,
690 $shell_command = command,
691 $shell_command_cwd = cwd);
694 provide("element");