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 - Ensuring Success
- Avoid Loading Multiple Links
- Anonymous Functions
- MIME Type Considerations
- URL Encoding
- The Final Bookmarklet
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.
[accessible-stylesheet-bookmarklet.php#comment1]
It is an id attribute - not a property!
Posted by Attributes on
[accessible-stylesheet-bookmarklet.php#comment2]
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
[accessible-stylesheet-bookmarklet.php#comment3]
I believe you might have missed out one thing: accounting for
createElementNS()
vs.createElement
. You've mentionedapplication/xhtml+xml
when checking for thehead
element, so it seems this might be a small omission... right?Posted by Aankhen on
[accessible-stylesheet-bookmarklet.php#comment4]
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
Posted by Gez on
[accessible-stylesheet-bookmarklet.php#comment5]
I've added a section called MIME Type Considerations. Thanks again, Aankhen.
Best regards,
Posted by Gez on
[accessible-stylesheet-bookmarklet.php#comment6]
Very nice!
I use a data: URL, like this:
Great for quick debugging.
Posted by zcorpan on
[accessible-stylesheet-bookmarklet.php#comment7]
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).
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.
- p
--
1.
http://www.w3.org/TR/WCAG20-SCRIPT-TECHS/#js-uri
Posted by Paul Arzul on
[accessible-stylesheet-bookmarklet.php#comment8]
That's good advice.
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
[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