Summary

This article looks at creating a bookmarklet to empower users who may benefit from an accessible style sheet, but prefer the option to use the style sheet defined by the author. The article examines how to load a style sheet into the head of the document, and general techniques to follow when creating a bookmarklet.

Author: Gez Lemon

Contents

Creating a link Element

A couple of weeks ago, I received an email from someone asking me how to use ECMAScript to create a bookmarklet to apply a user-defined style sheet to the current web page. This week, I've received a further three emails asking the same question (well, two people asking a question, and one person showing me what they had done). It's odd to receive four emails in a short space of time about the same topic, but as there seems to be a lot of interest in this area, I thought I'd share the answers here. The first three had managed to implement a bookmarklet that would use the user-defined style sheet in Internet explorer, but didn't work in Firefox, Mozilla, Opera, etc. Each used the following code:

javascript:void(document.createStyleSheet('http://domain.com/j.css'))

The createStyleSheet method is a Microsoft proprietary method, so would only work in Internet Explorer. The most recent email wasn't asking for help, as they had managed to get it working; they just wanted to know what I thought of the method they had used:

void(objCSS = document.createElement('link'));
void(objCSS.rel = 'stylesheet');
void(objCSS.href = 'http://domain.com/j.css');
void(objCSS.type = 'text/css');
void(document.body.appendChild(objCSS));

Although this technique works, it does so by adding a link element to the body section of the document, which is illegal according to the specification. The correct method would be to append the link in the head of the document.

var objHead = document.getElementsByTagName('head');
var objCSS = objHead[0].appendChild(document.createElement('link'));

Ensuring Success

It's good practice to ensure any references are successful before using them. Therefore, we should ensure the call to getElementsByTagName is successful before continuing. HTML documents served with a MIME type of text/html have an implicit head section, but XHTML documents served with a MIME type of application/xhtml+xml do not have an implicit head section. All documents should have an explicit head section, but we'll check for it just in case.

var objHead = document.getElementsByTagName('head');
if (objHead[0])
    var objCSS = objHead[0].appendChild(document.createElement('link'));

Avoid Loading Multiple Links

Another thing we want to avoid, is loading several references to the same style sheet in the document. This can be achieved by giving the link element a unique identifier through the id property. We can then check for the presence of the id before applying the style sheet.

if (!document.getElementById('someuniqueid'))
{
  var objHead = document.getElementsByTagName('head');
  if (objHead[0])
  {
    var objCSS = objHead[0].appendChild(document.createElement('link'));
    objCSS.id = 'someuniqueid';
    objCSS.rel = 'stylesheet';
    objCSS.href = 'http://domain.com/j.css';
    objCSS.type = 'text/css';
  }
}

Anonymous Functions

In the original example that loaded the link element in the body of the document, each statement has been cast to void. This is because all statements in ECMAScript return a value, which must be handled. In the absence of a handler, a new page will be loaded to catch the return value. Obviously, this isn't the desired result, so statements are cast to void so that they don't return a value. Authors can also use var to catch statements, but this would involve using redundant variables for some statements. A much better method is to wrap the code in an anonymous function.

javascript:(function(){
// ECMAScript statements go here
})()

So our complete bookmarklet would look something like this:

javascript:(function(){
if (!document.getElementById('someuniqueid'))
{
  var objHead = document.getElementsByTagName('head');
  if (objHead[0])
  {
    var objCSS = objHead[0].appendChild(document.createElement('link'));
    objCSS.id = 'someuniqueid';
    objCSS.rel = 'stylesheet';
    objCSS.href = 'http://domain.com/j.css';
    objCSS.type = 'text/css';
  }
}
})()

MIME Type Considerations

Thank you to Aankhen for pointing out that documents delivered as application/xhtml+xml should use createElementNS rather than createElement. For documents delivered as text/html, we should use the createElement method, but should be using createElementNS for documents delivered as application/xhtml+xml. When a document is delivered as text/html the elements are stored in uppercase in the DOM, and when delivered as application/xhtml+xml, elements are stored in lowercase in the DOM. This gives us a simple solution to determine which method we should use to add elements.

javascript:(function(){
if (!document.getElementById('someuniqueid'))
{
  var objHead = document.getElementsByTagName('head');
  if (objHead[0])
  {
    if (document.createElementNS && objHead[0].tagName == 'head')
        var objCSS = objHead[0].appendChild(document.createElementNS('http://www.w3.org/1999/xhtml', 'link'));
    else
        var objCSS = objHead[0].appendChild(document.createElement('link'));
    objCSS.id = 'someuniqueid';
    objCSS.rel = 'stylesheet';
    objCSS.href = 'http://domain.com/j.css';
    objCSS.type = 'text/css';
  }
}
})()

URL Encoding

Before we can use the code for a bookmarklet, we need to remove all unnecessary white-space, and ensure it's encoded correctly so it can be used in a URL. All statements in ECMAScript should contain a semicolon. The reason for this is that you cannot rely on carriage returns to indicate the end of the statement. When we remove the white-space, the interpreter couldn't determine for definite the end of one statement and the start of the next. Semicolons aren't required at the end of a construct (such as a condition or a loop), as the structure of the construct helps the interpreter understand how to handle it. For example, a single statement doesn't require braces for it to belong to the construct, as the construct expects a single statement. Multiple statements are contained in curly braces, to force the statements to belong to the construct. Adding a semicolon to the end of a construct is a common mistake, and makes the interpreter think that the construct contains no statements. Consider the following example, which has a single statement contained in curly braces for clarity.

if (objHead[0]);
{
  var objCSS = objHead[0].appendChild(document.createElement('link'));
}

The if statement has an erroneous semicolon at the end, illustrating the difference between a syntax error and a semantic error; it makes sense to the interpreter, but probably isn't what the author intended. The interpreter will determine from the code that if objHead[0] exists, it should do nothing (as there's an empty statement associated with that construct by placing a semicolon at the end), and execute the next block regardless.

Having removed all white-space, the next stage is to URL encode the script so that it works as a bookmarklet. This ensures that characters such as spaces (%20), arithmetic operators (such as a + sign (%2B)), and ampersands (%26) are encoded correctly. The following is the complete bookmarklet that loads a style sheet that could be used to make content more accessible.

The Final Bookmarklet

The style sheet needs some work for Internet Explorer, but gives an idea of the principle behind the technique.

Category: Scripting.

Comments

  1. [accessible-stylesheet-bookmarklet.php#comment1]

    This can be achieved by giving the link element a unique identifier through the id property

    It is an id attribute - not a property!

    Posted by Attributes on

  2. [accessible-stylesheet-bookmarklet.php#comment2]

    It is an id attribute - not a property!

    It's an attribute in markup, and a property of an object in the DOM. It's similar to class diagrams in UML: The data of an object is defined by its attributes, and the ability of an object is defined by its operations. When an object is instantiated, the data is stored in properties, and the operations are performed by methods of the object.

    Posted by Gez on

  3. [accessible-stylesheet-bookmarklet.php#comment3]

    I believe you might have missed out one thing: accounting for createElementNS() vs. createElement. You've mentioned application/xhtml+xml when checking for the head element, so it seems this might be a small omission... right?

    Posted by Aankhen on

  4. [accessible-stylesheet-bookmarklet.php#comment4]

    I believe you might have missed out one thing: accounting for createElementNS() vs. createElement.

    Thank you, Aankhen. I've got to go to work now, but I'll include it when I get back. Thank you for pointing it out *smile*

    Posted by Gez on

  5. [accessible-stylesheet-bookmarklet.php#comment6]

    Very nice!

    I use a data: URL, like this:

    objCSS.href = 'data:text/css,*{-moz-outline:1px dotted}';

    Great for quick debugging. *smile*

    Posted by zcorpan on

  6. [accessible-stylesheet-bookmarklet.php#comment7]

    A much better method is to wrap the code in an anonymous function.

    yes, but an anonymous function is still a statement and will return a value. best still to use the void operator to avoid catching a possible return value (and loading a new page).

    All statements in ECMAScript should contain a semicolon.

    it's pedantic, but i'd like to see it on the end of the bookmarklet too.

    although still a working draft (as of 30th june 2005), it's worth noting that the w3's client-side scripting techniques for web content accessibility guidelines 2.0[1] is against using the javascript pseudo protocol (:javascript). they currently suggest a hook via dom events, e.g. the html attribute onclick. that brings up another messy question of device-dependent event attributes, and that's as far down this rabbit hole as i'd like to go. *smile*

    - p

    --
    1.
    http://www.w3.org/TR/WCAG20-SCRIPT-TECHS/#js-uri

    Posted by Paul Arzul on

  7. [accessible-stylesheet-bookmarklet.php#comment8]

    yes, but an anonymous function is still a statement and will return a value. best still to use the void operator to avoid catching a possible return value (and loading a new page).

    That's good advice.

    although still a working draft (as of 30th june 2005), it's worth noting that the w3's client-side scripting techniques for web content accessibility guidelines 2.0[1] is against using the javascript pseudo protocol (:javascript). they currently suggest a hook via dom events, e.g. the html attribute onclick. that brings up another messy question of device-dependent event attributes, and that's as far down this rabbit hole as i'd like to go. *smile*

    A bookmarklet is stored in a user's bookmarks list (in their browser). There is no DOM at that point; the DOM is only available from a web page loaded in the browser. In the absence of any other method of notifying the user agent that the bookmark isn't a URL, but a script that should be run, the javascript pseudo protocol is the only way of handling the bookmarklet. Also, the colon is placed after the protocol, not before it.

    Posted by Gez on

  8. [accessible-stylesheet-bookmarklet.php#comment9]

    This is great. Using this, the css stylesheet can be set at runtime. I tried this before, but I forgot to add the link object to the head.

    Thanks for writing this page!

    Posted by R. Kippen on

Comments are closed for this entry.