2011-06-08

Implementing bookmarklets in JavaScript

Bookmarklets are little plugins for your browsers: JavaScript programs packed into javascript: URLs that you add to your bookmarks and start by clicking on them. They perform an operation on the currently open web page such as submitting it to Twitter. There are even bookmarklets that transform the current web page, for example, to add icons that, when clicked, add an event to Google Calendar. A separate post explains what bookmarklets are in more detail. This post tells you how to implement bookmarklets. It presents techniques, tools, and patterns for doing so.

Preparation and techniques

Firebug: helps you with trying out your JavaScript program in the same environment that the bookmark will eventually run in. Just go to an interesting website and run your code snippets in its shell. Its multi-line code editor is very helpful in this regard: To enable it, click the expansion button in the single-line console. Furthermore, Firebug lets you copy the current code to the clipboard, as a bookmarklet link. This link is crude, but it works. We’ll later see, how it can be made more compact.

Don’t pollute the global namespace: You don’t want to mess up the environment of the web site “in which” your bookmarklet is executed. For example, by using an Immediately Invoked Function Expression (IIFE) the variable t won’t become global, below.

    (function() {
        var t=document.title;
        console.log("Title: "+t);
    }())
Develop in strict mode: If you switch on strict mode, JavaScript warns you about more potential problems. Later on, you can switch it off so that the bookmarklet URL is more compact, but for development, it is very helpful. For example, the code below inadvertently creates a global variable, because a var has been forgotten. The output is abc.
    (function() {
        t = "abc";
    }());
    console.log(t);
Strict mode, however warns you that there is a problem:
    (function() {
        "use strict";
        t = "abc";
    }());
Result: ReferenceError: assignment to undeclared variable t

Finish with undefined: If you don’t return (or finish with!) undefined, the result replaces the current web page. [Note: Webkit browsers such as Chrome and Safari never replace a page, only non-Webkit browsers such as Firefox do.]

Load a script: The following code shows you how to load a script (with a library etc.). It creates a <script> element, adds the attributes src and onload and inserts it directly after the <head> element. The insertion causes the script to load and once loading is finished, the function pointed to by onload will be called.
    var d = document;
    var s = d.createElement('script');
    s.src='https://ajax.googleapis.com/.../jquery.min.js';
    s.onload = function() {...};
    d.getElementsByTagName('head')[0].appendChild(s);
Caveat: A bookmarklet is subject to the same restrictions as the page on which it is executed. Thus, invoking a bookmarklet on an online page cannot load files from your local hard drive. This is unfortunate, because it would allow you to put most of the code of a bookmarklet into an (offline-accessible) separate file.

Collect input

This section describes how to collect input from the currently shown web page.

Parse HTML manually: Standard DOM provides several methods that let you retrieve HTML elements from the current page.

  • getElementsByClassName()
  • getElementsByName() [attribute name]
  • getElementsByTagName()
  • getElementById()
Example:
    [].forEach.call(
        document.getElementsByTagName("a"),
        function(x) { console.log(x); });
forEach() is an array method (available in ECMAScript 5). By using call(), you can apply it to the non-array returned by the DOM. The DOM frequently returns array-like objects that look like arrays, but are not.

Using jQuery: jQuery is very helpful for extracting information from a web page. But you want to avoid the jQuery instance that you load from clashing with a jQuery instance that the page uses. The following code shows you how to do that.

    jQuery.noConflict(true)(function($) {
        // code using jQuery
    });
This reverts everything to the way it was before loading jQuery. The true argument means that in addition to the global variable $, jQuery will also be reverted to is prior value (if any). The jQuery web site has more information on jQuery.noConflict().

Transform HTML with JQuery: The following code snippet prepends the text "BLA: " to each <h3> tag in the current web page. You can run it in Firefox on a web page that has many of those tags (for example: spiegel.de).

    var d = document;
    var s = d.createElement('script');
    s.src='https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js';
    s.onload = function() {
        jQuery.noConflict(true)(function($){
            $("h3").each(function(i,e) {
                e.innerHTML = "BLA: "+$(e).text();
            });
        });
    };
    d.getElementsByTagName('head')[0].appendChild(s);

Producing output

This section describes how to show the results of your bookmarklet. You always have alert() or prompt() (which allows you to show a text to be copied to the user), but it is usually better present your results via HTML.

Show output in the current tab:

    document.body.innerHTML="<h1>Hi!</h1> Test!";

Show output in a new tab:

    var d=window.open().document;
    d.write("<html><body><h1>Hi!</h1> Test!</body></html>");
    d.close();

Minify the bookmarklet code

Yahoo has implemented the YUI Compressor, a tool that minifies your bookmarklet code (makes it as small as possible, without changing what it does). YUI Compressor can be downloaded as a JAR file which makes it easy to install.
  • Variable names can be minified.
  • Property names cannot be minified.
Example interaction:
    $ java -jar yuicompressor-2.4.6.jar copy_url.js
The following code has been minified by YUI Compressor.
javascript:(function(){var e=window.open().document;var c=(window.getSelection?""+window.getSelection():"").replace("<","&lt;");var b={h:document.location.href,t:document.title,st:(c?"\n\n"+c:c),sh:(c?"<br>"+c:c)};e.write(("<html><head><title>Copy link: {t}</title></head><body><a href='{h}'>{t}</a>{sh}<br><br><textarea id='text' cols='80' rows='10'>{t} {h}\n\n&lt;a href='{h}'&gt;{t}&lt;/a&gt;{st}</textarea></body></html>").replace(/{([a-z]+)}/g,function(d,a){return b[a]}));e.close()}())
The original looks as follows.
    (function() {
        var d=window.open().document;
        var s=(window.getSelection?""+window.getSelection():"")
              .replace("<","&lt;");
        var a={
            h: document.location.href,
            t: document.title,
            st: (s ? "\n\n"+s : s), // selection as text
            sh: (s ? "<br>"+s : s)  // selection as HTML
        };
        d.write(
            ("<html><head><title>Copy link: {t}</title></head><body>"
            +"<a href='{h}'>{t}</a>{sh}<br><br>"
            +"<textarea id='text' cols='80' rows='10'>"
            +"{t} {h}"
            +"\n\n&lt;a href='{h}'&gt;{t}&lt;/a&gt;"
            +"{st}"
            +"</textarea>"
            +"</body></html>"
            ).replace(/{([a-z]+)}/g, function(g0,g1){return a[g1]})
        );
        d.close();
    }())
This code is the “copy link” bookmarklet, which uses two notable techniques:
  • Mini-templating: The HTML in the output window is produced by inserting the property values of a into a text string, via the replace() method. Embedded references to the property values look like this: {t} refers to property t.
  • Doing something with the currently selected text: window.getSelection() is only used if the browser has implemented it.

Escaping

  • The link: Most modern browsers don’t place any limits on the characters of a javascript: URL. If you want to make sure that it works in old browsers, you can URL-encode the “body” of a bookmarklet:
        > "javascript:"+encodeURIComponent('alert("hello: world/universe")')
        'javascript:alert(%22hello%3A%20world%2Funiverse%22)'
    
    Try out the link inside the single quotes in a browser, it correctly brings up an alert.
  • The link in HTML: You face a different problem if you make the bookmarklet URL the value of an attribute in HTML. With URL-encoding nothing needs to be done, but if you use unencoded JavaScript, you need to escape some characters that are illegal inside the quotes of an attribute value.
        <a href="alert(&quot;hello: world/universe&quot;)">link</a>
    
    The following table shows you what to escape:
    Escapeas
    <&lt;
    >&gt;
    "&quot;
    '&#x27;
  • Composing URLs with query parameters in a bookmarklet: use encodeURIComponent() for the parameter values.
        location.href='http://example.com/postLink?url='
            + encodeURIComponent(location.href);
    

Invoking a bookmarklet via a browser keyword

Browser keywords allow you to define a command with a single argument that can be entered in the address field. That command is translated to a URL which is visited. You can combine this mechanism with a bookmarklet, to receive data from the address field. Example: Create the following browser keyword.
  • Keyword name: js
  • Keyword URL: javascript:alert(%s)
Now you can type in the address bar:
    js 7*45
And “go” to the following URL:
    javascript:alert(7*45)
The blog post “Tip: use JavaScript as a calculator in Firefox and Chrome” has more details.

Update 2011-06-11

There is a great thread on ycombinator with comments on this post. Read it, lots of good stuff!

No comments: