ractive
Version:
Next-generation DOM manipulation
2,088 lines (1,613 loc) • 234 kB
JavaScript
/*! Ractive - v0.3.7 - 2013-10-14
* Next-generation DOM manipulation
* http://ractivejs.org
* Copyright (c) 2013 Rich Harris; Licensed MIT */
(function ( global ) {
'use strict';
var Ractive,
// current version
VERSION = '0.3.7',
doc = global.document || null,
// Ractive prototype
proto = {},
// properties of the public Ractive object
adaptors = {},
eventDefinitions = {},
easing,
extend,
parse,
interpolate,
interpolators,
transitions = {},
// internal utils - instance-specific
teardown,
clearCache,
registerDependant,
unregisterDependant,
notifyDependants,
notifyMultipleDependants,
notifyDependantsByPriority,
resolveRef,
processDeferredUpdates,
// internal utils
toString,
isArray,
isObject,
isNumeric,
isEqual,
getEl,
insertHtml,
reassignFragments,
executeTransition,
getPartialDescriptor,
getComponentConstructor,
isStringFragmentSimple,
makeTransitionManager,
requestAnimationFrame,
defineProperty,
defineProperties,
create,
createFromNull,
hasOwn = {}.hasOwnProperty,
noop = function () {},
addEventProxies,
addEventProxy,
appendElementChildren,
bindElement,
createElementAttributes,
getElementNamespace,
updateAttribute,
bindAttribute,
console = global.console || { log: noop, warn: noop },
// internally used constructors
DomFragment,
DomElement,
DomAttribute,
DomPartial,
DomComponent,
DomInterpolator,
DomTriple,
DomSection,
DomText,
StringFragment,
StringInterpolator,
StringSection,
StringText,
ExpressionResolver,
Evaluator,
Animation,
// internally used regexes
leadingWhitespace = /^\s+/,
trailingWhitespace = /\s+$/,
// other bits and pieces
render,
initMustache,
updateMustache,
resolveMustache,
initFragment,
updateSection,
animationCollection,
// array modification
registerKeypathToArray,
unregisterKeypathFromArray,
// parser and tokenizer
getFragmentStubFromTokens,
getToken,
tokenize,
stripCommentTokens,
stripHtmlComments,
stripStandalones,
// error messages
missingParser = 'Missing Ractive.parse - cannot parse template. Either preparse or use the version that includes the parser',
// constants
TEXT = 1,
INTERPOLATOR = 2,
TRIPLE = 3,
SECTION = 4,
INVERTED = 5,
CLOSING = 6,
ELEMENT = 7,
PARTIAL = 8,
COMMENT = 9,
DELIMCHANGE = 10,
MUSTACHE = 11,
TAG = 12,
COMPONENT = 15,
NUMBER_LITERAL = 20,
STRING_LITERAL = 21,
ARRAY_LITERAL = 22,
OBJECT_LITERAL = 23,
BOOLEAN_LITERAL = 24,
GLOBAL = 26,
KEY_VALUE_PAIR = 27,
REFERENCE = 30,
REFINEMENT = 31,
MEMBER = 32,
PREFIX_OPERATOR = 33,
BRACKETED = 34,
CONDITIONAL = 35,
INFIX_OPERATOR = 36,
INVOCATION = 40,
testDiv = ( doc ? doc.createElement( 'div' ) : null ),
noMagic,
// namespaces
namespaces = {
html: 'http://www.w3.org/1999/xhtml',
mathml: 'http://www.w3.org/1998/Math/MathML',
svg: 'http://www.w3.org/2000/svg',
xlink: 'http://www.w3.org/1999/xlink',
xml: 'http://www.w3.org/XML/1998/namespace',
xmlns: 'http://www.w3.org/2000/xmlns/'
};
// we're creating a defineProperty function here - we don't want to add
// this to _legacy.js since it's not a polyfill. It won't allow us to set
// non-enumerable properties. That shouldn't be a problem, unless you're
// using for...in on a (modified) array, in which case you deserve what's
// coming anyway
try {
try {
Object.defineProperty({}, 'test', { value: 0 });
Object.defineProperties({}, { test: { value: 0 } });
} catch ( err ) {
noMagic = true;
throw err;
}
if ( doc ) {
Object.defineProperty( testDiv, 'test', { value: 0 });
Object.defineProperties( testDiv, { test: { value: 0 } });
}
defineProperty = Object.defineProperty;
defineProperties = Object.defineProperties;
} catch ( err ) {
// Object.defineProperty doesn't exist, or we're in IE8 where you can
// only use it with DOM objects (what the fuck were you smoking, MSFT?)
defineProperty = function ( obj, prop, desc ) {
obj[ prop ] = desc.value;
};
defineProperties = function ( obj, props ) {
var prop;
for ( prop in props ) {
if ( props.hasOwnProperty( prop ) ) {
defineProperty( obj, prop, props[ prop ] );
}
}
};
}
try {
Object.create( null );
create = Object.create;
createFromNull = function () {
return Object.create( null );
};
} catch ( err ) {
// sigh
create = (function () {
var F = function () {};
return function ( proto, props ) {
var obj;
F.prototype = proto;
obj = new F();
if ( props ) {
Object.defineProperties( obj, props );
}
return obj;
};
}());
createFromNull = function () {
return {}; // hope you're not modifying the Object prototype
};
}
var hyphenate = function ( str ) {
return str.replace( /[A-Z]/g, function ( match ) {
return '-' + match.toLowerCase();
});
};
// determine some facts about our environment
var cssTransitionsEnabled, transition, transitionend;
(function () {
if ( !doc ) {
return;
}
if ( testDiv.style.transition !== undefined ) {
transition = 'transition';
transitionend = 'transitionend';
cssTransitionsEnabled = true;
} else if ( testDiv.style.webkitTransition !== undefined ) {
transition = 'webkitTransition';
transitionend = 'webkitTransitionEnd';
cssTransitionsEnabled = true;
} else {
cssTransitionsEnabled = false;
}
}());
// Internet Explorer derp. Methods that should be attached to Node.prototype
// are instead attached to HTMLElement.prototype, which means SVG elements
// can't use them. Remember kids, friends don't let friends use IE.
if ( global.Node && !global.Node.prototype.contains && global.HTMLElement && global.HTMLElement.prototype.contains ) {
global.Node.prototype.contains = global.HTMLElement.prototype.contains;
}
(function () {
var getInterpolator,
updateModel,
getBinding,
inheritProperties,
MultipleSelectBinding,
SelectBinding,
RadioNameBinding,
CheckboxNameBinding,
CheckedBinding,
FileListBinding,
GenericBinding;
bindAttribute = function () {
var node = this.parentNode, interpolator, binding, bindings;
if ( !this.fragment ) {
return false; // report failure
}
interpolator = getInterpolator( this );
if ( !interpolator ) {
return false; // report failure
}
this.interpolator = interpolator;
// Hmmm. Not sure if this is the best way to handle this ambiguity...
//
// Let's say we were given `value="{{bar}}"`. If the context stack was
// context stack was `["foo"]`, and `foo.bar` *wasn't* `undefined`, the
// keypath would be `foo.bar`. Then, any user input would result in
// `foo.bar` being updated.
//
// If, however, `foo.bar` *was* undefined, and so was `bar`, we would be
// left with an unresolved partial keypath - so we are forced to make an
// assumption. That assumption is that the input in question should
// be forced to resolve to `bar`, and any user input would affect `bar`
// and not `foo.bar`.
//
// Did that make any sense? No? Oh. Sorry. Well the moral of the story is
// be explicit when using two-way data-binding about what keypath you're
// updating. Using it in lists is probably a recipe for confusion...
this.keypath = interpolator.keypath || interpolator.descriptor.r;
binding = getBinding( this );
if ( !binding ) {
return false;
}
node._ractive.binding = binding;
this.twoway = true;
// register this with the root, so that we can force an update later
bindings = this.root._twowayBindings[ this.keypath ] || ( this.root._twowayBindings[ this.keypath ] = [] );
bindings[ bindings.length ] = binding;
return true;
};
// This is the handler for DOM events that would lead to a change in the model
// (i.e. change, sometimes, input, and occasionally click and keyup)
updateModel = function () {
this._ractive.binding.update();
};
getInterpolator = function ( attribute ) {
var item;
// TODO refactor this? Couldn't the interpolator have got a keypath via an expression?
// Check this is a suitable candidate for two-way binding - i.e. it is
// a single interpolator, which isn't an expression
if ( attribute.fragment.items.length !== 1 ) {
return null;
}
item = attribute.fragment.items[0];
if ( item.type !== INTERPOLATOR ) {
return null;
}
if ( !item.keypath && !item.ref ) {
return null;
}
return item;
};
getBinding = function ( attribute ) {
var node = attribute.parentNode;
if ( node.tagName === 'SELECT' ) {
return ( node.multiple ? new MultipleSelectBinding( attribute, node ) : new SelectBinding( attribute, node ) );
}
if ( node.type === 'checkbox' || node.type === 'radio' ) {
if ( attribute.propertyName === 'name' ) {
if ( node.type === 'checkbox' ) {
return new CheckboxNameBinding( attribute, node );
}
if ( node.type === 'radio' ) {
return new RadioNameBinding( attribute, node );
}
}
if ( attribute.propertyName === 'checked' ) {
return new CheckedBinding( attribute, node );
}
return null;
}
if ( attribute.propertyName !== 'value' ) {
console.warn( 'This is... odd' );
}
if ( attribute.parentNode.type === 'file' ) {
return new FileListBinding( attribute, node );
}
return new GenericBinding( attribute, node );
};
MultipleSelectBinding = function ( attribute, node ) {
var valueFromModel;
inheritProperties( this, attribute, node );
node.addEventListener( 'change', updateModel, false );
valueFromModel = this.root.get( this.keypath );
if ( valueFromModel === undefined ) {
// get value from DOM, if possible
this.update();
}
};
MultipleSelectBinding.prototype = {
value: function () {
var value, options, i, len;
value = [];
options = this.node.options;
len = options.length;
for ( i=0; i<len; i+=1 ) {
if ( options[i].selected ) {
value[ value.length ] = options[i]._ractive.value;
}
}
return value;
},
update: function () {
var attribute, previousValue, value;
attribute = this.attr;
previousValue = attribute.value;
value = this.value();
if ( previousValue === undefined || !arrayContentsMatch( value, previousValue ) ) {
// either length or contents have changed, so we update the model
attribute.receiving = true;
attribute.value = value;
this.root.set( this.keypath, value );
attribute.receiving = false;
}
},
teardown: function () {
this.node.removeEventListener( 'change', updateModel, false );
}
};
SelectBinding = function ( attribute, node ) {
var valueFromModel;
inheritProperties( this, attribute, node );
node.addEventListener( 'change', updateModel, false );
valueFromModel = this.root.get( this.keypath );
if ( valueFromModel === undefined ) {
// get value from DOM, if possible
this.update();
}
};
SelectBinding.prototype = {
value: function () {
var options, i, len;
options = this.node.options;
len = options.length;
for ( i=0; i<len; i+=1 ) {
if ( options[i].selected ) {
return options[i]._ractive.value;
}
}
},
update: function () {
var value = this.value();
this.attr.receiving = true;
this.attr.value = value;
this.root.set( this.keypath, value );
this.attr.receiving = false;
},
teardown: function () {
this.node.removeEventListener( 'change', updateModel, false );
}
};
RadioNameBinding = function ( attribute, node ) {
var valueFromModel;
this.radioName = true; // so that updateModel knows what to do with this
inheritProperties( this, attribute, node );
node.name = '{{' + attribute.keypath + '}}';
node.addEventListener( 'change', updateModel, false );
if ( node.attachEvent ) {
node.addEventListener( 'click', updateModel, false );
}
valueFromModel = this.root.get( this.keypath );
if ( valueFromModel !== undefined ) {
node.checked = ( valueFromModel === node._ractive.value );
} else {
this.root._defRadios[ this.root._defRadios.length ] = this;
}
};
RadioNameBinding.prototype = {
value: function () {
return this.node._ractive ? this.node._ractive.value : this.node.value;
},
update: function () {
var node = this.node;
if ( node.checked ) {
this.attr.receiving = true;
this.root.set( this.keypath, this.value() );
this.attr.receiving = false;
}
},
teardown: function () {
this.node.removeEventListener( 'change', updateModel, false );
this.node.removeEventListener( 'click', updateModel, false );
}
};
CheckboxNameBinding = function ( attribute, node ) {
var valueFromModel, checked;
this.checkboxName = true; // so that updateModel knows what to do with this
inheritProperties( this, attribute, node );
node.name = '{{' + this.keypath + '}}';
node.addEventListener( 'change', updateModel, false );
// in case of IE emergency, bind to click event as well
if ( node.attachEvent ) {
node.addEventListener( 'click', updateModel, false );
}
valueFromModel = this.root.get( this.keypath );
// if the model already specifies this value, check/uncheck accordingly
if ( valueFromModel !== undefined ) {
checked = valueFromModel.indexOf( node._ractive.value ) !== -1;
node.checked = checked;
}
// otherwise make a note that we will need to update the model later
else {
if ( this.root._defCheckboxes.indexOf( this.keypath ) === -1 ) {
this.root._defCheckboxes[ this.root._defCheckboxes.length ] = this.keypath;
}
}
};
CheckboxNameBinding.prototype = {
changed: function () {
return this.node.checked !== !!this.checked;
},
update: function () {
this.checked = this.node.checked;
this.attr.receiving = true;
this.root.set( this.keypath, getValueFromCheckboxes( this.root, this.keypath ) );
this.attr.receiving = false;
},
teardown: function () {
this.node.removeEventListener( 'change', updateModel, false );
this.node.removeEventListener( 'click', updateModel, false );
}
};
CheckedBinding = function ( attribute, node ) {
inheritProperties( this, attribute, node );
node.addEventListener( 'change', updateModel, false );
if ( node.attachEvent ) {
node.addEventListener( 'click', updateModel, false );
}
};
CheckedBinding.prototype = {
value: function () {
return this.node.checked;
},
update: function () {
this.attr.receiving = true;
this.root.set( this.keypath, this.value() );
this.attr.receiving = false;
},
teardown: function () {
this.node.removeEventListener( 'change', updateModel, false );
this.node.removeEventListener( 'click', updateModel, false );
}
};
FileListBinding = function ( attribute, node ) {
inheritProperties( this, attribute, node );
node.addEventListener( 'change', updateModel, false );
};
FileListBinding.prototype = {
value: function () {
return this.attr.parentNode.files;
},
update: function () {
this.attr.root.set( this.attr.keypath, this.value() );
},
teardown: function () {
this.node.removeEventListener( 'change', updateModel, false );
}
};
GenericBinding = function ( attribute, node ) {
inheritProperties( this, attribute, node );
node.addEventListener( 'change', updateModel, false );
if ( !this.root.lazy ) {
node.addEventListener( 'input', updateModel, false );
if ( node.attachEvent ) {
node.addEventListener( 'keyup', updateModel, false );
}
}
};
GenericBinding.prototype = {
value: function () {
var value = this.attr.parentNode.value;
// if the value is numeric, treat it as a number. otherwise don't
if ( ( +value + '' === value ) && value.indexOf( 'e' ) === -1 ) {
value = +value;
}
return value;
},
update: function () {
var attribute = this.attr, value = this.value();
attribute.receiving = true;
attribute.root.set( attribute.keypath, value );
attribute.receiving = false;
},
teardown: function () {
this.node.removeEventListener( 'change', updateModel, false );
this.node.removeEventListener( 'input', updateModel, false );
this.node.removeEventListener( 'keyup', updateModel, false );
}
};
inheritProperties = function ( binding, attribute, node ) {
binding.attr = attribute;
binding.node = node;
binding.root = attribute.root;
binding.keypath = attribute.keypath;
};
}());
(function () {
var updateFileInputValue, deferSelect, initSelect, updateSelect, updateMultipleSelect, updateRadioName, updateCheckboxName, updateEverythingElse;
// There are a few special cases when it comes to updating attributes. For this reason,
// the prototype .update() method points to updateAttribute, which waits until the
// attribute has finished initialising, then replaces the prototype method with a more
// suitable one. That way, we save ourselves doing a bunch of tests on each call
updateAttribute = function () {
var node;
if ( !this.ready ) {
return this; // avoid items bubbling to the surface when we're still initialising
}
node = this.parentNode;
// special case - selects
if ( node.tagName === 'SELECT' && this.name === 'value' ) {
this.update = deferSelect;
this.deferredUpdate = initSelect; // we don't know yet if it's a select-one or select-multiple
return this.update();
}
// special case - <input type='file' value='{{fileList}}'>
if ( this.isFileInputValue ) {
this.update = updateFileInputValue; // save ourselves the trouble next time
return this;
}
// special case - <input type='radio' name='{{twoway}}' value='foo'>
if ( this.twoway && this.name === 'name' ) {
if ( node.type === 'radio' ) {
this.update = updateRadioName;
return this.update();
}
if ( node.type === 'checkbox' ) {
this.update = updateCheckboxName;
return this.update();
}
}
this.update = updateEverythingElse;
return this.update();
};
updateFileInputValue = function () {
return this; // noop - file inputs are readonly
};
initSelect = function () {
// we're now in a position to decide whether this is a select-one or select-multiple
this.deferredUpdate = ( this.parentNode.multiple ? updateMultipleSelect : updateSelect );
this.deferredUpdate();
};
deferSelect = function () {
// because select values depend partly on the values of their children, and their
// children may be entering and leaving the DOM, we wait until updates are
// complete before updating
this.root._defSelectValues.push( this );
return this;
};
updateSelect = function () {
var value = this.fragment.getValue(), options, option, i;
this.value = value;
options = this.parentNode.options;
i = options.length;
while ( i-- ) {
option = options[i];
if ( option._ractive.value === value ) {
option.selected = true;
return this;
}
}
// if we're still here, it means the new value didn't match any of the options...
// TODO figure out what to do in this situation
return this;
};
updateMultipleSelect = function () {
var value = this.fragment.getValue(), options, i;
if ( !isArray( value ) ) {
value = [ value ];
}
options = this.parentNode.options;
i = options.length;
while ( i-- ) {
options[i].selected = ( value.indexOf( options[i]._ractive.value ) !== -1 );
}
this.value = value;
return this;
};
updateRadioName = function () {
var node, value;
node = this.parentNode;
value = this.fragment.getValue();
node.checked = ( value === node._ractive.value );
return this;
};
updateCheckboxName = function () {
var node, value;
node = this.parentNode;
value = this.fragment.getValue();
if ( !isArray( value ) ) {
node.checked = ( value === node._ractive.value );
return this;
}
node.checked = ( value.indexOf( node._ractive.value ) !== -1 );
return this;
};
updateEverythingElse = function () {
var node, value;
node = this.parentNode;
value = this.fragment.getValue();
// store actual value, so it doesn't get coerced to a string
if ( this.isValueAttribute ) {
node._ractive.value = value;
}
if ( value === undefined ) {
value = '';
}
if ( value !== this.value ) {
if ( this.useProperty ) {
// with two-way binding, only update if the change wasn't initiated by the user
// otherwise the cursor will often be sent to the wrong place
if ( !this.receiving ) {
node[ this.propertyName ] = value;
}
this.value = value;
return this;
}
if ( this.namespace ) {
node.setAttributeNS( this.namespace, this.name, value );
this.value = value;
return this;
}
if ( this.name === 'id' ) {
if ( this.value !== undefined ) {
this.root.nodes[ this.value ] = undefined;
}
this.root.nodes[ value ] = node;
}
node.setAttribute( this.name, value );
this.value = value;
}
return this;
};
}());
addEventProxies = function ( element, proxies ) {
var i, eventName, eventNames;
for ( eventName in proxies ) {
if ( hasOwn.call( proxies, eventName ) ) {
eventNames = eventName.split( '-' );
i = eventNames.length;
while ( i-- ) {
addEventProxy( element, eventNames[i], proxies[ eventName ], element.parentFragment.contextStack );
}
}
}
};
(function () {
var MasterEventHandler,
ProxyEvent,
firePlainEvent,
fireEventWithArgs,
fireEventWithDynamicArgs,
customHandlers,
genericHandler,
getCustomHandler;
addEventProxy = function ( element, triggerEventName, proxyDescriptor, contextStack, indexRefs ) {
var events, master;
events = element.ractify().events;
master = events[ triggerEventName ] || ( events[ triggerEventName ] = new MasterEventHandler( element, triggerEventName, contextStack, indexRefs ) );
master.add( proxyDescriptor );
};
MasterEventHandler = function ( element, eventName, contextStack ) {
var definition;
this.element = element;
this.root = element.root;
this.node = element.node;
this.name = eventName;
this.contextStack = contextStack; // TODO do we need to pass contextStack down everywhere? Doesn't it belong to the parentFragment?
this.proxies = [];
if ( definition = ( this.root.eventDefinitions[ eventName ] || Ractive.eventDefinitions[ eventName ] ) ) {
this.custom = definition( this.node, getCustomHandler( eventName ) );
} else {
this.node.addEventListener( eventName, genericHandler, false );
}
};
MasterEventHandler.prototype = {
add: function ( proxy ) {
this.proxies[ this.proxies.length ] = new ProxyEvent( this.element, this.root, proxy, this.contextStack );
},
// TODO teardown when element torn down
teardown: function () {
var i;
if ( this.custom ) {
this.custom.teardown();
} else {
this.node.removeEventListener( this.name, genericHandler, false );
}
i = this.proxies.length;
while ( i-- ) {
this.proxies[i].teardown();
}
},
fire: function ( event ) {
var i = this.proxies.length;
while ( i-- ) {
this.proxies[i].fire( event );
}
}
};
ProxyEvent = function ( element, ractive, descriptor, contextStack ) {
var name;
this.root = ractive;
name = descriptor.n || descriptor;
if ( typeof name === 'string' ) {
this.n = name;
} else {
this.n = new StringFragment({
descriptor: descriptor.n,
root: this.root,
owner: element,
contextStack: contextStack
});
}
if ( descriptor.a ) {
this.a = descriptor.a;
this.fire = fireEventWithArgs;
return;
}
if ( descriptor.d ) {
this.d = new StringFragment({
descriptor: descriptor.d,
root: this.root,
owner: element,
contextStack: contextStack
});
this.fire = fireEventWithDynamicArgs;
return;
}
this.fire = firePlainEvent;
};
ProxyEvent.prototype = {
teardown: function () {
if ( this.n.teardown) {
this.n.teardown();
}
if ( this.d ) {
this.d.teardown();
}
},
bubble: noop // TODO can we get rid of this?
};
// the ProxyEvent instance fire method could be any of these
firePlainEvent = function ( event ) {
this.root.fire( this.n.toString(), event );
};
fireEventWithArgs = function ( event ) {
this.root.fire( this.n.toString(), event, this.a );
};
fireEventWithDynamicArgs = function ( event ) {
this.root.fire( this.n.toString(), event, this.d.toJSON() );
};
// all native DOM events dealt with by Ractive share a single handler
genericHandler = function ( event ) {
var storage = this._ractive;
storage.events[ event.type ].fire({
node: this,
original: event,
index: storage.index,
keypath: storage.keypath,
context: storage.root.get( storage.keypath )
});
};
customHandlers = {};
getCustomHandler = function ( eventName ) {
if ( customHandlers[ eventName ] ) {
return customHandlers[ eventName ];
}
return customHandlers[ eventName ] = function ( event ) {
var storage = event.node._ractive;
event.index = storage.index;
event.keypath = storage.keypath;
event.context = storage.root.get( storage.keypath );
storage.events[ eventName ].fire( event );
};
};
}());
appendElementChildren = function ( element, node, descriptor, docFrag ) {
if ( typeof descriptor.f === 'string' && ( !node || ( !node.namespaceURI || node.namespaceURI === namespaces.html ) ) ) {
// great! we can use innerHTML
element.html = descriptor.f;
if ( docFrag ) {
node.innerHTML = element.html;
}
}
else {
// once again, everyone has to suffer because of IE bloody 8
if ( descriptor.e === 'style' && node.styleSheet !== undefined ) {
element.fragment = new StringFragment({
descriptor: descriptor.f,
root: element.root,
contextStack: element.parentFragment.contextStack,
owner: element
});
if ( docFrag ) {
element.bubble = function () {
node.styleSheet.cssText = element.fragment.toString();
};
}
}
else {
element.fragment = new DomFragment({
descriptor: descriptor.f,
root: element.root,
parentNode: node,
contextStack: element.parentFragment.contextStack,
owner: element
});
if ( docFrag ) {
node.appendChild( element.fragment.docFrag );
}
}
}
};
bindElement = function ( element, attributes ) {
element.ractify();
// an element can only have one two-way attribute
switch ( element.descriptor.e ) {
case 'select':
case 'textarea':
if ( attributes.value ) {
attributes.value.bind();
}
return;
case 'input':
if ( element.node.type === 'radio' || element.node.type === 'checkbox' ) {
// we can either bind the name attribute, or the checked attribute - not both
if ( attributes.name && attributes.name.bind() ) {
return;
}
if ( attributes.checked && attributes.checked.bind() ) {
return;
}
}
if ( attributes.value && attributes.value.bind() ) {
return;
}
}
};
createElementAttributes = function ( element, attributes ) {
var attrName, attrValue, attr;
element.attributes = [];
for ( attrName in attributes ) {
if ( hasOwn.call( attributes, attrName ) ) {
attrValue = attributes[ attrName ];
attr = new DomAttribute({
element: element,
name: attrName,
value: attrValue,
root: element.root,
parentNode: element.node,
contextStack: element.parentFragment.contextStack
});
element.attributes[ element.attributes.length ] = attr;
// name, value and checked attributes are potentially bindable
if ( attrName === 'value' || attrName === 'name' || attrName === 'checked' ) {
element.attributes[ attrName ] = attr;
}
// The name attribute is a special case - it is the only two-way attribute that updates
// the viewmodel based on the value of another attribute. For that reason it must wait
// until the node has been initialised, and the viewmodel has had its first two-way
// update, before updating itself (otherwise it may disable a checkbox or radio that
// was enabled in the template)
if ( attrName !== 'name' ) {
attr.update();
}
}
}
return element.attributes;
};
getElementNamespace = function ( descriptor, parentNode ) {
// if the element has an xmlns attribute, use that
if ( descriptor.a && descriptor.a.xmlns ) {
return descriptor.a.xmlns;
}
// otherwise, use the svg namespace if this is an svg element, or inherit namespace from parent
return ( descriptor.e.toLowerCase() === 'svg' ? namespaces.svg : parentNode.namespaceURI );
};
executeTransition = function ( descriptor, root, owner, contextStack, isIntro ) {
var transitionName, transitionParams, fragment, transitionManager, transition;
if ( !root.transitionsEnabled ) {
return;
}
if ( typeof descriptor === 'string' ) {
transitionName = descriptor;
} else {
transitionName = descriptor.n;
if ( descriptor.a ) {
transitionParams = descriptor.a;
} else if ( descriptor.d ) {
fragment = new StringFragment({
descriptor: descriptor.d,
root: root,
owner: owner,
contextStack: owner.parentFragment.contextStack
});
transitionParams = fragment.toJSON();
fragment.teardown();
}
}
transition = root.transitions[ transitionName ] || Ractive.transitions[ transitionName ];
if ( transition ) {
transitionManager = root._transitionManager;
transitionManager.push( owner.node );
transition.call( root, owner.node, function () {
transitionManager.pop( owner.node );
}, transitionParams, isIntro );
}
};
getComponentConstructor = function ( root, name ) {
// TODO... write this properly!
return root.components[ name ];
};
(function () {
var elementCache = {};
insertHtml = function ( html, tagName, docFrag ) {
var container, nodes = [];
container = elementCache[ tagName ] || ( elementCache[ tagName ] = doc.createElement( tagName ) );
container.innerHTML = html;
while ( container.firstChild ) {
nodes[ nodes.length ] = container.firstChild;
docFrag.appendChild( container.firstChild );
}
return nodes;
};
}());
(function () {
var reassignFragment, reassignElement, reassignMustache;
reassignFragments = function ( root, section, start, end, by ) {
var i, fragment, indexRef, oldIndex, newIndex, oldKeypath, newKeypath;
indexRef = section.descriptor.i;
for ( i=start; i<end; i+=1 ) {
fragment = section.fragments[i];
oldIndex = i - by;
newIndex = i;
oldKeypath = section.keypath + '.' + ( i - by );
newKeypath = section.keypath + '.' + i;
// change the fragment index
fragment.index += by;
reassignFragment( fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
}
processDeferredUpdates( root );
};
reassignFragment = function ( fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ) {
var i, item, context;
if ( fragment.indexRefs && fragment.indexRefs[ indexRef ] !== undefined ) {
fragment.indexRefs[ indexRef ] = newIndex;
}
// fix context stack
i = fragment.contextStack.length;
while ( i-- ) {
context = fragment.contextStack[i];
if ( context.substr( 0, oldKeypath.length ) === oldKeypath ) {
fragment.contextStack[i] = context.replace( oldKeypath, newKeypath );
}
}
i = fragment.items.length;
while ( i-- ) {
item = fragment.items[i];
switch ( item.type ) {
case ELEMENT:
reassignElement( item, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
break;
case PARTIAL:
reassignFragment( item.fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
break;
case SECTION:
case INTERPOLATOR:
case TRIPLE:
reassignMustache( item, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
break;
}
}
};
reassignElement = function ( element, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ) {
var i, attribute, storage, masterEventName, proxies, proxy;
i = element.attributes.length;
while ( i-- ) {
attribute = element.attributes[i];
if ( attribute.fragment ) {
reassignFragment( attribute.fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
if ( attribute.twoway ) {
attribute.updateBindings();
}
}
}
if ( storage = element.node._ractive ) {
if ( storage.keypath.substr( 0, oldKeypath.length ) === oldKeypath ) {
storage.keypath = storage.keypath.replace( oldKeypath, newKeypath );
}
if ( indexRef !== undefined ) {
storage.index[ indexRef ] = newIndex;
}
for ( masterEventName in storage.events ) {
proxies = storage.events[ masterEventName ].proxies;
i = proxies.length;
while ( i-- ) {
proxy = proxies[i];
if ( typeof proxy.n === 'object' ) {
reassignFragment( proxy.a, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
}
if ( proxy.d ) {
reassignFragment( proxy.d, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
}
}
}
if ( storage.binding ) {
if ( storage.binding.keypath.substr( 0, oldKeypath.length ) === oldKeypath ) {
storage.binding.keypath = storage.binding.keypath.replace( oldKeypath, newKeypath );
}
}
}
// reassign children
if ( element.fragment ) {
reassignFragment( element.fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
}
};
reassignMustache = function ( mustache, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ) {
var i;
// expression mustache?
if ( mustache.descriptor.x ) {
if ( mustache.keypath ) {
unregisterDependant( mustache );
}
if ( mustache.expressionResolver ) {
mustache.expressionResolver.teardown();
}
mustache.expressionResolver = new ExpressionResolver( mustache );
}
// normal keypath mustache?
if ( mustache.keypath ) {
if ( mustache.keypath.substr( 0, oldKeypath.length ) === oldKeypath ) {
mustache.resolve( mustache.keypath.replace( oldKeypath, newKeypath ) );
}
}
// index ref mustache?
else if ( mustache.indexRef === indexRef ) {
mustache.value = newIndex;
mustache.render( newIndex );
}
// otherwise, it's an unresolved reference. the context stack has been updated
// so it will take care of itself
// if it's a section mustache, we need to go through any children
if ( mustache.fragments ) {
i = mustache.fragments.length;
while ( i-- ) {
reassignFragment( mustache.fragments[i], indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
}
}
};
}());
(function ( cache ) {
var Reference, SoftReference, getFunctionFromString, thisPattern, wrapFunction;
Evaluator = function ( root, keypath, functionStr, args, priority ) {
var i, arg;
this.root = root;
this.keypath = keypath;
this.priority = priority;
this.dependants = 0;
this.fn = getFunctionFromString( functionStr, args.length );
this.values = [];
this.refs = [];
i = args.length;
while ( i-- ) {
if ( arg = args[i] ) {
if ( arg[0] ) {
// this is an index ref... we don't need to register a dependant
this.values[i] = arg[1];
}
else {
this.refs[ this.refs.length ] = new Reference( root, arg[1], this, i, priority );
}
}
else {
this.values[i] = undefined;
}
}
this.selfUpdating = ( this.refs.length <= 1 );
};
Evaluator.prototype = {
wake: function () {
this.awake = true;
this.update();
},
sleep: function () {
this.awake = false;
},
bubble: function () {
if ( !this.awake ) {
return;
}
// If we only have one reference, we can update immediately...
if ( this.selfUpdating ) {
this.update();
}
// ...otherwise we want to register it as a deferred item, to be
// updated once all the information is in, to prevent unnecessary
// cascading. Only if we're already resolved, obviously
else if ( !this.deferred ) {
this.root._defEvals[ this.root._defEvals.length ] = this;
this.deferred = true;
}
},
update: function () {
var value;
// prevent infinite loops
if ( this.evaluating ) {
return this;
}
this.evaluating = true;
try {
value = this.fn.apply( null, this.values );
} catch ( err ) {
if ( this.root.debug ) {
throw err;
} else {
value = undefined;
}
}
if ( !isEqual( value, this.value ) ) {
clearCache( this.root, this.keypath );
this.root._cache[ this.keypath ] = value;
notifyDependants( this.root, this.keypath );
this.value = value;
}
this.evaluating = false;
return this;
},
// TODO should evaluators ever get torn down? At present, they don't...
teardown: function () {
while ( this.refs.length ) {
this.refs.pop().teardown();
}
clearCache( this.root, this.keypath );
this.root._evaluators[ this.keypath ] = null;
},
// This method forces the evaluator to sync with the current model
// in the case of a smart update
refresh: function () {
if ( !this.selfUpdating ) {
this.deferred = true;
}
var i = this.refs.length;
while ( i-- ) {
this.refs[i].update();
}
if ( this.deferred ) {
this.update();
this.deferred = false;
}
},
updateSoftDependencies: function ( softDeps ) {
var i, keypath, ref;
if ( !this.softRefs ) {
this.softRefs = [];
}
// teardown any references that are no longer relevant
i = this.softRefs.length;
while ( i-- ) {
ref = this.softRefs[i];
if ( !softDeps[ ref.keypath ] ) {
this.softRefs.splice( i, 1 );
this.softRefs[ ref.keypath ] = false;
ref.teardown();
}
}
// add references for any new soft dependencies
i = softDeps.length;
while ( i-- ) {
keypath = softDeps[i];
if ( !this.softRefs[ keypath ] ) {
ref = new SoftReference( this.root, keypath, this );
this.softRefs[ this.softRefs.length ] = ref;
this.softRefs[ keypath ] = true;
}
}
this.selfUpdating = ( this.refs.length + this.softRefs.length <= 1 );
}
};
Reference = function ( root, keypath, evaluator, argNum, priority ) {
var value;
this.evaluator = evaluator;
this.keypath = keypath;
this.root = root;
this.argNum = argNum;
this.type = REFERENCE;
this.priority = priority;
value = root.get( keypath );
if ( typeof value === 'function' ) {
value = value._wrapped || wrapFunction( value, root, evaluator );
}
this.value = evaluator.values[ argNum ] = value;
registerDependant( this );
};
Reference.prototype = {
update: function () {
var value = this.root.get( this.keypath );
if ( typeof value === 'function' && !value._nowrap ) {
value = value[ '_' + this.root._guid ] || wrapFunction( value, this.root, this.evaluator );
}
if ( !isEqual( value, this.value ) ) {
this.evaluator.values[ this.argNum ] = value;
this.evaluator.bubble();
this.value = value;
}
},
teardown: function () {
unregisterDependant( this );
}
};
SoftReference = function ( root, keypath, evaluator ) {
this.root = root;
this.keypath = keypath;
this.priority = evaluator.priority;
this.evaluator = evaluator;
registerDependant( this );
};
SoftReference.prototype = {
update: function () {
var value = this.root.get( this.keypath );
if ( !isEqual( value, this.value ) ) {
this.evaluator.bubble();
this.value = value;
}
},
teardown: function () {
unregisterDependant( this );
}
};
getFunctionFromString = function ( str, i ) {
var fn, args;
str = str.replace( /\$\{([0-9]+)\}/g, '_$1' );
if ( cache[ str ] ) {
return cache[ str ];
}
args = [];
while ( i-- ) {
args[i] = '_' + i;
}
fn = new Function( args.join( ',' ), 'return(' + str + ')' );
cache[ str ] = fn;
return fn;
};
thisPattern = /this/;
wrapFunction = function ( fn, ractive, evaluator ) {
var prop;
// if the function doesn't refer to `this`, we don't need
// to set the context
if ( !thisPattern.test( fn.toString() ) ) {
defineProperty( fn, '_nowrap', { // no point doing this every time
value: true
});
return fn;
}
// otherwise, we do
defineProperty( fn, '_' + ractive._guid, {
value: function () {
var originalGet, result, softDependencies;
originalGet = ractive.get;
ractive.get = function ( keypath ) {
if ( !softDependencies ) {
softDependencies = [];
}
if ( !softDependencies[ keypath ] ) {
softDependencies[ softDependencies.length ] = keypath;
softDependencies[ keypath ] = true;
}
return originalGet.call( ractive, keypath );
};
result = fn.apply( ractive, arguments );
if ( softDependencies ) {
evaluator.updateSoftDependencies( softDependencies );
}
// reset
ractive.get = originalGet;
return result;
},
writable: true
});
for ( prop in fn ) {
if ( hasOwn.call( fn, prop ) ) {
fn[ '_' + ractive._guid ][ prop ] = fn[ prop ];
}
}
return fn[ '_' + ractive._guid ];
};
}({}));
(function () {
var ReferenceScout, getKeypath;
ExpressionResolver = function ( mustache ) {
var expression, i, len, ref, indexRefs;
this.root = mustache.root;
this.mustache = mustache;
this.args = [];
this.scouts = [];
expression = mustache.descriptor.x;
indexRefs = mustache.parentFragment.indexRefs;
this.str = expression.s;
// send out scouts for each reference
len = this.unresolved = this.args.length = ( expression.r ? expression.r.length : 0 );
if ( !len ) {
this.resolved = this.ready = true;
this.bubble(); // some expressions don't have references. edge case, but, yeah.
return;
}
for ( i=0; i<len; i+=1 ) {
ref = expression.r[i];
// is this an index ref?
if ( indexRefs && indexRefs[ ref ] !== undefined ) {
this.resolveRef( i, true, indexRefs[ ref ] );
}
else {
this.scouts[ this.scouts.length ] = new ReferenceScout( this, ref, mustache.contextStack, i );
}
}
this.ready = true;
this.bubble();
};
ExpressionResolver.prototype = {
bubble: function () {
if ( !this.ready ) {
return;
}
this.keypath = getKeypath( this.str, this.args );
this.createEvaluator();
this.mustache.resolve( this.keypath );
},
teardown: function () {
while ( this.scouts.length ) {
this.scouts.pop().teardown();
}
},
resolveRef: function ( argNum, isIndexRef, value ) {
this.args[ argNum ] = [ isIndexRef, value ];
this.bubble();
// when all references have been resolved, we can flag the entire expression
// as having been resolved
this.resolved = !( --this.unresolved );
},
createEvaluator: function () {
// only if it doesn't exist yet!
if ( !this.root._evaluators[ this.keypath ] ) {
this.root._evaluators[ this.keypath ] = new Evaluator( this.root, this.keypath, this.str, this.args, this.mustache.priority );
}
else {
// we need to trigger a refresh of the evaluator, since it
// will have become de-synced from the model if we're in a
// reassignment cycle
this.root._evaluators[ this.keypath ].refresh();
}
}
};
ReferenceScout = function ( resolver, ref, contextStack, argNum ) {
var keypath, root;
root = this.root = resolver.root;
keypath = resolveRef( root, ref, contextStack );
if ( keypath ) {
resolver.resolveRef( argNum, false, keypath );
} else {
this.ref = ref;
this.argNum = argNum;
this.resolver = resolver;
this.contextStack = contextStack;
root._pendingResolution[ root._pendingResolution.length ] = this;
}
};
ReferenceScout.prototype = {
resolve: function ( keypath ) {
this.keypath = keypath;
this.resolver.resolveRef( this.argNum, false, keypath );
},
teardown: function () {
// if we haven't found a keypath yet, we can
// stop the search now
if ( !this.keypath ) {
teardown( this );
}
}
};
getKeypath = function ( str, args ) {
var unique;
// get string that is unique to this expression
unique = str.replace( /\$\{([0-9]+)\}/g, function ( match, $1 ) {
return args[ $1 ] ? args[ $1 ][1] : 'undefined';
});
// then sanitize by removing any periods or square brackets. Otherwise
// we can't split the keypath into keys!
return '(' + unique.replace( /[\.\[\]]/g, '-' ) + ')';
};
}());
(function () {
var getPartialFromRegistry, unpack;
getPartialDescriptor = function ( root, name ) {
var el, partial;
// If the partial was specified on this instance, great
if ( partial = getPartialFromRegistry( root, name ) ) {
return partial;
}
// If not, is it a global partial?
if ( partial = getPartialFromRegistry( Ractive, name ) ) {
return partial;
}
// Does it exist on the page as a script tag?
if ( doc ) {
el = doc.getElementById( name );
if ( el && el.tagName === 'SCRIPT' ) {
if ( !Ractive.parse ) {
throw new Error( missingParser );
}
Ractive.partials[ name ] = Ractive.parse( el.innerHTML );
}
}
partial = Ractive.partials[ name ];
// No match? Return an empty array
if ( !partial ) {
if ( root.debug && console && console.warn ) {
console.warn( 'Could not find descriptor for partial "' + name + '"' );
}
return [];
}
return unpack( partial );
};
getPartialFromRegistry = function ( registry, name ) {
var partial, key;
if ( registry.partials[ name ] ) {
// If this was added manually to the registry, but hasn't been parsed,
// parse it now
if ( typeof registry.partials[ name ] === 'string' ) {
if ( !Ractive.parse ) {
throw new Error( missingParser );
}
partial = Ractive.parse( registry.partials[ name ], registry.parseOptions );
if ( isObject( partial ) ) {
registry.partials[ name ] = partial.main;
for ( key in partial.partials ) {
if ( partial.partials.hasOwnProperty( key ) ) {
registry.partials[ key ] = partial.partials[ key ];
}
}
} else {
registry.partials[ name ] = partial;
}
}
return unpack( registry.partials[ name ] );
}
};
unpack = function ( partial ) {
// Unpack string, if necessary
if ( partial.length === 1 && typeof partial[0] === 'string' ) {
return partial[0];
}
return partial;
};
}());
initFragment = function ( fragment, options ) {
var numItems, i, parentFragment, parentRefs, ref;
// The item that owns this fragment - an element, section, partial, or attribute
fragment.owner = options.owner;
parentFragment = fragment.owner.parentFragment;
// inherited properties
fragment.root = options.root;
fragment.parentNode = options.parentNode;
fragment.contextStack = options.contextStack || [];
// If parent item is a section, this may not be the only fragment
// that belongs to it - we need to make a note of the index
if ( fragment.owner.type === SECTION ) {
fragment.index = options.index;
}
// index references (the 'i' in {{#section:i}}<!-- -->{{/section}}) need to cascade
// down the tree
if ( parentFragment ) {
parentRefs = parentFragment.indexRefs;
if ( parentRefs ) {
fragment.indexRefs = createFromNull(); // avoids need for hasOwnProperty
for ( ref in parentRefs ) {
fragment.indexRefs[ ref ] = parentRefs[ ref ];
}
}
}
// inherit priority
fragment.priority = ( parentFragment ? parentFragment.priority + 1 : 1 );
if ( options.indexRef ) {
if ( !fragment.indexRefs ) {
fragment.indexRefs = {};
}
fragment.indexRefs[ options.indexRef ] = options.index;
}
// Time to create this fragment's child items;
fragment.items = [];
numItems = ( options.descriptor ? options.descriptor.length : 0 );
for ( i=0; i<numItems; i+=1 ) {
fragment.items[ fragment.items.length ] = fragment.createItem({
parentFragment: fragment,
descriptor: options.descriptor[i],
index: i
});
}
};
isStringFragmentSimple = function ( fragment ) {
var i, item, containsInterpolator;
i = fragment.items.length;
while ( i-- ) {
item = fragment.items[i];
if ( item.type === TEXT ) {
continue;
}
// we can only have one interpolator and still be self-updating
if ( item.type === INTERPOLATOR ) {
if ( containsInterpolator ) {
return false;
} else {
containsInterpolator = true;
continue;
}
}
// anything that isn't text or an interpolator (i.e. a section)
// and we can't self-update
return false;
}
return true;
};
initMustache = function ( mustache, options ) {
var keypath, indexRef, parentFragment;
parentFragment = mustache.parentFragment = options.parentFragment;
mustache.root = parentFragment.root;
mustache.contextStack = parentFragment.contextStack;
mustache.descriptor = options.descriptor;
mustache.index = options.index || 0;
mustache.priority = parentFragment.priority;
// DOM only
if ( parentFragment.parentNode ) {
mustache.parentNode = parentFragment.parentNode;
}
mustache.type = options.descriptor.t;
// if this is a simple mustache, with a reference, we just need to resolve
// the reference to a keypath
if ( options.descriptor.r ) {
if ( parentFragment.indexRefs && parentFragment.indexRefs[ options.descriptor.r ] !== undefined ) {
indexRef = parentFragment.indexRefs[ options.descriptor.r ];
mustache.indexRef = options.descriptor.r;
mustache.value = indexRef;
mustache.render( mustache.value );
}
else {
keypath = resolveRef( mustache.root, options.descriptor.r, mustache.contextStack );
if ( keypath ) {
mustache.resolve( keypath );
} else {
mustache.ref = options.descriptor.r;
mustache.root._pendingResolution[ mustache.root._pendingResolution.length ] = mustache;
// inverted section? initialise
if ( mustache.descriptor.n ) {
mustache.render( false );
}
}
}
}
// if it's an expression, we have a bit more work to do
if ( options.descriptor.x ) {
mustache.expressionResolver = new ExpressionResolver( mustache );
}
};
// methods to add to individual mustache prototypes
updateMustache = function () {
var value = this.root.get( this.keypath, true );
if ( !isEqual( value, this.value ) ) {
this.