Developer’s Guide
1. Setup and Configuration
Before you can use CSSUtilities you'll need to setup and configure it, and have a working knowledge of the different ways to get data from it. This part of the documentation will take you through that:
Setting it up
There are two script includes to put at the end of the
<head>
section —
the main CSSUtilities library, plus an additional
Selectors API:
<script type="text/javascript" src="CSSUtilities.js"></script>
<script type="text/javascript" src="Selector.js"></script>
The additional Selectors API provides functionality to browsers that don't support the querySelectorAll method (which is Opera 9, Firefox 1.5–3.0, Konqueror, IE6 and IE7), so if you don't include the additional library then CSSUtilities won't work in those browsers either. The library that's bundled by default is LlamaLab's Selector.js, because in my testing it gave the most consistently accurate information with a very small code size (Dean Edward's base2 and Robert Nyman's DOMAssistant gave equally good results, but they're larger script files, and CSSUtilities is not exactly tiny by itself!). But you can use a different Selectors API if you want; this might make sense if you were already including the codebase for another library anyway.
The library scripts go in the <head>
section so you can
initialize as soon as possible during page load; and they go at
the end of the <head>
so that
all the page's stylesheets come before it.
But as we'll see, most of the actual scripting
you do with the library will need to go at the end of the
<body>
— or be deferred until page-load —
because such scripting will generally also refer to
elements in the DOM.
Getting data
The script has two primary execution modes (asynchronous or synchronous), and two implementation modes ("browser" mode or "author" mode). These modes and their interaction have specific implications for how you use the library:
So with that in mind, there are four primary design patterns you can choose from (two for each execution mode):
Pattern A/1: Asynchronous with manual init
With this pattern you call the initialization method asynchronously, passing a callback for when it's complete. Within the callback you can then use the data methods synchronously like normal:
<head xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
... style sheets ...
<script type="text/javascript" src="CSSUtilities.js"></script>
<script type="text/javascript" src="Selector.js"></script>
</head>
<body>
... page content ...
<script type="text/javascript">
//configure
CSSUtilities.define('async', true);
//initialize with callback
CSSUtilities.init(function()
{
//synchronously call data method
var rules = CSSUtilities.getCSSRules('#foo');
//work with the returned data
alert(rules);
});
</script>
</body>
Pattern A/2: Asynchronous with auto-init
With this pattern you call the data methods asynchronously without first initializing, and each of them has a completion callback of its own, which is fired once auto-init (if necessary) and the data-retrieval is complete. Within each callback the response data is passed in as an argument:
<head xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
... style sheets ...
<script type="text/javascript" src="CSSUtilities.js"></script>
<script type="text/javascript" src="Selector.js"></script>
</head>
<body>
... page content ...
<script type="text/javascript">
//configure
CSSUtilities.define('async', true);
//asynchronously call data method with callback
CSSUtilities.getCSSRules('#foo', function(rules)
{
//work with the response data
alert(rules);
});
</script>
</body>
Every data method has its own completion callback, and it's always the last argument.
Pattern S/1: Synchronous with manual init
With this pattern you call the intialization method synchronously, and then use the data methods synchronously once that's complete; since all the calls are separate you can initialize early on, and thereby reduce the impact of synchronous network use:
<head xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
... style sheets ...
<script type="text/javascript" src="CSSUtilities.js"></script>
<script type="text/javascript" src="Selector.js"></script>
<script type="text/javascript">
//configure
CSSUtilities.define('async', false);
//initialize
CSSUtilities.init();
</script>
</head>
<body>
... page content ...
<script type="text/javascript">
//synchronously call data method
var rules = CSSUtilities.getCSSRules('#foo');
//work with the returned data
alert(rules);
</script>
</body>
Pattern S/2: Synchronous with auto-init
With this pattern you call the data methods synchronously, without first initializing, and this triggers a synchronous auto-init (if necessary) before retrieving and returning the data; using this approach will probably cause an obvious pause the first time you do it:
<head xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
... style sheets ...
<script type="text/javascript" src="CSSUtilities.js"></script>
<script type="text/javascript" src="Selector.js"></script>
</head>
<body>
... page content ...
<script type="text/javascript">
//configure
CSSUtilities.define('async', false);
//synchronously call data method
var rules = CSSUtilities.getCSSRules('#foo');
//work with the returned data
alert(rules);
</script>
</body>
Configuration options
CSSUtilities has a small number of global configuration options, all of which are defined before initialising the library (the design patterns shown above also illustrate where this happens).
Options are defined using the
define
method,
which takes two required arguments, for the name
(always a String
) and the value (some String
some Boolean
):
CSSUtilities.define('mode', 'author');
CSSUtilities.define('async', true);
CSSUtilities.define('attributes', false);
(For one of the settings there's an optional third argument; for details see :: external Selectors API settings.)
The define()
method includes comprehensive validation and
error reporting, and any invalid definitions will throw a fatal error
that prevents any further use of the library;
for a list of possible errors see ::
Error messages.
Here's a reference of all the available options, their meanings and possible values:
"mode"
CSSUtilities.define("mode", "browser");
- implementation mode [REQUIRED String]
-
A
String
that specifies one of two internal parser implementations — either"browser"
or"author"
. The default value if undefined is"browser"
.This variable fundamentally changes how the library gets its CSS data, and therefore what data it returns.
In
"browser"
mode the library uses thedocument.styleSheets
collection to get its data, which means that what we get is subject to each browser's implementation: it only includes what the browser understands and supports, and the properties will be normalized in various ways (for example, among other things: Firefox normalizes hex colors to RGB, Opera normalizes colors to 6-digit hex, Internet Explorer splits-up margin shorthands into their component longhand properties).In
"author"
mode, the library loads and parses every stylesheet as plain text, which means that each browser should return the same data, whether or not it actually supports everything listed, and all the properties will be returned exactly as they were specified in the stylesheet — with no normalization of units or value-types, and everything listed in the same syntax it was defined with (except for properties defined in HTMLstyle
attributes — these properties cannot be returned independently of browser normalization).So as a consequence of each approach,
"author"
mode has to make a number of Ajax requests in addition to the normal page load, where"browser"
mode already has the data it needs. This difference may in turn affect your choice of asynchronous or synchronous execution mode; for more about that see :: Getting data.Which mode you choose will obviously depend on what you need, and there are no hard or fast rules about which is better. But broadly speaking,
"author"
mode may be suitable for testing and development tools, while"browser"
mode may be more appropriate for real-world applications.You should also note that only
"browser"
mode has full access to dynamically-generated styles. In"author"
mode, the library won't see any rules created usingaddRule
orinsertRule
; but all browsers except IE will see rules created as text-nodes inside a<style>
element, and all browsers will see properties added to.style
(subject to browser support for the individual properties). Naturally in all cases, only styles created before initialization will be picked-up, though you can of course re-initialize at any time.Some of the data methods are affected by your choice of implementation, for example, where particular options are only relevant in one mode or the other; such details are documented wherever applicable.
"async"
CSSUtilities.define("async", false);
- asynchronous execution mode [REQUIRED Boolean]
-
A
Boolean
that defines whether the library's initialization should be asynchronous (true
) or synchronous (false
). The default value if undefined isfalse
(synchronous execution).The difference in grammar is simply that with synchronous execution you can query the library using all synchronous function calls, whereas with asynchronous execution you have to use callbacks for some.
The difference in practical terms though is that the execution mode primarily determines how Ajax requests are made — correspondingly synchronous or asynchronous — which in turn has an impact on usability, because synchronous requests lock-up the browser thread until they're complete (the impact depending on when you initialize and how many requests are made, factors which are affected by your choice of implementation mode).
This may sound a little convoluted at first, but once you get used to the design patterns I hope you'll appreciate the flexibility they afford; for details please see :: Getting data.
"page"
CSSUtilities.define("page", document);
- context document [REQUIRED Document]
-
A
Document
that defines the document object (DOM) you wish to inspect — containing the test elements and stylesheets you're interested in. The default value if undefined isdocument
.Generally you won't need to change this, but it's there so that you can configure the library to work with virtualized or remote documents, such as the
responseXML
of an Ajax request, or thecontentDocument
of an<iframe>
.If you do specify a different document, you will often need to specify a matching base URI for stylesheet paths as well.
If you're also defining
"api"
then you must define this option first.
"base"
CSSUtilities.define("base", document.location.href);
- base URI for stylesheet paths [REQUIRED String]
-
A
String
that defines a URI base for qualifying stylesheet paths. The default value if undefined is the URL of the document specified in thepage
variable, which in turn defaults todocument
, and so the default value forbase
isdocument.location.href
.Most of the time you won't need to change this, but it's there so that the DOM you're inspecting can be at a different location than the stylesheets, or virtualized entirely such that it has no addressable location.
For example, in some versions of CodeBurner the DOM inspection works by loading a remote page using Ajax and then writing the returned HTML into an
<iframe>
, in order to create a document object for testing. But then of course, the document'slocation
no longer matches the URL of the original page, so any relative paths in<link href>
stylesheet addresses could no longer be resolved. So we set the"base"
variable to match the remote page's original URL, and then all the stylesheet URLs can be resolved against that!The value of
base
must be an absolute URL. If you specify a folder root then you must include the trailing slash.If you're also defining
"api"
then you must define this option first.
"attributes"
CSSUtilities.define("attributes", true);
- parse HTML style attributes [REQUIRED Boolean]
-
A
Boolean
that defines whether to include the data from HTMLstyle
attributes. The default value if undefined istrue
.Any such CSS properties defined for an element will be organised into a single rule object, with meta-data that reflects their origin — such as higher specificity, and a selector with the special value
""
(empty string).Other things being equal, there's no reason to change this option — disabling it might mean that some data sets are incomplete, missing any properties defined this way. However if you know that the page you're checking doesn't use any
style
attributes, then you can disable this option and give the script one less thing to check.Please also note that CSS properties defined in
style
attributes cannot bypass browser normalization in "author" mode (as opposed to properties defined in external stylehseets, which are returned independently of normalization in this mode); such inconsistency might also be a reason for disabling this option, and/or avoiding the use of HTMLstyle
attributes on applicable pages (assuming you have any such choice or inclination!).
"watch"
CSSUtilities.define("watch", false);
- watch for stylesheet switching [REQUIRED Boolean|Object]
-
A
Boolean
that defines whether or not the library should monitor for stylesheet switching activity. The default value if undefined isfalse
.If this is set to
true
, the script will continually check thedisabled
state of every listed stylesheet; as soon as any of them changes, the library automatically re-initializes to refresh its data cache. Automatic re-initialization will also re-fire any stored initialization callback.If you set this to
false
then the timer will not run, so if any state changes do occur then the library will no longer have accurate data. In that situation it would be up to you to manually re-initialize as necessary (for example, as part of a stylesheet switching script).In "author" mode (only) you can also set this to
null
, which tells the library to ignore the disabled state of all stylesheets, and parse them as though they were all enabled; and no further monitoring will take place. This is a reference setting and should be used with care, because it means that the data methods may subsequently return rules and properties which are marked as active when really they're not (because they're inside a stylesheet that's disabled). - → Important Browser Note:
-
In Safari and other Webkit browsers, a stylesheet element's
disabled
property will always returnfalse
until it's been explicitly set. This is a browser issue which affects the script in "author" mode, and means that alternate stylesheets will appear to be normal stylesheets, enabled by default, and the watch mechanism won't work until at least one switch has happened.This issue is easily fixed, but the script can't do so automatically, since it may potentially change the page's default appearance or behavior.
So if you have this problem (ie. if you're running in
"author"
mode, and thewatch
setting is notnull
, and you have alternate stylesheets on the page), then you should implement the fix yourself; like this:for(var links=document.getElementsByTagName('link'), i=0; i<links.length; i++) { if(links[i].getAttribute('rel') == 'alternate stylesheet') { links[i].disabled = true; } }
All that does is set a default
disabled
value for alternate stylesheets, which reflects the state they have anyway, but so that it can be recorded accurately. You should add the code before intializing CSSUtilities; and if you're implementing any kind of stylesheet switcher script, you should also add the fix before re-creating any saved state (such as from a cookie).(This issue can also be fixed by adding a
disabled="disabled"
attribute to the alternate stylesheets; however that would be invalid HTML, for some reason.)
"api"
CSSUtilities.define("api", false);
- external Selectors API settings [REQUIRED Boolean][, OPTIONAL Function]
-
A
Boolean
and aFunction
that together control any external Selectors API.Much of the functionality available from CSSUtilities is only possible because of the existence of a Selectors API, that identifies elements matching a given CSS selector. The most recent browser versions (Opera 10, Firefox 3.5+, Safari, Chrome and IE8) have this functionality built-in, but to make it work in the other supported browsers (Opera 9, Firefox 1.5–3.0, Konqueror, IE6 and IE7) we must use an external library to provide the API; LlamaLab's Selector.js is bundled by default.
So, if you leave the
"api"
option undefined, the behaviors just described will apply.The first value is a
Boolean
which forces every browser to use the fallback Selectors API (true
), or lets each browser prefer its native implementation if one is available (false
). The default value isfalse
.The second value allows you to specify a different library, other than the one bundled by default. Once you've added its
<script>
include to the page, you define a wrapper to acts as an intermediary between CSSUtilities and your library's Selectors API; like this:CSSUtilities.define("api", false, function(selector, page) { return queryMyAPI(selector, page); });
The wrapper will be passed a
String
selector (the"selector"
argument) and aDocument
context (the"page"
argument), and must return an array orNodeList
of zero or moreElement
nodes, as returned by a selector query. If your chosen API returns the data in a different structure or format, you'll have to convert it before returning.CSSUtilities will test the wrapper as soon as you define it, and will throw a fatal error if it returns invalid data, that prevents any further use of the library.
You can further test it by examining the data it returns yourself — just change the first value to
true
and then all browsers will be using it.If you are defining this option, you must define both
"page"
and"base"
first, as they cannot be re-defined afterwards.Here are some sample wrappers for a selection of popular libraries:
base2
base2 returns a static node list where square-bracket notation is not supported, so it must be converted to an array:
CSSUtilities.define('api', false, function(selector, page) { var nodes = [], list = base2.DOM.Document.querySelectorAll(page, selector); for(var len=list.length, i=0; i<len; i++) { nodes.push(list.item(i)); } return nodes; });
DOMAssistant
DOMAssistant occasionally returns
undefined
for a selector it's unable to match, so we test for that and return an empty array instead:CSSUtilities.define('api', false, function(selector, page) { var nodes = $(page).cssSelect(selector); return typeof nodes != 'undefined' ? nodes : []; });
Sizzle / jQuery
Sizzle is missing support for a number of CSS3 selectors.
CSSUtilities.define('api', false, function(selector, page) { return Sizzle(selector, page); });
prototype
CSSUtilities.define('api', false, function(selector, page) { return new Selector(selector).findElements(page); });
dojo
CSSUtilities.define('api', false, function(selector, page) { return dojo.query(selector, page); });
YUI
CSSUtilities.define('api', false, function(selector, page) { return YAHOO.util.Selector.query(selector, page); });
The .supported
property
CSSUtilities includes a global property for filtering-out known unsupported browsers. It's set as soon as the script exists, and can be used as a filter to minimize or eliminate scripting errors in older browsers.
The value is formed by testing for three properties —
document.getElementById
, document.styleSheets
and document.nodeType
— and if
any one of those properties is missing,
the browser will be marked as unsupported. This is known to
completely exclude Internet Explorer 5.5 and Opera 8,
but can be reasonably assumed to exclude most legacy browsers
from that generation or earlier.
If you wish to use this then, simply wrap any or all uses of the library's methods with an “if-supported” condition:
if(CSSUtilities.supported)
{
CSSUtilities.init();
//etc.
}