todomvc
Version:
> Helping you select an MV\* framework
1,655 lines (1,467 loc) • 34.9 kB
JavaScript
/*global window */
/**
* @license DUEL v0.8.2 http://duelengine.org
* Copyright (c)2006-2012 Stephen M. McKamey.
* Licensed under The MIT License.
*/
/**
* @public
* @param {Array|Object|string|number|function(*,number,number):Array|Object|string} view The view template
* @return {function(*)}
*/
var duel = (
/**
* @param {Document} document Document reference
* @param {function()} scriptEngine script engine version
* @param {*=} undef undefined
*/
function(document, scriptEngine, undef) {
'use strict';
/* types.js --------------------*/
/**
* @private
* @const
* @type {number}
*/
var NUL = 0;
/**
* @private
* @const
* @type {number}
*/
var FUN = 1;
/**
* @private
* @const
* @type {number}
*/
var ARY = 2;
/**
* @private
* @const
* @type {number}
*/
var OBJ = 3;
/**
* @private
* @const
* @type {number}
*/
var VAL = 4;
/**
* @private
* @const
* @type {number}
*/
var RAW = 5;
/**
* Wraps a data value to maintain as raw markup in output
*
* @private
* @this {Markup}
* @param {string} value The value
* @constructor
*/
function Markup(value) {
/**
* @type {string}
* @const
* @protected
*/
this.value = value;
}
/**
* Renders the value
*
* @public
* @override
* @this {Markup}
* @return {string} value
*/
Markup.prototype.toString = function() {
return this.value;
};
/**
* Determines if the value is an Array
*
* @private
* @param {*} val the object being tested
* @return {boolean}
*/
var isArray = Array.isArray || function(val) {
return (val instanceof Array);
};
/**
* Determines the type of the value
*
* @private
* @param {*} val the object being tested
* @return {number}
*/
function getType(val) {
switch (typeof val) {
case 'object':
return !val ? NUL : (isArray(val) ? ARY : ((val instanceof Markup) ? RAW : ((val instanceof Date) ? VAL : OBJ)));
case 'function':
return FUN;
case 'undefined':
return NUL;
default:
return VAL;
}
}
/**
* Determines if the value is a string
*
* @private
* @param {*} val the object being tested
* @return {boolean}
*/
function isString(val) {
return (typeof val === 'string');
}
/**
* Determines if the value is a function
*
* @private
* @param {*} val the object being tested
* @return {boolean}
*/
function isFunction(val) {
return (typeof val === 'function');
}
/**
* String buffer
*
* @private
* @this {Buffer}
* @constructor
*/
function Buffer() {
/**
* @type {Array|string}
* @private
*/
this.value = Buffer.FAST ? '' : [];
}
/**
* Only IE<9 benefits from Array.join()
*
* @private
* @constant
* @type {boolean}
*/
Buffer.FAST = !(scriptEngine && scriptEngine() < 9);
/**
* Appends to the internal value
*
* @public
* @this {Buffer}
* @param {null|string} v1
* @param {null|string=} v2
* @param {null|string=} v3
*/
Buffer.prototype.append = function(v1, v2, v3) {
if (Buffer.FAST) {
if (v1 !== null) {
this.value += v1;
if (v2 !== null && v2 !== undef) {
this.value += v2;
if (v3 !== null && v3 !== undef) {
this.value += v3;
}
}
}
} else {
this.value.push.apply(
// Closure Compiler type cast
/** @type{Array} */(this.value),
arguments);
}
};
/**
* Clears the internal value
*
* @public
* @this {Buffer}
*/
Buffer.prototype.clear = function() {
this.value = Buffer.FAST ? '' : [];
};
/**
* Renders the value
*
* @public
* @override
* @this {Buffer}
* @return {string} value
*/
Buffer.prototype.toString = function() {
return Buffer.FAST ?
// Closure Compiler type cast
/** @type{string} */(this.value) :
this.value.join('');
};
function digits(n) {
return (n < 10) ? '0'+n : n;
}
/**
* Formats the value as a string
*
* @private
* @param {*} val the object being rendered
* @return {string|null}
*/
function asString(val) {
var buffer, needsDelim;
switch (getType(val)) {
case VAL:
return ''+val;
case NUL:
return '';
case ARY:
// flatten into simple list
buffer = new Buffer();
for (var i=0, length=val.length; i<length; i++) {
if (needsDelim) {
buffer.append(', ');
} else {
needsDelim = true;
}
buffer.append(asString(val[i]));
}
return buffer.toString();
case OBJ:
// format JSON-like
buffer = new Buffer();
buffer.append('{');
for (var key in val) {
if (val.hasOwnProperty(key)) {
if (needsDelim) {
buffer.append(', ');
} else {
needsDelim = true;
}
buffer.append(key, '=', asString(val[key]));
}
}
buffer.append('}');
return buffer.toString();
}
// Closure Compiler type cast
return /** @type{string} */(val);
}
/**
* Wraps a binding result with rendering methods
*
* @private
* @this {Result}
* @param {Array|Object|string|number} view The result tree
* @constructor
*/
function Result(view) {
if (!isArray(view)) {
// ensure is rooted element
view = ['', view];
}
/**
* @type {Array}
* @const
* @protected
*/
// Closure Compiler type cast
this.value = /** @type {Array} */(view);
}
/* bind.js --------------------*/
/**
* @private
* @constant
* @type {string}
*/
var FOR = '$for';
/**
* @private
* @constant
* @type {string}
*/
var XOR = '$xor';
/**
* @private
* @constant
* @type {string}
*/
var IF = '$if';
/**
* @private
* @constant
* @type {string}
*/
var CALL = '$call';
/**
* @private
* @constant
* @type {string}
*/
var PART = '$part';
/**
* @private
* @constant
* @type {string}
*/
var TEST = 'test';
/**
* @private
* @constant
* @type {string}
*/
var EACH = 'each';
/**
* @private
* @constant
* @type {string}
*/
var IN = 'in';
/**
* @private
* @constant
* @type {string}
*/
var VIEW = 'view';
/**
* @private
* @constant
* @type {string}
*/
var DATA = 'data';
/**
* @private
* @constant
* @type {string}
*/
var INDEX = 'index';
/**
* @private
* @constant
* @type {string}
*/
var COUNT = 'count';
/**
* @private
* @constant
* @type {string}
*/
var KEY = 'key';
/**
* @private
* @constant
* @type {string}
*/
var NAME = 'name';
var bind;
/**
* Appends a node to a parent
*
* @private
* @param {Array} parent The parent node
* @param {Array|Object|string|number} child The child node
*/
function append(parent, child) {
switch (getType(child)) {
case ARY:
if (child[0] === '') {
// child is documentFragment
// directly append children, skip fragment identifier
for (var i=1, length=child.length; i<length; i++) {
append(parent, child[i]);
}
} else {
// child is an element array
parent.push(child);
}
break;
case OBJ:
// child is attributes object
if (parent.length === 1) {
parent.push(child);
} else {
var old = parent[1];
if (getType(old) === OBJ) {
// merge attribute objects
for (var key in child) {
if (child.hasOwnProperty(key)) {
old[key] = child[key];
}
}
} else {
// insert attributes object
parent.splice(1, 0, child);
}
}
break;
case VAL:
if (child !== '') {
// coerce primitive to string literal
child = '' + child;
var last = parent.length-1;
if (last > 0 && getType(parent[last]) === VAL) {
// combine string literals
parent[last] += child;
} else {
// append
parent.push(child);
}
}
break;
case NUL:
// cull empty values
break;
default:
// append others
parent.push(child);
break;
}
}
/**
* Binds the child nodes ignoring parent element and attributes
*
* @private
* @param {Array} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
function bindContent(node, data, index, count, key, parts) {
// second item might be attributes object
var hasAttr = (getType(node[1]) === OBJ);
if (node.length === (hasAttr ? 3 : 2)) {
// unwrap single nodes
return bind(node[node.length-1], data, index, count, key, parts);
}
// element array, make a doc frag
var result = [''];
for (var i=hasAttr ? 2 : 1, length=node.length; i<length; i++) {
append(result, bind(node[i], data, index, count, key, parts));
}
return result;
}
/**
* Binds the content once for each item in data
*
* @private
* @param {Array|Object|string|number|function(*,number,number):*} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
function loop(node, data, index, count, key, parts) {
var args = node[1] || {},
result = [''],
items, i, length;
if (args.hasOwnProperty(COUNT)) {
// evaluate for-count loop
length = args[COUNT];
if (isFunction(length)) {
// execute code block
length = length(data, index, count, key);
}
var d;
if (args.hasOwnProperty(DATA)) {
d = args[DATA];
if (isFunction(d)) {
// execute code block
d = d(data, index, count, key);
}
} else {
d = data;
}
// iterate over the items
for (i=0; i<length; i++) {
// Closure Compiler type cast
append(result, bindContent(/** @type {Array} */(node), d, i, length, null, parts));
}
return result;
}
if (args.hasOwnProperty(IN)) {
// convert for-in loop to for-each loop
var obj = args[IN];
if (isFunction(obj)) {
// execute code block
obj = obj(data, index, count, key);
}
if (getType(obj) === OBJ) {
// accumulate the property keys to get count
items = [];
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
items.push(k);
}
}
// iterate over the keys
for (i=0, length=items.length; i<length; i++) {
// Closure Compiler type cast
append(result, bindContent(/** @type {Array} */(node), obj[items[i]], i, length, items[i], parts));
}
return result;
}
// just bind to single value
items = obj;
} else {
// evaluate for-each loop
items = args[EACH];
if (isFunction(items)) {
// execute code block
items = items(data, index, count, key);
}
}
var type = getType(items);
if (type === ARY) {
// iterate over the items
for (i=0, length=items.length; i<length; i++) {
// Closure Compiler type cast
append(result, bindContent(/** @type {Array} */(node), items[i], i, length, null, parts));
}
} else if (type !== NUL) {
// just bind the single value
// Closure Compiler type cast
result = bindContent(/** @type {Array} */(node), items, 0, 1, null, parts);
}
return result;
}
/**
* Binds the node to the first conditional block that evaluates to true
*
* @private
* @param {Array|Object|string|number|function(*,number,number):Array|Object|string} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
function xor(node, data, index, count, key, parts) {
for (var i=1, length=node.length; i<length; i++) {
var block = node[i],
args = block[1],
test = args[TEST];
if (getType(block[1]) === OBJ && test) {
// execute test if exists
if (isFunction(test)) {
test = test(data, index, count, key);
}
if (!test) {
continue;
}
}
// process block contents
return bindContent(block, data, index, count, key, parts);
}
return null;
}
/**
* Calls into another view
*
* @private
* @param {Array|Object|string|number|function(*,*,*,*):(Object|null)} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @return {Array|Object|string|number}
*/
function call(node, data, index, count, key) {
var args = node[1] || {};
if (!args[VIEW]) {
return null;
}
// evaluate the arguments
var v = bind(args[VIEW], data, index, count, key),
d = args.hasOwnProperty(DATA) ? bind(args[DATA], data, index, count, key) : data,
i = args.hasOwnProperty(INDEX) ? bind(args[INDEX], data, index, count, key) : index,
c = args.hasOwnProperty(COUNT) ? bind(args[COUNT], data, index, count, key) : count,
k = args.hasOwnProperty(KEY) ? bind(args[KEY], data, index, count, key) : key,
p = {};
// check for view parts
for (var j=node.length-1; j>=2; j--) {
var block = node[j];
args = block[1] || {};
if (args.hasOwnProperty(NAME)) {
p[args[NAME]] = block;
}
}
return (v && isFunction(v.getView)) ?
// Closure Compiler type cast
bind(v.getView(), d, /** @type {number} */i, /** @type {number} */c, /** @type {string} */k, p) : null;
}
/**
* Replaces a place holder part with the named part from the calling view
*
* @private
* @param {Array|Object|string|number|function(*,*,*,*):(Object|null)} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
function part(node, data, index, count, key, parts) {
var args = node[1] || {},
block = args[NAME] || '';
block = parts && parts.hasOwnProperty(block) ? parts[block] : node;
return bindContent(block, data, index, count, key);
}
/**
* Binds the node to data
*
* @private
* @param {Array|Object|string|number|function(*,*,*,*):(Object|null)} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
bind = function(node, data, index, count, key, parts) {
switch (getType(node)) {
case FUN:
// execute code block
// Closure Compiler type cast
return (/** @type {function(*,*,*,*):(Object|null)} */ (node))(data, index, count, key);
case ARY:
// inspect element name for template commands
/**
* @type {string}
*/
var tag = node[0] || '';
switch (tag) {
case FOR:
return loop(node, data, index, count, key, parts);
case XOR:
return xor(node, data, index, count, key, parts);
case IF:
return xor([XOR, node], data, index, count, key, parts);
case CALL:
// parts not needed when calling another view
return call(node, data, index, count, key);
case PART:
return part(node, data, index, count, key, parts);
}
// element array, first item is name
var elem = [tag];
for (var i=1, length=node.length; i<length; i++) {
append(elem, bind(node[i], data, index, count, key, parts));
}
return elem;
case OBJ:
// attribute map
var attr = {};
for (var k in node) {
if (node.hasOwnProperty(k)) {
// parts not needed when binding attributes
attr[k] = bind(node[k], data, index, count, key);
}
}
return attr;
}
// literal values
return node;
};
/* factory.js --------------------*/
/**
* Renders an error as text
*
* @private
* @param {Error} ex The exception
* @return {string}
*/
function onError(ex) {
return '['+ex+']';
}
/**
* Wraps a view definition with binding method
*
* @private
* @param {Array|Object|string|number} view The template definition
* @return {function(*)}
*/
function factory(view) {
if (getType(view) !== ARY) {
// ensure is rooted element
view = ['', view];
}
/**
* Binds and wraps the result
*
* @public
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @return {Result}
*/
var self = function(data, index, count, key) {
try {
var result = bind(
// Closure Compiler type cast
/** @type {Array} */(view),
data,
isFinite(index) ? index : 0,
isFinite(count) ? count : 1,
isString(key) ? key : null);
return new Result(result);
} catch (ex) {
// handle error with context
return new Result(onError(ex));
}
};
/**
* Gets the internal view definition
*
* @private
* @return {Array}
*/
self.getView = function() {
// Closure Compiler type cast
return /** @type {Array} */(view);
};
return self;
}
/**
* @public
* @param {Array|Object|string|number|function(*,number,number):Array|Object|string} view The view template
* @return {Array|Object|string|number}
*/
var duel = function(view) {
return (isFunction(view) && isFunction(view.getView)) ? view : factory(view);
};
/**
* @public
* @param {string} value Markup text
* @return {Markup}
*/
duel.raw = function(value) {
return new Markup(value);
};
/* render.js --------------------*/
/**
* Void tag lookup
*
* @private
* @constant
* @type {Object.<boolean>}
*/
var VOID_TAGS = {
'area' : true,
'base' : true,
'basefont' : true,
'br' : true,
'col' : true,
'frame' : true,
'hr' : true,
'img' : true,
'input' : true,
'isindex' : true,
'keygen' : true,
'link' : true,
'meta' : true,
'param' : true,
'source' : true,
'wbr' : true
};
/**
* Boolean attribute map
*
* @private
* @constant
* @type {Object.<number>}
*/
var ATTR_BOOL = {
'async': 1,
'checked': 1,
'defer': 1,
'disabled': 1,
'hidden': 1,
'novalidate': 1,
'formnovalidate': 1
// can add more attributes here as needed
};
/**
* Encodes invalid literal characters in strings
*
* @private
* @param {Array|Object|string|number} val The value
* @return {Array|Object|string|number}
*/
function htmlEncode(val) {
if (!isString(val)) {
return val;
}
return val.replace(/[&<>]/g,
function(ch) {
switch(ch) {
case '&':
return '&';
case '<':
return '<';
case '>':
return '>';
default:
return ch;
}
});
}
/**
* Encodes invalid attribute characters in strings
*
* @private
* @param {Array|Object|string|number} val The value
* @return {Array|Object|string|number}
*/
function attrEncode(val) {
if (!isString(val)) {
return val;
}
return val.replace(/[&<>"]/g,
function(ch) {
switch(ch) {
case '&':
return '&';
case '<':
return '<';
case '>':
return '>';
case '"':
return '"';
default:
return ch;
}
});
}
/**
* Renders the comment as a string
*
* @private
* @param {Buffer} buffer The output buffer
* @param {Array} node The result tree
*/
function renderComment(buffer, node) {
if (node[0] === '!DOCTYPE') {
// emit doctype
buffer.append('<!DOCTYPE ', node[1], '>');
} else {
// emit HTML comment
buffer.append('<!--', node[1], '-->');
}
}
/**
* Renders the element as a string
*
* @private
* @param {Buffer} buffer The output buffer
* @param {Array} node The result tree
*/
function renderElem(buffer, node) {
var tag = node[0] || '',
length = node.length,
i = 1,
child,
isVoid = VOID_TAGS[tag];
if (tag.charAt(0) === '!') {
renderComment(buffer, node);
return;
}
if (tag) {
// emit open tag
buffer.append('<', tag);
child = node[i];
if (getType(child) === OBJ) {
// emit attributes
for (var name in child) {
if (child.hasOwnProperty(name)) {
var val = child[name];
if (ATTR_BOOL[name]) {
if (val) {
val = name;
} else {
// falsey boolean attributes must not be present
continue;
}
}
buffer.append(' ', name);
if (getType(val) !== NUL) {
// Closure Compiler type cast
buffer.append('="', /** @type{string} */(attrEncode(val)), '"');
}
}
}
i++;
}
if (isVoid) {
buffer.append(' /');
}
buffer.append('>');
}
// emit children
for (; i<length; i++) {
child = node[i];
if (isArray(child)) {
renderElem(buffer, child);
} else {
// encode string literals
// Closure Compiler type cast
buffer.append(/** @type{string} */(htmlEncode(child)));
}
}
if (tag && !isVoid) {
// emit close tag
buffer.append('</', tag, '>');
}
}
/**
* Renders the result as a string
*
* @private
* @param {Array} view The compiled view
* @return {string}
*/
function render(view) {
try {
var buffer = new Buffer();
renderElem(buffer, view);
return buffer.toString();
} catch (ex) {
// handle error with context
return onError(ex);
}
}
/**
* Returns result as HTML text
*
* @public
* @override
* @this {Result}
* @return {string}
*/
Result.prototype.toString = function() {
return render(this.value);
};
/**
* Immediately writes the resulting value to the document
*
* @public
* @this {Result}
* @param {Document} doc optional Document reference
*/
Result.prototype.write = function(doc) {
/*jslint evil:true*/
(doc||document).write(''+this);
/*jslint evil:false*/
};
/* dom.js --------------------*/
/**
* @private
* @constant
* @type {string}
*/
var INIT = '$init';
/**
* @private
* @constant
* @type {string}
*/
var LOAD = '$load';
/**
* Attribute name map
*
* @private
* @constant
* @type {Object.<string>}
*/
var ATTR_MAP = {
'rowspan': 'rowSpan',
'colspan': 'colSpan',
'cellpadding': 'cellPadding',
'cellspacing': 'cellSpacing',
'tabindex': 'tabIndex',
'accesskey': 'accessKey',
'hidefocus': 'hideFocus',
'usemap': 'useMap',
'maxlength': 'maxLength',
'readonly': 'readOnly',
'contenteditable': 'contentEditable'
// can add more attributes here as needed
};
/**
* Attribute duplicates map
*
* @private
* @constant
* @type {Object.<string>}
*/
var ATTR_DUP = {
'enctype': 'encoding',
'onscroll': 'DOMMouseScroll',
'checked': 'defaultChecked'
// can add more attributes here as needed
};
/**
* Leading SGML line ending pattern
*
* @private
* @constant
* @type {RegExp}
*/
var LEADING = /^[\r\n]+/;
/**
* Trailing SGML line ending pattern
*
* @private
* @constant
* @type {RegExp}
*/
var TRAILING = /[\r\n]+$/;
/**
* Creates a DOM element
*
* @private
* @param {string} tag The element's tag name
* @return {Node}
*/
function createElement(tag) {
if (!tag) {
// create a document fragment to hold multiple-root elements
if (document.createDocumentFragment) {
return document.createDocumentFragment();
}
tag = '';
} else if (tag.charAt(0) === '!') {
return document.createComment(tag === '!' ? '' : tag.substr(1)+' ');
}
if (tag.toLowerCase() === 'style' && document.createStyleSheet) {
// IE requires this interface for styles
return document.createStyleSheet();
}
return document.createElement(tag);
}
/**
* Appends a child to an element
*
* @private
* @param {Node} elem The parent element
* @param {Node} child The child
*/
function appendDOM(elem, child) {
if (child) {
var tag = (elem.tagName||'').toLowerCase();
if (elem.nodeType === 8) { // comment
if (child.nodeType === 3) { // text node
elem.nodeValue += child.nodeValue;
}
} else if (tag === 'table' && elem.tBodies) {
if (!child.tagName) {
// must unwrap documentFragment for tables
if (child.nodeType === 11) {
while (child.firstChild) {
appendDOM(elem, child.removeChild(child.firstChild));
}
}
return;
}
// in IE must explicitly nest TRs in TBODY
var childTag = child.tagName.toLowerCase();// child tagName
if (childTag && childTag !== 'tbody' && childTag !== 'thead') {
// insert in last tbody
var tBody = elem.tBodies.length > 0 ? elem.tBodies[elem.tBodies.length-1] : null;
if (!tBody) {
tBody = createElement(childTag === 'th' ? 'thead' : 'tbody');
elem.appendChild(tBody);
}
tBody.appendChild(child);
} else if (elem.canHaveChildren !== false) {
elem.appendChild(child);
}
} else if (tag === 'style' && document.createStyleSheet) {
// IE requires this interface for styles
elem.cssText = child;
} else if (elem.canHaveChildren !== false) {
elem.appendChild(child);
} else if (tag === 'object' &&
child.tagName && child.tagName.toLowerCase() === 'param') {
// IE-only path
try {
elem.appendChild(child);
} catch (ex1) {}
try {
if (elem.object) {
elem.object[child.name] = child.value;
}
} catch (ex2) {}
}
}
}
/**
* Adds an event handler to an element
*
* @private
* @param {Node} elem The element
* @param {string} name The event name
* @param {function(Event)} handler The event handler
*/
function addHandler(elem, name, handler) {
switch (typeof handler) {
case 'function':
if (elem.addEventListener) {
// DOM Level 2
elem.addEventListener((name.substr(0,2) === 'on') ? name.substr(2) : name, handler, false);
} else {
// DOM Level 0
elem[name] = handler;
}
break;
case 'string':
// inline functions are DOM Level 0
/*jslint evil:true */
elem[name] = new Function('event', handler);
/*jslint evil:false */
break;
}
}
/**
* Appends a child to an element
*
* @private
* @param {Node} elem The element
* @param {Object} attr Attributes object
* @return {Node}
*/
function addAttributes(elem, attr) {
if (attr.name && document.attachEvent && !elem.parentNode) {
try {
// IE fix for not being able to programatically change the name attribute
var alt = createElement('<'+elem.tagName+' name="'+attr.name+'">');
// fix for Opera 8.5 and Netscape 7.1 creating malformed elements
if (elem.tagName === alt.tagName) {
elem = alt;
}
} catch (ex) { }
}
// for each attributeName
for (var name in attr) {
if (attr.hasOwnProperty(name)) {
// attributeValue
var value = attr[name],
type = getType(value);
if (name) {
if (type === NUL) {
value = '';
type = VAL;
}
name = ATTR_MAP[name.toLowerCase()] || name;
if (ATTR_BOOL[name]) {
elem[name] = !!value;
// also set duplicated attributes
if (ATTR_DUP[name]) {
elem[ATTR_DUP[name]] = !!value;
}
} else if (name === 'style') {
if (typeof elem.style.cssText !== 'undefined') {
elem.style.cssText = value;
} else {
elem.style = value;
}
} else if (name === 'class') {
elem.className = value;
} else if (name.substr(0,2) === 'on') {
addHandler(elem, name, value);
// also set duplicated events
if (ATTR_DUP[name]) {
addHandler(elem, ATTR_DUP[name], value);
}
} else if (type === VAL && name.charAt(0) !== '$') {
elem.setAttribute(name, value);
// also set duplicated attributes
if (ATTR_DUP[name]) {
elem.setAttribute(ATTR_DUP[name], value);
}
} else {
// allow direct setting of complex properties
elem[name] = value;
// also set duplicated attributes
if (ATTR_DUP[name]) {
elem[ATTR_DUP[name]] = value;
}
}
}
}
}
return elem;
}
/**
* Tests a node for whitespace
*
* @private
* @param {Node} node The node
* @return {boolean}
*/
function isWhitespace(node) {
return !!node && (node.nodeType === 3) && (!node.nodeValue || !/\S/.exec(node.nodeValue));
}
/**
* Trims whitespace pattern from the text node
*
* @private
* @param {Node} node The node
*/
function trimPattern(node, pattern) {
if (!!node && (node.nodeType === 3) && pattern.exec(node.nodeValue)) {
node.nodeValue = node.nodeValue.replace(pattern, '');
}
}
/**
* Removes leading and trailing whitespace nodes
*
* @private
* @param {Node} elem The node
*/
function trimWhitespace(elem) {
if (elem) {
while (isWhitespace(elem.firstChild)) {
// trim leading whitespace text nodes
elem.removeChild(elem.firstChild);
}
// trim leading whitespace text
trimPattern(elem.firstChild, LEADING);
while (isWhitespace(elem.lastChild)) {
// trim trailing whitespace text nodes
elem.removeChild(elem.lastChild);
}
// trim trailing whitespace text
trimPattern(elem.lastChild, TRAILING);
}
}
/**
* Converts the markup to DOM nodes
*
* @private
* @param {string|Markup} value The node
* @return {Node}
*/
function toDOM(value) {
var wrapper = createElement('div');
wrapper.innerHTML = ''+value;
// trim extraneous whitespace
trimWhitespace(wrapper);
// eliminate wrapper for single nodes
if (wrapper.childNodes.length === 1) {
return wrapper.firstChild;
}
// create a document fragment to hold elements
var frag = createElement('');
while (wrapper.firstChild) {
frag.appendChild(wrapper.firstChild);
}
return frag;
}
/**
* Retrieve and remove method
*
* @private
* @param {Node} elem The element
* @param {string} key The callback name
* @return {function(Node)}
*/
function popCallback(elem, key) {
var method = elem[key];
if (method) {
try {
delete elem[key];
} catch (ex) {
// sometimes IE doesn't like deleting from DOM
elem[key] = undef;
}
if (!isFunction(method)) {
try {
/*jslint evil:true */
method = new Function(''+method);
/*jslint evil:false */
} catch (ex2) {
// filter
method = null;
}
}
}
return method;
}
/**
* Executes oninit/onload callbacks
*
* @private
* @param {Node} elem The element
*/
function callbacks(elem) {
if (!elem) {
return;
}
// execute and remove oninit method
var method = popCallback(elem, INIT);
if (method) {
// execute in context of element
method.call(elem);
}
// execute and remove onload method
method = popCallback(elem, LOAD);
if (method) {
// queue up to execute after insertion into parentNode
setTimeout(function() {
// execute in context of element
method.call(elem);
method = elem = null;
}, 0);
} else {
method = elem = null;
}
}
/**
* Applies node to DOM
*
* @private
* @param {Node} elem The element to append
* @param {Array} node The node to populate
* @return {Node}
*/
function patchDOM(elem, node) {
for (var i=1, length=node.length; i<length; i++) {
var child = node[i];
switch (getType(child)) {
case ARY:
// build child element
var childTag = child[0];
child = patchDOM(createElement(childTag), child);
if (childTag === 'html') {
// trim extraneous whitespace
trimWhitespace(child);
// trigger callbacks
callbacks(child);
// unwrap HTML root, to simplify insertion
return child;
}
// append child element
appendDOM(elem, child);
break;
case VAL:
if (child !== '') {
// append child value as text
appendDOM(elem, document.createTextNode(''+child));
}
break;
case OBJ:
if (elem.nodeType === 1) {
// add attributes
elem = addAttributes(elem, child);
}
break;
case RAW:
appendDOM(elem, toDOM(child));
break;
}
}
// trim extraneous whitespace
trimWhitespace(elem);
// trigger callbacks
callbacks(elem);
// eliminate wrapper for single nodes
if (elem.nodeType === 11 && elem.childNodes.length === 1) {
elem = elem.firstChild;
}
return elem;
}
/**
* Renders an error as a text node
*
* @private
* @param {Error} ex The exception
* @return {Node}
*/
function onErrorDOM(ex) {
return document.createTextNode(onError(ex));
}
/**
* Returns result as DOM objects
*
* @public
* @this {Result}
* @param {Node|string=} elem An optional element or element ID to be replaced or merged
* @param {boolean=} merge Optionally merge result into elem
* @return {Node|null}
*/
Result.prototype.toDOM = function(elem, merge) {
// resolve the element ID
if (getType(elem) === VAL) {
elem = document.getElementById(
// Closure Compiler type cast
/** @type{string} */(elem));
}
var view;
try {
if (merge) {
view = elem;
elem = null;
}
// Closure Compiler type cast
view = patchDOM(/** @type{Node} */(view) || createElement(this.value[0]), this.value);
} catch (ex) {
// handle error with context
view = onErrorDOM(ex);
}
if (elem && elem.parentNode) {
// replace existing element with result
// Closure Compiler type cast
elem.parentNode.replaceChild(view, /** @type{Node} */(elem));
}
return view;
};
/**
* Replaces entire document with this Result
*
* @public
* @this {Result}
*/
Result.prototype.reload = function() {
// http://stackoverflow.com/questions/4297877
var doc = document;
try {
var newRoot = this.toDOM();
doc.replaceChild(newRoot, doc.documentElement);
if (doc.createStyleSheet) {
// IE requires link repair
var head = newRoot.firstChild;
while (head && (head.tagName||'') !== 'HEAD') {
head = head.nextSibling;
}
var link = head && head.firstChild;
while (link) {
if ((link.tagName||'') === 'LINK') {
// this seems to repair the link
link.href = link.href;
}
link = link.nextSibling;
}
}
} catch (ex) {
/*jslint evil:true*/
doc = doc.open('text/html');
doc.write(this.toString());
doc.close();
/*jslint evil:false*/
}
};
return duel;
})(document, window.ScriptEngineMajorVersion);