domtokenlist
Version:
Enables support for the DOMTokenList interface in IE8-9, and fixes buggy implementations in newer browsers.
354 lines (259 loc) • 10.1 kB
JavaScript
/** DOMTokenList polyfill */
(function(){
"use strict";
/*<*/
var UNDEF,
WIN = window,
DOC = document,
OBJ = Object,
NULL = null,
TRUE = true,
FALSE = false,
/*>*/
/** Munge the hell out of our string literals. Saves a tonne of space after compression. */
SPACE = " ",
ELEMENT = "Element",
CREATE_ELEMENT = "create"+ELEMENT,
DOM_TOKEN_LIST = "DOMTokenList",
DEFINE_GETTER = "__defineGetter__",
DEFINE_PROPERTY = "defineProperty",
CLASS_ = "class",
LIST = "List",
CLASS_LIST = CLASS_+LIST,
REL = "rel",
REL_LIST = REL+LIST,
DIV = "div",
LENGTH = "length",
CONTAINS = "contains",
APPLY = "apply",
HTML_ = "HTML",
METHODS = ("item "+CONTAINS+" add remove toggle toString toLocaleString").split(SPACE),
ADD = METHODS[2],
REMOVE = METHODS[3],
TOGGLE = METHODS[4],
PROTOTYPE = "prototype",
/** Ascertain browser support for Object.defineProperty */
dpSupport = DEFINE_PROPERTY in OBJ || DEFINE_GETTER in OBJ[ PROTOTYPE ] || NULL,
/** Wrapper for Object.defineProperty that falls back to using the legacy __defineGetter__ method if available. */
defineGetter = function(object, name, fn, configurable){
if(OBJ[ DEFINE_PROPERTY ])
OBJ[ DEFINE_PROPERTY ](object, name, {
configurable: FALSE === dpSupport ? TRUE : !!configurable,
get: fn
});
else object[ DEFINE_GETTER ](name, fn);
},
/** DOMTokenList interface replacement */
DOMTokenList = function(el, prop){
var THIS = this,
/** Private variables */
tokens = [],
tokenMap = {},
length = 0,
maxLength = 0,
reindex = function(){
/** Define getter functions for array-like access to the tokenList's contents. */
if(length >= maxLength)
for(; maxLength < length; ++maxLength) (function(i){
defineGetter(THIS, i, function(){
preop();
return tokens[i];
}, FALSE);
})(maxLength);
},
/** Helper function called at the start of each class method. Internal use only. */
preop = function(){
var error, i,
args = arguments,
rSpace = /\s+/;
/** Validate the token/s passed to an instance method, if any. */
if(args[ LENGTH ])
for(i = 0; i < args[ LENGTH ]; ++i)
if(rSpace.test(args[i])){
error = new SyntaxError('String "' + args[i] + '" ' + CONTAINS + ' an invalid character');
error.code = 5;
error.name = "InvalidCharacterError";
throw error;
}
/** Split the new value apart by whitespace*/
tokens = ("" + el[prop]).replace(/^\s+|\s+$/g, "").split(rSpace);
/** Avoid treating blank strings as single-item token lists */
if("" === tokens[0]) tokens = [];
/** Repopulate the internal token lists */
tokenMap = {};
for(i = 0; i < tokens[ LENGTH ]; ++i)
tokenMap[tokens[i]] = TRUE;
length = tokens[ LENGTH ];
reindex();
};
/** Populate our internal token list if the targeted attribute of the subject element isn't empty. */
preop();
/** Return the number of tokens in the underlying string. Read-only. */
defineGetter(THIS, LENGTH, function(){
preop();
return length;
});
/** Override the default toString/toLocaleString methods to return a space-delimited list of tokens when typecast. */
THIS[ METHODS[6] /** toLocaleString */ ] =
THIS[ METHODS[5] /** toString */ ] = function(){
preop();
return tokens.join(SPACE);
};
/** Return an item in the list by its index (or undefined if the number is greater than or equal to the length of the list) */
THIS.item = function(idx){
preop();
return tokens[idx];
};
/** Return TRUE if the underlying string contains `token`; otherwise, FALSE. */
THIS[ CONTAINS ] = function(token){
preop();
return !!tokenMap[token];
};
/** Add one or more tokens to the underlying string. */
THIS[ADD] = function(){
preop[APPLY](THIS, args = arguments);
for(var args, token, i = 0, l = args[ LENGTH ]; i < l; ++i){
token = args[i];
if(!tokenMap[token]){
tokens.push(token);
tokenMap[token] = TRUE;
}
}
/** Update the targeted attribute of the attached element if the token list's changed. */
if(length !== tokens[ LENGTH ]){
length = tokens[ LENGTH ] >>> 0;
el[prop] = tokens.join(SPACE);
reindex();
}
};
/** Remove one or more tokens from the underlying string. */
THIS[ REMOVE ] = function(){
preop[APPLY](THIS, args = arguments);
/** Build a hash of token names to compare against when recollecting our token list. */
for(var args, ignore = {}, i = 0, t = []; i < args[ LENGTH ]; ++i){
ignore[args[i]] = TRUE;
delete tokenMap[args[i]];
}
/** Run through our tokens list and reassign only those that aren't defined in the hash declared above. */
for(i = 0; i < tokens[ LENGTH ]; ++i)
if(!ignore[tokens[i]]) t.push(tokens[i]);
tokens = t;
length = t[ LENGTH ] >>> 0;
/** Update the targeted attribute of the attached element. */
el[prop] = tokens.join(SPACE);
reindex();
};
/** Add or remove a token depending on whether it's already contained within the token list. */
THIS[TOGGLE] = function(token, force){
preop[APPLY](THIS, [token]);
/** Token state's being forced. */
if(UNDEF !== force){
if(force) { THIS[ADD](token); return TRUE; }
else { THIS[REMOVE](token); return FALSE; }
}
/** Token already exists in tokenList. Remove it, and return FALSE. */
if(tokenMap[token]){
THIS[ REMOVE ](token);
return FALSE;
}
/** Otherwise, add the token and return TRUE. */
THIS[ADD](token);
return TRUE;
};
/** Mark our newly-assigned methods as non-enumerable. */
(function(o, defineProperty){
if(defineProperty)
for(var i = 0; i < 7; ++i)
defineProperty(o, METHODS[i], {enumerable: FALSE});
}(THIS, OBJ[ DEFINE_PROPERTY ]));
return THIS;
},
/** Polyfills a property with a DOMTokenList */
addProp = function(o, name, attr){
defineGetter(o[PROTOTYPE], name, function(){
var tokenList,
THIS = this,
/** Prevent this from firing twice for some reason. What the hell, IE. */
gibberishProperty = DEFINE_GETTER + DEFINE_PROPERTY + name;
if(THIS[gibberishProperty]) return tokenList;
THIS[gibberishProperty] = TRUE;
/**
* IE8 can't define properties on native JavaScript objects, so we'll use a dumb hack instead.
*
* What this is doing is creating a dummy element ("reflection") inside a detached phantom node ("mirror")
* that serves as the target of Object.defineProperty instead. While we could simply use the subject HTML
* element instead, this would conflict with element types which use indexed properties (such as forms and
* select lists).
*/
if(FALSE === dpSupport){
var visage,
mirror = addProp.mirror = addProp.mirror || DOC[ CREATE_ELEMENT ](DIV),
reflections = mirror.childNodes,
/** Iterator variables */
l = reflections[ LENGTH ],
i = 0;
for(; i < l; ++i)
if(reflections[i]._R === THIS){
visage = reflections[i];
break;
}
/** Couldn't find an element's reflection inside the mirror. Materialise one. */
visage || (visage = mirror.appendChild(DOC[ CREATE_ELEMENT ](DIV)));
tokenList = DOMTokenList.call(visage, THIS, attr);
}
else tokenList = new DOMTokenList(THIS, attr);
defineGetter(THIS, name, function(){ return tokenList; });
delete THIS[gibberishProperty];
return tokenList;
}, TRUE);
},
/** Variables used for patching native methods that're partially implemented (IE doesn't support adding/removing multiple tokens, for instance). */
testList,
nativeAdd,
nativeRemove;
/** No discernible DOMTokenList support whatsoever. Time to remedy that. */
if(!WIN[ DOM_TOKEN_LIST ]){
/** Ensure the browser allows Object.defineProperty to be used on native JavaScript objects. */
if(dpSupport)
try{ defineGetter({}, "support"); }
catch(e){ dpSupport = FALSE; }
DOMTokenList.polyfill = TRUE;
WIN[ DOM_TOKEN_LIST ] = DOMTokenList;
addProp( WIN[ ELEMENT ], CLASS_LIST, CLASS_ + "Name"); /* Element.classList */
addProp( WIN[ HTML_+ "Link" + ELEMENT ], REL_LIST, REL); /* HTMLLinkElement.relList */
addProp( WIN[ HTML_+ "Anchor" + ELEMENT ], REL_LIST, REL); /* HTMLAnchorElement.relList */
addProp( WIN[ HTML_+ "Area" + ELEMENT ], REL_LIST, REL); /* HTMLAreaElement.relList */
}
/**
* Possible support, but let's check for bugs.
*
* Where arbitrary values are needed for performing a test, previous variables
* are recycled to save space in the minified file.
*/
else{
testList = DOC[ CREATE_ELEMENT ](DIV)[CLASS_LIST];
/** We'll replace a "string constant" to hold a reference to DOMTokenList.prototype (filesize optimisation, yaddah-yaddah...) */
PROTOTYPE = WIN[DOM_TOKEN_LIST][PROTOTYPE];
/** Check if we can pass multiple arguments to add/remove. To save space, we'll just recycle a previous array of strings. */
testList[ADD][APPLY](testList, METHODS);
if(2 > testList[LENGTH]){
nativeAdd = PROTOTYPE[ADD];
nativeRemove = PROTOTYPE[REMOVE];
PROTOTYPE[ADD] = function(){
for(var i = 0, args = arguments; i < args[LENGTH]; ++i)
nativeAdd.call(this, args[i]);
};
PROTOTYPE[REMOVE] = function(){
for(var i = 0, args = arguments; i < args[LENGTH]; ++i)
nativeRemove.call(this, args[i]);
};
}
/** Check if the "force" option of .toggle is supported. */
if(testList[TOGGLE](LIST, FALSE))
PROTOTYPE[TOGGLE] = function(token, force){
var THIS = this;
THIS[(force = UNDEF === force ? !THIS[CONTAINS](token) : force) ? ADD : REMOVE](token);
return !!force;
};
}
}());