ractive
Version:
Next-generation DOM manipulation
2,139 lines (1,651 loc) • 134 kB
JavaScript
/*! Ractive - v0.3.4 - 2013-08-06
* Next-generation DOM manipulation
* http://rich-harris.github.com/Ractive/
* Copyright (c) 2013 Rich Harris; Licensed MIT */
/*jslint eqeq: true, plusplus: true */
/*global document, HTMLElement */
(function ( global ) {
'use strict';
var Ractive,
// current version
VERSION = '0.3.4',
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,
registerIndexRef,
unregisterIndexRef,
resolveRef,
processDeferredUpdates,
// internal utils
splitKeypath,
toString,
isArray,
isObject,
isNumeric,
isEqual,
getEl,
insertHtml,
reassignFragments,
executeTransition,
getPartialDescriptor,
makeTransitionManager,
requestAnimationFrame,
defineProperty,
defineProperties,
create,
createFromNull,
hasOwn = {}.hasOwnProperty,
noop = function () {},
// internally used caches
keypathCache = {},
// internally used constructors
DomFragment,
DomElement,
DomAttribute,
DomPartial,
DomInterpolator,
DomTriple,
DomSection,
DomText,
StringFragment,
StringPartial,
StringInterpolator,
StringSection,
StringText,
ExpressionResolver,
Evaluator,
Animation,
// internally used regexes
leadingWhitespace = /^\s+/,
trailingWhitespace = /\s+$/,
// other bits and pieces
render,
initMustache,
updateMustache,
resolveMustache,
evaluateMustache,
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,
ATTR_VALUE_TOKEN = 13,
EXPRESSION = 14,
NUMBER_LITERAL = 20,
STRING_LITERAL = 21,
ARRAY_LITERAL = 22,
OBJECT_LITERAL = 23,
BOOLEAN_LITERAL = 24,
LITERAL = 25,
GLOBAL = 26,
KEY_VALUE_PAIR = 27,
REFERENCE = 30,
REFINEMENT = 31,
MEMBER = 32,
PREFIX_OPERATOR = 33,
BRACKETED = 34,
CONDITIONAL = 35,
INFIX_OPERATOR = 36,
INVOCATION = 40,
UNSET = { unset: true },
testDiv = ( doc ? doc.createElement( 'div' ) : null ),
// 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 {
Object.defineProperty({}, 'test', { value: 0 });
Object.defineProperties({}, { test: { value: 0 } });
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;
}
}());
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 TextFragment({
descriptor: descriptor.d,
root: root,
owner: owner,
contextStack: 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, transitionManager.info, isIntro );
}
};
insertHtml = function ( html, docFrag ) {
var div, nodes = [];
div = doc.createElement( 'div' );
div.innerHTML = html;
while ( div.firstChild ) {
nodes[ nodes.length ] = div.firstChild;
docFrag.appendChild( div.firstChild );
}
return nodes;
};
(function () {
var reassignFragment, reassignElement, reassignMustache;
reassignFragments = function ( root, section, start, end, by ) {
var fragmentsToReassign, 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, j, 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;
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();
}
}
}
// reassign proxy argument fragments TODO and intro/outro param fragments
if ( element.proxyFrags ) {
i = element.proxyFrags.length;
while ( i-- ) {
reassignFragment( element.proxyFrags[i], indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath );
}
}
if ( element.node._ractive ) {
if ( element.node._ractive.keypath.substr( 0, oldKeypath.length ) === oldKeypath ) {
element.node._ractive.keypath = element.node._ractive.keypath.replace( oldKeypath, newKeypath );
}
element.node._ractive.index[ indexRef ] = newIndex;
}
// 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 ) {
unregisterDependant( mustache );
mustache.keypath = mustache.keypath.replace( oldKeypath, newKeypath );
registerDependant( mustache );
}
}
// 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, getFunctionFromString;
Evaluator = function ( root, keypath, functionStr, args, priority ) {
var i, arg;
this.root = root;
this.keypath = keypath;
this.fn = getFunctionFromString( functionStr, args.length );
this.values = [];
this.refs = [];
i = args.length;
while ( i-- ) {
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 );
}
}
this.selfUpdating = ( this.refs.length <= 1 );
this.update();
};
Evaluator.prototype = {
bubble: function () {
// 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;
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;
}
return this;
},
// TODO should evaluators ever get torn down?
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;
}
}
};
Reference = function ( root, keypath, evaluator, argNum, priority ) {
this.evaluator = evaluator;
this.keypath = keypath;
this.root = root;
this.argNum = argNum;
this.type = REFERENCE;
this.priority = priority;
this.value = evaluator.values[ argNum ] = root.get( keypath );
registerDependant( this );
};
Reference.prototype = {
update: function () {
var value = this.root.get( this.keypath );
if ( !isEqual( value, this.value ) ) {
this.evaluator.values[ this.argNum ] = 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;
};
}({}));
(function () {
var ReferenceScout, getKeypath;
ExpressionResolver = function ( mustache ) {
var expression, i, len, ref, indexRefs, args;
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 = ( expression.r ? expression.r.length : 0 );
if ( !len ) {
this.init(); // some expressions don't have references. edge case, but, yeah.
}
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 );
}
}
};
ExpressionResolver.prototype = {
init: function () {
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 ];
// can we initialise yet?
if ( --this.unresolved ) {
// no;
return;
}
this.init();
},
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 ][1];
});
// then sanitize by removing any periods or square brackets. Otherwise
// splitKeypath will go mental!
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 ) {
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 );
}
registry.partials[ name ] = Ractive.parse( registry.partials[ name ] );
}
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, itemOptions, 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 : 0 );
if ( options.indexRef ) {
if ( !fragment.indexRefs ) {
fragment.indexRefs = {};
}
fragment.indexRefs[ options.indexRef ] = options.index;
}
// Time to create this fragment's child items;
fragment.items = [];
itemOptions = {
parentFragment: fragment
};
numItems = ( options.descriptor ? options.descriptor.length : 0 );
for ( i=0; i<numItems; i+=1 ) {
itemOptions.descriptor = options.descriptor[i];
itemOptions.index = i;
fragment.items[ fragment.items.length ] = fragment.createItem( itemOptions );
}
};
initMustache = function ( mustache, options ) {
var keypath, index, 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;
value = this.root.get( this.keypath, true );
if ( !isEqual( value, this.value ) ) {
this.render( value );
this.value = value;
}
};
resolveMustache = function ( keypath ) {
this.keypath = keypath;
registerDependant( this );
this.update();
if ( this.expressionResolver ) {
this.expressionResolver = null;
}
};
(function () {
var updateInvertedSection, updateListSection, updateContextSection, updateConditionalSection;
updateSection = function ( section, value ) {
var fragmentOptions;
fragmentOptions = {
descriptor: section.descriptor.f,
root: section.root,
parentNode: section.parentNode,
owner: section
};
// if section is inverted, only check for truthiness/falsiness
if ( section.descriptor.n ) {
updateConditionalSection( section, value, true, fragmentOptions );
return;
}
// otherwise we need to work out what sort of section we're dealing with
// if value is an array, iterate through
if ( isArray( value ) ) {
updateListSection( section, value, fragmentOptions );
}
// if value is a hash...
else if ( isObject( value ) ) {
updateContextSection( section, fragmentOptions );
}
// otherwise render if value is truthy, unrender if falsy
else {
updateConditionalSection( section, value, false, fragmentOptions );
}
};
updateListSection = function ( section, value, fragmentOptions ) {
var i, fragmentsToRemove;
// if the array is shorter than it was previously, remove items
if ( value.length < section.length ) {
fragmentsToRemove = section.fragments.splice( value.length, section.length - value.length );
while ( fragmentsToRemove.length ) {
fragmentsToRemove.pop().teardown( true );
}
}
// otherwise...
else {
if ( value.length > section.length ) {
// add any new ones
for ( i=section.length; i<value.length; i+=1 ) {
// append list item to context stack
fragmentOptions.contextStack = section.contextStack.concat( section.keypath + '.' + i );
fragmentOptions.index = i;
if ( section.descriptor.i ) {
fragmentOptions.indexRef = section.descriptor.i;
}
section.fragments[i] = section.createFragment( fragmentOptions );
}
}
}
section.length = value.length;
};
updateContextSection = function ( section, fragmentOptions ) {
// ...then if it isn't rendered, render it, adding section.keypath to the context stack
// (if it is already rendered, then any children dependent on the context stack
// will update themselves without any prompting)
if ( !section.length ) {
// append this section to the context stack
fragmentOptions.contextStack = section.contextStack.concat( section.keypath );
fragmentOptions.index = 0;
section.fragments[0] = section.createFragment( fragmentOptions );
section.length = 1;
}
};
updateConditionalSection = function ( section, value, inverted, fragmentOptions ) {
var doRender, emptyArray, fragmentsToRemove;
emptyArray = ( isArray( value ) && value.length === 0 );
if ( inverted ) {
doRender = emptyArray || !value;
} else {
doRender = value && !emptyArray;
}
if ( doRender ) {
if ( !section.length ) {
// no change to context stack
fragmentOptions.contextStack = section.contextStack;
fragmentOptions.index = 0;
section.fragments[0] = section.createFragment( fragmentOptions );
section.length = 1;
}
if ( section.length > 1 ) {
fragmentsToRemove = section.fragments.splice( 1 );
while ( fragmentsToRemove.length ) {
fragmentsToRemove.pop().teardown( true );
}
}
}
else if ( section.length ) {
section.teardownFragments( true );
section.length = 0;
}
};
}());
(function ( proto ) {
var add = function ( root, keypath, d ) {
var value;
if ( typeof keypath !== 'string' || !isNumeric( d ) ) {
if ( root.debug ) {
throw new Error( 'Bad arguments' );
}
return;
}
value = root.get( keypath );
if ( value === undefined ) {
value = 0;
}
if ( !isNumeric( value ) ) {
if ( root.debug ) {
throw new Error( 'Cannot add to a non-numeric value' );
}
return;
}
root.set( keypath, value + d );
};
proto.add = function ( keypath, d ) {
add( this, keypath, ( d === undefined ? 1 : d ) );
};
proto.subtract = function ( keypath, d ) {
add( this, keypath, ( d === undefined ? -1 : -d ) );
};
proto.toggle = function ( keypath ) {
var value;
if ( typeof keypath !== 'string' ) {
if ( this.debug ) {
throw new Error( 'Bad arguments' );
}
return;
}
value = this.get( keypath );
this.set( keypath, !value );
};
}( proto ));
(function ( proto ) {
var animate, noAnimation;
proto.animate = function ( keypath, to, options ) {
var k, animation, animations;
// animate multiple keypaths
if ( typeof keypath === 'object' ) {
options = to || {};
animations = [];
for ( k in keypath ) {
if ( hasOwn.call( keypath, k ) ) {
animations[ animations.length ] = animate( this, k, keypath[k], options );
}
}
return {
stop: function () {
while ( animations.length ) {
animations.pop().stop();
}
}
};
}
// animate a single keypath
options = options || {};
animation = animate( this, keypath, to, options );
return {
stop: function () {
animation.stop();
}
};
};
noAnimation = {
stop: noop
};
animate = function ( root, keypath, to, options ) {
var easing, duration, animation, i, keys, from;
from = root.get( keypath );
// cancel any existing animation
// TODO what about upstream/downstream keypaths?
i = animationCollection.animations.length;
while ( i-- ) {
if ( animationCollection.animations[ i ].keypath === keypath ) {
animationCollection.animations[ i ].stop();
}
}
// don't bother animating values that stay the same
if ( isEqual( from, to ) ) {
if ( options.complete ) {
options.complete( 1, options.to );
}
return noAnimation;
}
// easing function
if ( options.easing ) {
if ( typeof options.easing === 'function' ) {
easing = options.easing;
}
else {
if ( root.easing && root.easing[ options.easing ] ) {
// use instance easing function first
easing = root.easing[ options.easing ];
} else {
// fallback to global easing functions
easing = Ractive.easing[ options.easing ];
}
}
if ( typeof easing !== 'function' ) {
easing = null;
}
}
// duration
duration = ( options.duration === undefined ? 400 : options.duration );
// TODO store keys, use an internal set method
//keys = splitKeypath( keypath );
animation = new Animation({
keypath: keypath,
from: from,
to: to,
root: root,
duration: duration,
easing: easing,
step: options.step,
complete: options.complete
});
animationCollection.push( animation );
root._animations[ root._animations.length ] = animation;
return animation;
};
}( proto ));
proto.bind = function ( adaptor ) {
var bound = this._bound;
if ( bound.indexOf( adaptor ) === -1 ) {
bound[ bound.length ] = adaptor;
adaptor.init( this );
}
};
proto.cancelFullscreen = function () {
Ractive.cancelFullscreen( this.el );
};
proto.find = function ( selector ) {
if ( !this.el ) {
return null;
}
return this.el.querySelector( selector );
};
proto.findAll = function ( selector ) {
if ( !this.el ) {
return [];
}
return this.el.querySelectorAll( selector );
};
proto.fire = function ( eventName ) {
var args, i, len, subscribers = this._subs[ eventName ];
if ( !subscribers ) {
return;
}
args = Array.prototype.slice.call( arguments, 1 );
for ( i=0, len=subscribers.length; i<len; i+=1 ) {
subscribers[i].apply( this, args );
}
};
// TODO use dontNormalise
// TODO refactor this shitball
proto.get = function ( keypath, dontNormalise ) {
var cache, cacheMap, keys, normalised, key, parentKeypath, parentValue, value, ignoreUndefined;
if ( !keypath ) {
return this.data;
}
cache = this._cache;
if ( isArray( keypath ) ) {
if ( !keypath.length ) {
return this.data;
}
keys = keypath.slice(); // clone
normalised = keys.join( '.' );
ignoreUndefined = true; // because this should be a branch, sod the cache
}
else {
// cache hit? great
if ( hasOwn.call( cache, keypath ) && cache[ keypath ] !== UNSET ) {
return cache[ keypath ];
}
keys = splitKeypath( keypath );
normalised = keys.join( '.' );
}
// we may have a cache hit now that it's been normalised
if ( hasOwn.call( cache, normalised ) && cache[ normalised ] !== UNSET ) {
if ( cache[ normalised ] === undefined && ignoreUndefined ) {
// continue
} else {
return cache[ normalised ];
}
}
// is this an uncached evaluator value?
if ( this._evaluators[ normalised ] ) {
value = this._evaluators[ normalised ].value;
cache[ normalised ] = value;
return value;
}
// otherwise it looks like we need to do some work
key = keys.pop();
parentKeypath = keys.join( '.' );
parentValue = ( keys.length ? this.get( keys ) : this.data );
if ( parentValue === null || typeof parentValue !== 'object' || parentValue === UNSET ) {
return;
}
// update cache map
if ( !( cacheMap = this._cacheMap[ parentKeypath ] ) ) {
this._cacheMap[ parentKeypath ] = [ normalised ];
} else {
if ( cacheMap.indexOf( normalised ) === -1 ) {
cacheMap[ cacheMap.length ] = normalised;
}
}
value = parentValue[ key ];
// Is this an array that needs to be wrapped?
if ( this.modifyArrays ) {
// if it's not an expression, is an array, and we're not here because it sent us here, wrap it
if ( ( normalised.charAt( 0 ) !== '(' ) && isArray( value ) && ( !value._ractive || !value._ractive.setting ) ) {
registerKeypathToArray( value, normalised, this );
}
}
// Update cache
cache[ normalised ] = value;
return value;
};
clearCache = function ( ractive, keypath ) {
var value, len, kp, cacheMap;
// is this a modified array, which shouldn't fire set events on this keypath anymore?
if ( ractive.modifyArrays ) {
if ( keypath.charAt( 0 ) !== '(' ) { // expressions (and their children) don't get wrapped
value = ractive._cache[ keypath ];
if ( isArray( value ) && !value._ractive.setting ) {
unregisterKeypathFromArray( value, keypath, ractive );
}
}
}
ractive._cache[ keypath ] = UNSET;
if ( cacheMap = ractive._cacheMap[ keypath ] ) {
while ( cacheMap.length ) {
clearCache( ractive, cacheMap.pop() );
}
}
};
notifyDependants = function ( ractive, keypath, onlyDirect ) {
var i;
for ( i=0; i<ractive._deps.length; i+=1 ) { // can't cache ractive._deps.length, it may change
notifyDependantsByPriority( ractive, keypath, i, onlyDirect );
}
};
notifyDependantsByPriority = function ( ractive, keypath, priority, onlyDirect ) {
var depsByKeypath, deps, i, len, childDeps;
depsByKeypath = ractive._deps[ priority ];
if ( !depsByKeypath ) {
return;
}
deps = depsByKeypath[ keypath ];
if ( deps ) {
i = deps.length;
while ( i-- ) {
deps[i].update();
}
}
// If we're only notifying direct dependants, not dependants
// of downstream keypaths, then YOU SHALL NOT PASS
if ( onlyDirect ) {
return;
}
// cascade
childDeps = ractive._depsMap[ keypath ];
if ( childDeps ) {
i = childDeps.length;
while ( i-- ) {
notifyDependantsByPriority( ractive, childDeps[i], priority );
}
}
};
notifyMultipleDependants = function ( ractive, keypaths, onlyDirect ) {
var i, j, len;
len = keypaths.length;
for ( i=0; i<ractive._deps.length; i+=1 ) {
if ( ractive._deps[i] ) {
j = len;
while ( j-- ) {
notifyDependantsByPriority( ractive, keypaths[j], i, onlyDirect );
}
}
}
};
processDeferredUpdates = function ( ractive ) {
var evaluator, attribute;
while ( ractive._defEvals.length ) {
evaluator = ractive._defEvals.pop();
evaluator.update().deferred = false;
}
while ( ractive._defAttrs.length ) {
attribute = ractive._defAttrs.pop();
attribute.update().deferred = false;
}
while ( ractive._defSelectValues.length ) {
attribute = ractive._defSelectValues.pop();
attribute.parentNode.value = attribute.value;
// value may not be what we think it should be, if the relevant <option>
// element doesn't exist!
attribute.value = attribute.parentNode.value;
}
};
registerDependant = function ( dependant ) {
var depsByKeypath, deps, keys, parentKeypath, map, ractive, keypath, priority;
ractive = dependant.root;
keypath = dependant.keypath;
priority = dependant.priority;
depsByKeypath = ractive._deps[ priority ] || ( ractive._deps[ priority ] = {} );
deps = depsByKeypath[ keypath ] || ( depsByKeypath[ keypath ] = [] );
deps[ deps.length ] = dependant;
// update dependants map
keys = splitKeypath( keypath );
while ( keys.length ) {
keys.pop();
parentKeypath = keys.join( '.' );
map = ractive._depsMap[ parentKeypath ] || ( ractive._depsMap[ parentKeypath ] = [] );
if ( map[ keypath ] === undefined ) {
map[ keypath ] = 0;
map[ map.length ] = keypath;
}
map[ keypath ] += 1;
keypath = parentKeypath;
}
};
// Render instance to element specified here or at initialization
render = function ( ractive, options ) {
var el, transitionManager;
el = ( options.el ? getEl( options.el ) : ractive.el );
// Clear the element, unless `append` is `true`
if ( el && !options.append ) {
el.innerHTML = '';
}
ractive._transitionManager = transitionManager = makeTransitionManager( ractive, options.complete );
// Render our *root fragment*
ractive.fragment = new DomFragment({
descriptor: ractive.template,
root: ractive,
owner: ractive, // saves doing `if ( ractive.parent ) { /*...*/ }` later on
parentNode: el
});
processDeferredUpdates( ractive );
if ( el ) {
el.appendChild( ractive.fragment.docFrag );
}
// transition manager has finished its work
ractive._transitionManager = null;
transitionManager.ready();
};
// Resolve a full keypath from `ref` within the given `contextStack` (e.g.
// `'bar.baz'` within the context stack `['foo']` might resolve to `'foo.bar.baz'`
resolveRef = function ( ractive, ref, contextStack ) {
var keys, lastKey, innerMostContext, contextKeys, parentValue, keypath;
// Implicit iterators - i.e. {{.}} - are a special case
if ( ref === '.' ) {
return contextStack[ contextStack.length - 1 ];
}
// References prepended with '.' are another special case
if ( ref.charAt( 0 ) === '.' ) {
return contextStack[ contextStack.length - 1 ] + ref;
}
keys = splitKeypath( ref );
lastKey = keys.pop();
// Clone the context stack, so we don't mutate the original
contextStack = contextStack.concat();
// Take each context from the stack, working backwards from the innermost context
while ( contextStack.length ) {
innerMostContext = contextStack.pop();
contextKeys = splitKeypath( innerMostContext );
parentValue = ractive.get( contextKeys.concat( keys ) );
if ( typeof parentValue === 'object' && parentValue !== null && hasOwn.call( parentValue, lastKey ) ) {
keypath = innerMostContext + '.' + ref;
break;
}
}
if ( !keypath && ractive.get( ref ) !== undefined ) {
keypath = ref;
}
return keypath;
};
teardown = function ( thing ) {
if ( !thing.keypath ) {
// this was on the 'unresolved' list, we need to remove it
var index = thing.root._pendingResolution.indexOf( thing );
if ( index !== -1 ) {
thing.root._pendingResolution.splice( index, 1 );
}
} else {
// this was registered as a dependant
unregisterDependant( thing );
}
};
unregisterDependant = function ( dependant ) {
var deps, i, keep, keys, parentKeypath, map, evaluator, ractive, keypath, priority;
ractive = dependant.root;
keypath = dependant.keypath;
priority = dependant.priority;
deps = ractive._deps[ priority ][ keypath ];
deps.splice( deps.indexOf( dependant ), 1 );
// update dependants map
keys = splitKeypath( keypath );
while ( keys.length ) {
keys.pop();
parentKeypath = keys.join( '.' );
map = ractive._depsMap[ parentKeypath ];
map[ keypath ] -= 1;
if ( !map[ keypath ] ) {
// remove from parent deps map
map.splice( map.indexOf( keypath ), 1 );
map[ keypath ] = undefined;
}
keypath = parentKeypath;
}
};
proto.link = function ( keypath ) {
var self = this;
return function ( value ) {
self.set( keypath, value );
};
};
(function ( proto ) {
var observe, Observer, updateObserver;
proto.observe = function ( keypath, callback, options ) {
var observers = [], k;
if ( typeof keypath === 'object' ) {
options = callback;
for ( k in keypath ) {
if ( hasOwn.call( keypath, k ) ) {
callback = keypath[k];
observers[ observers.length ] = observe( this, k, callback, options );
}
}
return {
cancel: function () {
while ( observers.length ) {
observers.pop().cancel();
}
}
};
}
return observe( this, keypath, callback, options );
};
observe = function ( root, keypath, callback, options ) {
var observer;
observer = new Observer( root, keypath, callback, options );
if ( !options || options.init !== false ) {
observer.update( true );
}
registerDependant( observer );
return {
cancel: function () {
unregisterDependant( observer );
}
};
};
Observer = function ( root, keypath, callback, options ) {
this.root = root;
this.keypath = keypath;
this.callback = callback;
this.priority = 0; // observers get top priority
// default to root as context, but allow it to be overridden
this.context = ( options && options.context ? options.context : root );
};
Observer.prototype = {
update: function ( init ) {
var value;
// TODO create, and use, an internal get method instead - we can skip checks
value = this.root.get( this.keypath, true );
if ( !isEqual( value, this.value ) || init ) {
// wrap the callback in a try-catch block, and only throw error in
// debug mode
try {
this.callback.call( this.context, value, this.value );
} catch ( err ) {
if ( this.root.debug ) {
throw err;
}
}
this.value = value;
}
}
};
}( proto ));
proto.off = function ( eventName, callback ) {
var subscribers, index;
// if no callback specified, remove all callbacks
if ( !callback ) {
// if no event name specified, remove all callbacks for all events
if ( !eventName ) {
this._subs = {};
} else {
this._subs[ eventName ] = [];
}
}
subscribers = this._subs[ eventName ];
if ( subscribers ) {
index = subscribers.indexOf( callback );
if ( index !== -1 ) {
subscribers.splice( index, 1 );
}
}
};
proto.on = function ( eventName, callback ) {
var self = this, listeners, n;
// allow mutliple listeners to be bound in one go
if ( typeof eventName === 'object' ) {
listeners = [];
for ( n in eventName ) {
if ( hasOwn.call( eventName, n ) ) {
listeners[ listeners.length ] = this.on( n, eventName[ n ] );
}
}
return {
cancel: function () {
while ( listeners.length ) {
listeners.pop().cancel();
}
}
};
}
if ( !this._subs[ eventName ] ) {
this._subs[ eventName ] = [ callback ];
} else {
this._subs[ eventName ].push( callback );
}
return {
cancel: function () {
self.off( eventName, callback );
}
};
};
proto.renderHTML = function () {
return this.fragment.toString();
};
proto.requestFullscreen = function () {
Ractive.requestFullscreen( this.el );
};
(function ( proto ) {
var set, attemptKeypathResolution;
proto.set = function ( keypath, value, complete ) {
var notificationQueue, upstreamQueue, k, normalised, keys, previous, previousTransitionManager, transitionManager;
upstreamQueue = [ '' ]; // empty string will always be an upstream keypath
notificationQueue = [];
if ( isObject( keypath ) ) {
complete = value;
}
// manage transitions
previousTransitionManager = this._transitionManager;
this._transitionManager = transitionManager = makeTransitionManager( this, complete );
// setting multiple values in one go
if ( isObject( keypath ) ) {
for ( k in keypath ) {
if ( hasOwn.call( keypath, k ) ) {
keys = splitKeypath( k );
normalised = keys.join( '.' );
value = keypath[k];
set( this, normalised, keys, value, notificationQueue, upstreamQueue );
}
}
}
// setting a single value
else {
keys = splitKeypath( keypath );
normalised = keys.join( '.' );
set( this, normalised, keys, value, notificationQueue, upstreamQueue );
}
// if anything has changed, attempt to resolve any unresolved keypaths...
if ( notificationQueue.length && this._pendingResolution.length ) {
attemptKeypathResolution( this );
}
// ...and notify dependants
if ( upstreamQueue.length ) {
notifyMultipleDependants( this, upstreamQueue, true );
}
if ( notificationQueue.length ) {
notifyMultipleDependants( this, notificationQueue );
}
// Attributes don't reflect changes automatically if there is a possibility
// that they will need to change again before the .set() cycle is complete
// - they defer their updates until all values have been set
processDeferredUpdates( this );
// transition manager has finished its work
this._transitionManager = previousTransitionManager;
transitionManager.ready();
// fire event
if ( !this.setting ) {
this.setting = true; // short-circuit any potential infinite loops
if ( typeof keypath === 'object' ) {
this.fire( 'set', keypath );
} else {
this.fire( 'set', keypath, value );
}
this.setting = false;
}
return this;
};
set = function ( root, keypath, keys, value, queue, upstreamQueue ) {
var previous, key, obj, keysClone, accumulated, keypathToClear;
keysClone = keys.slice();
accumulated = [];
previous = root.get( keypath );
// update the model, if necessary
if ( previous !== value ) {
// update data
obj = root.data;
while ( keys.length > 1 ) {
key = accumulated[ accumulated.length ] = keys.shift();
// If this branch doesn't exist yet, create a new one - if the next
// key matches /^\s*[0-9]+\s*$/, assume we want an array branch rather
// than an object
if ( !obj[ key ] ) {
// if we're creating a new branch, we may need to clear the upstream
// keypath
if ( !keypathToClear ) {
keypathToClear = accumulated.join( '.' );
}
obj[ key ] = ( /^\s*[0-9]+\s*$/.test( keys[0] ) ? [] : {} );
}
obj = obj[ key ];
}
key = keys[0];
obj[ key ] = value;
}
else {
// if value is a primitive, we don't need to do anything else
if ( typeof value !== 'object' ) {
return;
}
}
// Clear cache
clearCache( root, keypathToClear || keypath );
// add this keypath to the notification queue
queue[ queue.length ] = keypath;
// add upstream keypaths to the upstream notification queue
while ( keysClone.length > 1 ) {
keysClone.pop();
keypath = keysClone.join( '.' );
if ( upstreamQueue.indexOf( keypath ) === -1 ) {
upstreamQueue[ upstreamQueue.length ] = keypath;
}
}
};
attemptKeypathResolution = function ( root ) {
var i, unresolved, keypath;
// See if we can resolve any of the unresolved keypaths (if such there be)
i = root._pendingResolution.length;
while ( i-- ) { // Work backwards, so we don't go in circles!
unresolved = root._pendingResolution.splice( i, 1 )[0];
if ( keypath = resolveRef( root, unresolved.ref, unresolved.contextStack ) ) {
// If we've resolved the keypath, we can initialise this item
unresolved.resolve( keypath );
} else {
// If we can't resolve the reference, add to the back of
// the queue (this is why we're working backwards)
root._pendingResolution[ root._pendingResolution.length ] = unresolved;
}
}
};
}( proto ));
// Teardown. This goes through the root fragment and all its children, removing observers
// and generally cleaning up after itself
proto.teardown = function ( complete ) {
var keypath, transitionManager, previousTransitionManager;
this.fire( 'teardown' );
previousTransitionManager = this._transitionManager;
this._transitionManager = transitionManager = makeTransitionManager( this, complete );
this.fragment.teardown( true );
// Cancel any animations in progress
while ( this._animations[0] ) {
this._animations[0].stop(); // it will remove itself from the index
}
// Clear cache - this has the side-effect of unregistering keypaths from modified arrays.
for ( keypath in this._cache ) {
clearCache( this, keypath );
}
// Teardown any bindings
while ( this._bound.length ) {
this.unbind( this._bound.pop() );
}
// transition manager has finished its work
this._transitionManager = previousTransitionManager;
transitionManager.ready();
};
proto.toggleFullscreen = function () {
if ( Ractive.isFullscreen( this.el ) ) {
this.cancelFullscreen();
} else {
this.requestFullscreen();
}
};
proto.unbind = function ( adaptor ) {
var bound = this._bound, index;
index = bound.indexOf( adaptor );
if ( index !== -1 ) {
bound.splice( index, 1 );
adaptor.teardown( this );
}
};
proto.update = function ( keypath, complete ) {
var transitionManager, previousTransitionManager;
if ( typeof keypath === 'function' ) {
complete = keypath;
}
// manage transitions
previousTransitionManager = this._transitionManager;
this._transitionManager = transitionManager = makeTransitionManager( this, complete );
clearCache( this, keypath || '' );
notifyDependants( this, keypath || '' );
processDeferredUpdates( this );
// transition manager has finished its work
this._transitionManager = previousTransitionManager;
transitionManager.ready();
if ( typeof keypath === 'string' ) {
this.fire( 'update', keypath );
} else {
this.fire( 'update' );
}
return this;
};
adaptors.backbone = function ( model, path ) {
var settingModel, settingView, setModel, setView, pathMatcher, pathLength, prefix;
if ( path ) {
path += '.';
pathMatcher = new RegExp( '^' + path.replace( /\./g, '\\.' ) );
pathLength = path.length;
}
return {
init: function ( view ) {
// if no path specified...
if ( !path ) {
setView = function ( model ) {
if ( !settingModel ) {
settingView = true;
view.set( model.changed );
settingView = false;
}
};
setModel = function ( keypath, value ) {
if ( !settingView ) {
settingModel = true;
model.set( keypath, value );
settingModel = false;
}
};
}
else {
prefix = function ( attrs ) {
var attr, result;
result = {};
for ( attr in attrs ) {
if ( hasOwn.call( attrs, attr ) ) {
result[ path + attr ] = attrs[ attr ];
}
}
return result;
};
setView = function ( model ) {
if ( !settingModel ) {
settingView = true;
view.set( prefix( model.changed ) );
settingView = false;
}
};
setModel = function ( keypath, value ) {
if ( !settingView ) {
if ( pathMatcher.test( keypath ) ) {
settingModel = true;
model.set( keypath.substring( pathLength ), value );
settingModel = false;
}
}
};
}
model.on( 'change', setView );
view.on( 'set', setModel );
// initialise
view.set( path ? prefix( model.attributes ) : model.attributes );
},
teardown: function ( view ) {
model.off( 'change', setView );
view.off( 'set', setModel );
}
};
};
adaptors.statesman = function ( model, path ) {
var settingModel, settingView, setModel, setView, pathMatcher, pathLength, prefix;
if ( path ) {
path += '.';
pathMatcher = new RegExp( '^' + path.replace( /\./g, '\\.' ) );
pathLength = path.length;
prefix = function ( attrs ) {
var attr, result;
if ( !attrs ) {
return;
}
result = {};
for ( attr in attrs ) {
if ( hasOwn.call( attrs, attr ) ) {
result[ path + attr ] = attrs[ attr ];
}
}
return result;
};
}
return {
init: function ( view ) {
var data;
// if no path specified...
if ( !path ) {
setVie