Make it easier to remove mode_line_adder functions from hooks
[conkeror.git] / modules / element.js
blob992e4b5629f247e173bbc4eb0a3094b4d7d9dd6e
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 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(
30                 $buffer = buf,
31                 $prompt = prompt,
32                 $hint_xpath_expression = xpath_expression);
33             yield co_return(result);
34         };
35     }
36     var base_obj = browser_object_classes[name];
37     if (base_obj == null)
38         base_obj = browser_object_classes[name] = {};
39     var obj;
40     if (arguments.$action) {
41         name = name + "/" + arguments.$action;
42         obj = browser_object_classes[name];
43         if (obj == null)
44             obj = browser_object_classes[name] = {__proto__: base_obj};
45     } else
46         obj = 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;
53     interactive(
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; },
58         $prefix = true);
61 define_browser_object_class("images",
62                             $label = "image",
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)
70     {
71         // only one frame (the top-level one), no need to use the hints system
72         yield co_return(buf.top_frame);
73     }
75     var result = yield buf.window.minibuffer.read_hinted_element(
76         $buffer = buf,
77         $prompt = prompt,
78         $hint_xpath_expression = "//iframe | //frame | //xhtml:iframe | //xhtml:frame");
79     yield co_return(result);
80 });
82 define_browser_object_class(
83     "links", $label = "link",
84     $xpath_expression =
85         "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or " +
86         "@role='link'] | " +
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);
100                             });
102 define_variable(
103     "default_browser_object_classes",
104     {
105         follow: "links",
106         follow_top: "frames",
107         focus: "frames",
108         save: "links",
109         copy: "links",
110         view_source: "frames",
111         bookmark: "frames",
112         save_page: "frames",
113         save_page_complete: "top",
114         save_page_as_text: "frames",
115         default: "links"
116     },
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) {
121     var cls =
122         this._browser_object_class ||
123         this.get("default_browser_object_classes")[action_name] ||
124         this.get("default_browser_object_classes")["default"];
125     return cls;
128 function lookup_browser_object_class(class_name, action) {
129     var obj;
130     if (action != null) {
131         obj = browser_object_classes[class_name + "/" + action];
132         if (obj)
133             return obj;
134     }
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;
145     if (target != null)
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)
156         return true;
157     if (elem instanceof Ci.nsIDOMWindow)
158         return true;
159     return false;
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.
166  */
167 function browser_set_element_focus(buffer, elem, prevent_scroll) {
168     if (!is_dom_node_or_window(elem))
169         return;
171     buffer.last_user_input_received = Date.now();
172     if (prevent_scroll)
173         set_focus_no_scroll(buffer.window, elem);
174     else
175         elem.focus();
178 function browser_element_focus(buffer, elem)
180     if (!is_dom_node_or_window(elem))
181         return;
183     if (elem instanceof Ci.nsIDOMXULTextBoxElement)  {
184         // Focus the input field instead
185         elem = elem.wrappedJSObject.inputField;
186     }
188     browser_set_element_focus(buffer, elem);
189     if (elem instanceof Ci.nsIDOMWindow) {
190         return;
191     }
192     // If it is not a window, it must be an HTML element
193     var x = 0;
194     var y = 0;
195     if (elem instanceof Ci.nsIDOMHTMLFrameElement || elem instanceof Ci.nsIDOMHTMLIFrameElement) {
196         elem.contentWindow.focus();
197         return;
198     }
199     if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
200         var coords = elem.getAttribute("coords").split(",");
201         x = Number(coords[0]);
202         y = Number(coords[1]);
203     }
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) {
226         var x = 1, y = 1;
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;
232             }
233         }
234         browser_follow_link_with_click(buffer, elem, x, y);
235         return;
236     }
238     var spec = element_get_load_spec(elem);
239     if (spec == null) {
240         throw interactive_error("Element has no associated URL");
241         return;
242     }
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");
247     }
249     switch (target) {
250     case FOLLOW_CURRENT_FRAME:
251         var current_frame = load_spec_source_frame(spec);
252         if (current_frame && current_frame != buffer.top_frame) {
253             var target_obj = get_web_navigation_for_frame(current_frame);
254             apply_load_spec(target_obj, spec);
255             break;
256         }
257     case FOLLOW_DEFAULT:
258     case FOLLOW_TOP_FRAME:
259     case OPEN_CURRENT_BUFFER:
260         buffer.load(spec);
261         break;
262     case OPEN_NEW_WINDOW:
263     case OPEN_NEW_BUFFER:
264     case OPEN_NEW_BUFFER_BACKGROUND:
265         create_buffer(buffer.window,
266                       buffer_creator(content_buffer,
267                                      $load = spec,
268                                      $configuration = buffer.configuration),
269                       target);
270     }
274  * Follow a link-like element by generating fake mouse events.
275  */
276 function browser_follow_link_with_click(buffer, elem, x, y) {
277     var doc = elem.ownerDocument;
278     var view = doc.defaultView;
280     var evt = doc.createEvent("MouseEvents");
281     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
282                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
283     elem.dispatchEvent(evt);
285     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
286                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
287     elem.dispatchEvent(evt);
290 function element_get_load_spec(elem) {
292     if (is_load_spec(elem))
293         return elem;
295     var spec = null;
297     if (elem instanceof Ci.nsIDOMWindow)
298         spec = load_spec({document: elem.document});
300     else if (elem instanceof Ci.nsIDOMHTMLFrameElement ||
301              elem instanceof Ci.nsIDOMHTMLIFrameElement)
302         spec = load_spec({document: elem.contentDocument});
304     else {
305         var url = null;
306         var title = null;
308         if (elem instanceof Ci.nsIDOMHTMLAnchorElement ||
309             elem instanceof Ci.nsIDOMHTMLAreaElement ||
310             elem instanceof Ci.nsIDOMHTMLLinkElement) {
311             if (!elem.hasAttribute("href"))
312                 return null; // nothing can be done, as no nesting within these elements is allowed
313             url = elem.href;
314             title = elem.title || elem.textContent;
315         }
316         else if (elem instanceof Ci.nsIDOMHTMLImageElement) {
317             url = elem.src;
318             title = elem.title || elem.alt;
319         }
320         else {
321             var node = elem;
322             while (node && !(node instanceof Ci.nsIDOMHTMLAnchorElement))
323                 node = node.parentNode;
324             if (node && !node.hasAttribute("href"))
325                 node = null;
326             else
327                 url = node.href;
328             if (!node) {
329                 // Try simple XLink
330                 node = elem;
331                 while (node) {
332                     if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
333                         url = linkNode.getAttributeNS(XLINK_NS, "href");
334                         break;
335                     }
336                     node = node.parentNode;
337                 }
338                 if (url)
339                     url = makeURLAbsolute(node.baseURI, url);
340                 title = node.title || node.textContent;
341             }
342         }
343         if (url && url.length > 0) {
344             if (title && title.length == 0)
345                 title = null;
346             spec = load_spec({uri: url, source_frame: elem.ownerDocument.defaultView, title: title});
347         }
348     }
349     return spec;
352 interactive("follow", null, function (I) {
353     var target = I.browse_target("follow");
354     var element = yield I.read_browser_object("follow", "Follow", target);
355     browser_element_follow(I.buffer, target, element);
358 interactive("follow-top", null, function (I) {
359     var target = I.browse_target("follow-top");
360     var element = yield I.read_browser_object("follow_top", "Follow", target);
361     browser_element_follow(I.buffer, target, element);
364 interactive("focus", null, function (I) {
365     var element = yield I.read_browser_object("focus", "Focus");
366     browser_element_focus(I.buffer, element);
369 function element_get_load_target_label(element) {
370     if (element instanceof Ci.nsIDOMWindow)
371         return "page";
372     if (element instanceof Ci.nsIDOMHTMLFrameElement)
373         return "frame";
374     if (element instanceof Ci.nsIDOMHTMLIFrameElement)
375         return "iframe";
376     return null;
379 function element_get_operation_label(element, op_name, suffix) {
380     var target_label = element_get_load_target_label(element);
381     if (target_label != null)
382         target_label = " " + target_label;
383     else
384         target_label = "";
386     if (suffix != null)
387         suffix = " " + suffix;
388     else
389         suffix = "";
391     return op_name + target_label + suffix + ":";
394 interactive("save", null, function (I) {
395     var element = yield I.read_browser_object("save", "Save");
397     var spec = element_get_load_spec(element);
398     if (spec == null)
399         throw interactive_error("Element has no associated URI");
401     var panel;
402     panel = create_info_panel(I.window, "download-panel",
403                               [["downloading",
404                                 element_get_operation_label(element, "Saving"),
405                                 load_spec_uri_string(spec)],
406                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
408     try {
409         var file = yield I.minibuffer.read_file_check_overwrite(
410             $prompt = "Save as:",
411             $initial_value = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer),
412             $history = "save");
414     } finally {
415         panel.destroy();
416     }
418     save_uri(spec, file,
419              $buffer = I.buffer,
420              $use_cache = false);
423 function browser_element_copy(buffer, elem)
425     var spec = element_get_load_spec(elem);
426     var text = null;
427     if (spec)
428         text = load_spec_uri_string(spec);
429     else  {
430         if (!(elem instanceof Ci.nsIDOMNode))
431             throw interactive_error("Element has no associated text to copy.");
432         switch (elem.localName) {
433         case "INPUT":
434         case "TEXTAREA":
435             text = elem.value;
436             break;
437         case "SELECT":
438             if (elem.selectedIndex >= 0)
439                 text = elem.item(elem.selectedIndex).text;
440             break;
441         default:
442             text = elem.textContent;
443             break;
444         }
445     }
446     browser_set_element_focus(buffer, elem);
447     writeToClipboard (text);
448     buffer.window.minibuffer.message ("Copied: " + text);
452 interactive("copy", null, function (I) {
453     var element = yield I.read_browser_object("copy", "Copy");
454     browser_element_copy(I.buffer, element);
457 var view_source_use_external_editor = false, view_source_function = null;
458 function browser_element_view_source(buffer, target, elem)
460     if (view_source_use_external_editor || view_source_function)
461     {
462         var spec = element_get_load_spec(elem);
463         if (spec == null) {
464             throw interactive_error("Element has no associated URL");
465             return;
466         }
468         let [file, temp] = yield download_as_temporary(spec,
469                                                        $buffer = buffer,
470                                                        $action = "View source");
471         if (view_source_use_external_editor)
472             yield open_file_with_external_editor(file, $temporary = temp);
473         else
474             yield view_source_function(file, $temporary = temp);
475         return;
476     }
478     var win = null;
479     var window = buffer.window;
480     if (elem.localName) {
481         switch (elem.localName.toLowerCase()) {
482         case "frame": case "iframe":
483             win = elem.contentWindow;
484             break;
485         case "math":
486             view_mathml_source (window, charset, elem);
487             return;
488         default:
489             throw new Error("Invalid browser element");
490         }
491     } else
492         win = elem;
493     win.focus();
495     var url_s = win.location.href;
496     if (url_s.substring (0,12) != "view-source:") {
497         try {
498             open_in_browser(buffer, target, "view-source:" + url_s);
499         } catch(e) { dump_error(e); }
500     } else {
501         window.minibuffer.message ("Already viewing source");
502     }
505 interactive("view-source", null, function (I) {
506     var target = I.browse_target("follow");
507     var element = yield I.read_browser_object("view_source", "View source", target);
508     yield browser_element_view_source(I.buffer, target, element);
511 interactive("shell-command-on-url", null, function (I) {
512     var cwd = I.cwd;
513     var element = yield I.read_browser_object("shell_command_url", "URL shell command");
514     var spec = element_get_load_spec(element);
515     if (spec == null)
516         throw interactive_error("Unable to obtain URI from element");
518     var uri = load_spec_uri_string(spec);
520     var panel;
521     panel = create_info_panel(I.window, "download-panel",
522                               [["downloading",
523                                 element_get_operation_label(element, "Running on", "URI"),
524                                 load_spec_uri_string(spec)],
525                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
527     try {
528         var cmd = yield I.minibuffer.read_shell_command(
529             $cwd = cwd,
530             $initial_value = load_spec_default_shell_command(spec));
531     } finally {
532         panel.destroy();
533     }
535     shell_command_with_argument_blind(cmd, uri, $cwd = cwd);
538 function browser_element_shell_command(buffer, elem, command) {
539     var spec = element_get_load_spec(elem);
540     if (spec == null) {
541         throw interactive_error("Element has no associated URL");
542         return;
543     }
544     yield download_as_temporary(spec,
545                                 $buffer = buffer,
546                                 $shell_command = command,
547                                 $shell_command_cwd = buffer.cwd);
550 interactive("shell-command-on-file", null, function (I) {
551     var cwd = I.cwd;
552     var element = yield I.read_browser_object("shell_command", "Shell command");
554     var spec = element_get_load_spec(element);
555     if (spec == null)
556         throw interactive_error("Unable to obtain URI from element");
558     var uri = load_spec_uri_string(spec);
560     var panel;
561     panel = create_info_panel(I.window, "download-panel",
562                               [["downloading",
563                                 element_get_operation_label(element, "Running on"),
564                                 load_spec_uri_string(spec)],
565                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
567     try {
569         var cmd = yield I.minibuffer.read_shell_command(
570             $cwd = cwd,
571             $initial_value = load_spec_default_shell_command(spec));
572     } finally {
573         panel.destroy();
574     }
576     /* FIXME: specify cwd as well */
577     yield browser_element_shell_command(I.buffer, element, cmd);
580 interactive("bookmark", null, function (I) {
581     var element = yield I.read_browser_object("bookmark", "Bookmark");
582     var spec = element_get_load_spec(element);
583     if (!spec)
584         throw interactive_error("Element has no associated URI");
585     var uri_string = load_spec_uri_string(spec);
586     var panel;
587     panel = create_info_panel(I.window, "bookmark-panel",
588                               [["bookmarking",
589                                 element_get_operation_label(element, "Bookmarking"),
590                                 uri_string]]);
591     try {
592         var title = yield I.minibuffer.read($prompt = "Bookmark with title:", $initial_value = load_spec_title(spec) || "");
593     } finally {
594         panel.destroy();
595     }
596     add_bookmark(uri_string, title);
597     I.minibuffer.message("Added bookmark: " + uri_string + " - " + title);
600 interactive("save-page", null, function (I) {
601     check_buffer(I.buffer, content_buffer);
602     var element = yield I.read_browser_object("save_page", "Save page");
603     var spec = element_get_load_spec(element);
604     if (!spec || !load_spec_document(spec))
605         throw interactive_error("Element is not associated with a document.");
606     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
608     var panel;
609     panel = create_info_panel(I.window, "download-panel",
610                               [["downloading",
611                                 element_get_operation_label(element, "Saving"),
612                                 load_spec_uri_string(spec)],
613                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
615     try {
616         var file = yield I.minibuffer.read_file_check_overwrite(
617             $prompt = "Save page as:",
618             $history = "save",
619             $initial_value = suggested_path);
620     } finally {
621         panel.destroy();
622     }
624     save_uri(spec, file, $buffer = I.buffer);
627 interactive("save-page-as-text", null, function (I) {
628     check_buffer(I.buffer, content_buffer);
629     var element = yield I.read_browser_object("save_page_as_text", "Save page as text");
630     var spec = element_get_load_spec(element);
631     var doc;
632     if (!spec || !(doc = load_spec_document(spec)))
633         throw interactive_error("Element is not associated with a document.");
634     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec, "txt"), I.buffer);
636     var panel;
637     panel = create_info_panel(I.window, "download-panel",
638                               [["downloading",
639                                 element_get_operation_label(element, "Saving", "as text"),
640                                 load_spec_uri_string(spec)],
641                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
643     try {
644         var file = yield I.minibuffer.read_file_check_overwrite(
645             $prompt = "Save page as text:",
646             $history = "save",
647             $initial_value = suggested_path);
648     } finally {
649         panel.destroy();
650     }
652     save_document_as_text(doc, file, $buffer = I.buffer);
655 interactive("save-page-complete", null, function (I) {
656     check_buffer(I.buffer, content_buffer);
657     var element = yield I.read_browser_object("save_page_complete", "Save page complete");
658     var spec = element_get_load_spec(element);
659     var doc;
660     if (!spec || !(doc = load_spec_document(spec)))
661         throw interactive_error("Element is not associated with a document.");
662     var suggested_path = suggest_save_path_from_file_name(suggest_file_name(spec), I.buffer);
664     var panel;
665     panel = create_info_panel(I.window, "download-panel",
666                               [["downloading",
667                                 element_get_operation_label(element, "Saving complete"),
668                                 load_spec_uri_string(spec)],
669                                ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
671     try {
672         var file = yield I.minibuffer.read_file_check_overwrite(
673             $prompt = "Save page complete:",
674             $history = "save",
675             $initial_value = suggested_path);
676         // FIXME: use proper read function
677         var dir = yield I.minibuffer.read_file(
678             $prompt = "Data Directory:",
679             $history = "save",
680             $initial_value = file.path + ".support");
681     } finally {
682         panel.destroy();
683     }
685     save_document_complete(doc, file, dir, $buffer = I.buffer);
688 default_browse_targets["view-as-mime-type"] = [FOLLOW_CURRENT_FRAME, OPEN_CURRENT_BUFFER,
689                                                OPEN_NEW_BUFFER, OPEN_NEW_WINDOW];
690 interactive("view-as-mime-type",
691             "Display a browser object in the browser using the specified MIME type.",
692             function (I) {
693                 var element = yield I.read_browser_object("view_as_mime_type", "View in browser as mime type");
694                 var spec = element_get_load_spec(element);
696                 var target = I.browse_target("view-as-mime-type");
698                 if (!spec)
699                     throw interactive_error("Element is not associated with a URI");
701                 if (!can_override_mime_type_for_uri(load_spec_uri(spec)))
702                     throw interactive_error("Overriding the MIME type is not currently supported for non-HTTP URLs.");
704                 var panel;
706                 var mime_type = load_spec_mime_type(spec);
707                 panel = create_info_panel(I.window, "download-panel",
708                                           [["downloading",
709                                             element_get_operation_label(element, "View in browser"),
710                                             load_spec_uri_string(spec)],
711                                            ["mime-type", "Mime type:", load_spec_mime_type(spec)]]);
714                 try {
715                     let suggested_type = mime_type;
716                     if (gecko_viewable_mime_type_list.indexOf(suggested_type) == -1)
717                         suggested_type = "text/plain";
718                     mime_type = yield I.minibuffer.read_gecko_viewable_mime_type(
719                         $prompt = "View internally as",
720                         $initial_value = suggested_type,
721                         $select);
722                     override_mime_type_for_next_load(load_spec_uri(spec), mime_type);
723                     browser_element_follow(I.buffer, target, spec);
724                 } finally {
725                     panel.destroy();
726                 }
727             });