ractive
Version:
Next-generation DOM manipulation
1,703 lines (1,616 loc) • 385 kB
JavaScript
/*
Ractive.js v0.4.0
2014-04-08 - commit 276c0e2b
http://ractivejs.org
http://twitter.com/RactiveJS
Released under the MIT License.
*/
( function( global ) {
'use strict';
var noConflict = global.Ractive;
var legacy = undefined;
var config_initOptions = function() {
var defaults, initOptions;
defaults = {
el: null,
template: '',
complete: null,
preserveWhitespace: false,
append: false,
twoway: true,
modifyArrays: true,
lazy: false,
debug: false,
noIntro: false,
transitionsEnabled: true,
magic: false,
noCssTransform: false,
adapt: [],
sanitize: false,
stripComments: true,
isolated: false,
delimiters: [
'{{',
'}}'
],
tripleDelimiters: [
'{{{',
'}}}'
],
computed: null
};
initOptions = {
keys: Object.keys( defaults ),
defaults: defaults
};
return initOptions;
}( legacy );
var config_svg = function() {
if ( typeof document === 'undefined' ) {
return;
}
return document && document.implementation.hasFeature( 'http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1' );
}();
var config_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/'
};
var utils_createElement = function( svg, namespaces ) {
// Test for SVG support
if ( !svg ) {
return function( type, ns ) {
if ( ns && ns !== namespaces.html ) {
throw 'This browser does not support namespaces other than http://www.w3.org/1999/xhtml. The most likely cause of this error is that you\'re trying to render SVG in an older browser. See http://docs.ractivejs.org/latest/svg-and-older-browsers for more information';
}
return document.createElement( type );
};
} else {
return function( type, ns ) {
if ( !ns || ns === namespaces.html ) {
return document.createElement( type );
}
return document.createElementNS( ns, type );
};
}
}( config_svg, config_namespaces );
var config_isClient = typeof document === 'object';
var utils_defineProperty = function( isClient ) {
try {
Object.defineProperty( {}, 'test', {
value: 0
} );
if ( isClient ) {
Object.defineProperty( document.createElement( 'div' ), 'test', {
value: 0
} );
}
return Object.defineProperty;
} 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?)
return function( obj, prop, desc ) {
obj[ prop ] = desc.value;
};
}
}( config_isClient );
var utils_defineProperties = function( createElement, defineProperty, isClient ) {
try {
try {
Object.defineProperties( {}, {
test: {
value: 0
}
} );
} catch ( err ) {
// TODO how do we account for this? noMagic = true;
throw err;
}
if ( isClient ) {
Object.defineProperties( createElement( 'div' ), {
test: {
value: 0
}
} );
}
return Object.defineProperties;
} catch ( err ) {
return function( obj, props ) {
var prop;
for ( prop in props ) {
if ( props.hasOwnProperty( prop ) ) {
defineProperty( obj, prop, props[ prop ] );
}
}
};
}
}( utils_createElement, utils_defineProperty, config_isClient );
var utils_isNumeric = function( thing ) {
return !isNaN( parseFloat( thing ) ) && isFinite( thing );
};
var Ractive_prototype_shared_add = function( isNumeric ) {
return function( root, keypath, d ) {
var value;
if ( typeof keypath !== 'string' || !isNumeric( d ) ) {
throw new Error( 'Bad arguments' );
}
value = +root.get( keypath ) || 0;
if ( !isNumeric( value ) ) {
throw new Error( 'Cannot add to a non-numeric value' );
}
return root.set( keypath, value + d );
};
}( utils_isNumeric );
var Ractive_prototype_add = function( add ) {
return function( keypath, d ) {
return add( this, keypath, d === undefined ? 1 : +d );
};
}( Ractive_prototype_shared_add );
var utils_isEqual = function( a, b ) {
if ( a === null && b === null ) {
return true;
}
if ( typeof a === 'object' || typeof b === 'object' ) {
return false;
}
return a === b;
};
var utils_Promise = function() {
var Promise, PENDING = {}, FULFILLED = {}, REJECTED = {};
Promise = function( callback ) {
var fulfilledHandlers = [],
rejectedHandlers = [],
state = PENDING,
result, dispatchHandlers, makeResolver, fulfil, reject, promise;
makeResolver = function( newState ) {
return function( value ) {
if ( state !== PENDING ) {
return;
}
result = value;
state = newState;
dispatchHandlers = makeDispatcher( state === FULFILLED ? fulfilledHandlers : rejectedHandlers, result );
// dispatch onFulfilled and onRejected handlers asynchronously
wait( dispatchHandlers );
};
};
fulfil = makeResolver( FULFILLED );
reject = makeResolver( REJECTED );
callback( fulfil, reject );
promise = {
// `then()` returns a Promise - 2.2.7
then: function( onFulfilled, onRejected ) {
var promise2 = new Promise( function( fulfil, reject ) {
var processResolutionHandler = function( handler, handlers, forward ) {
// 2.2.1.1
if ( typeof handler === 'function' ) {
handlers.push( function( p1result ) {
var x;
try {
x = handler( p1result );
resolve( promise2, x, fulfil, reject );
} catch ( err ) {
reject( err );
}
} );
} else {
// Forward the result of promise1 to promise2, if resolution handlers
// are not given
handlers.push( forward );
}
};
// 2.2
processResolutionHandler( onFulfilled, fulfilledHandlers, fulfil );
processResolutionHandler( onRejected, rejectedHandlers, reject );
if ( state !== PENDING ) {
// If the promise has resolved already, dispatch the appropriate handlers asynchronously
wait( dispatchHandlers );
}
} );
return promise2;
}
};
promise[ 'catch' ] = function( onRejected ) {
return this.then( null, onRejected );
};
return promise;
};
Promise.all = function( promises ) {
return new Promise( function( fulfil, reject ) {
var result = [],
pending, i, processPromise;
if ( !promises.length ) {
fulfil( result );
return;
}
processPromise = function( i ) {
promises[ i ].then( function( value ) {
result[ i ] = value;
if ( !--pending ) {
fulfil( result );
}
}, reject );
};
pending = i = promises.length;
while ( i-- ) {
processPromise( i );
}
} );
};
Promise.resolve = function( value ) {
return new Promise( function( fulfil ) {
fulfil( value );
} );
};
Promise.reject = function( reason ) {
return new Promise( function( fulfil, reject ) {
reject( reason );
} );
};
return Promise;
// TODO use MutationObservers or something to simulate setImmediate
function wait( callback ) {
setTimeout( callback, 0 );
}
function makeDispatcher( handlers, result ) {
return function() {
var handler;
while ( handler = handlers.shift() ) {
handler( result );
}
};
}
function resolve( promise, x, fulfil, reject ) {
// Promise Resolution Procedure
var then;
// 2.3.1
if ( x === promise ) {
throw new TypeError( 'A promise\'s fulfillment handler cannot return the same promise' );
}
// 2.3.2
if ( x instanceof Promise ) {
x.then( fulfil, reject );
} else if ( x && ( typeof x === 'object' || typeof x === 'function' ) ) {
try {
then = x.then;
} catch ( e ) {
reject( e );
// 2.3.3.2
return;
}
// 2.3.3.3
if ( typeof then === 'function' ) {
var called, resolvePromise, rejectPromise;
resolvePromise = function( y ) {
if ( called ) {
return;
}
called = true;
resolve( promise, y, fulfil, reject );
};
rejectPromise = function( r ) {
if ( called ) {
return;
}
called = true;
reject( r );
};
try {
then.call( x, resolvePromise, rejectPromise );
} catch ( e ) {
if ( !called ) {
// 2.3.3.3.4.1
reject( e );
// 2.3.3.3.4.2
called = true;
return;
}
}
} else {
fulfil( x );
}
} else {
fulfil( x );
}
}
}();
var utils_normaliseKeypath = function() {
var regex = /\[\s*(\*|[0-9]|[1-9][0-9]+)\s*\]/g;
return function normaliseKeypath( keypath ) {
return ( keypath || '' ).replace( regex, '.$1' );
};
}();
var config_vendors = [
'o',
'ms',
'moz',
'webkit'
];
var utils_requestAnimationFrame = function( vendors ) {
// If window doesn't exist, we don't need requestAnimationFrame
if ( typeof window === 'undefined' ) {
return;
}
// https://gist.github.com/paulirish/1579671
( function( vendors, lastTime, window ) {
var x, setTimeout;
if ( window.requestAnimationFrame ) {
return;
}
for ( x = 0; x < vendors.length && !window.requestAnimationFrame; ++x ) {
window.requestAnimationFrame = window[ vendors[ x ] + 'RequestAnimationFrame' ];
}
if ( !window.requestAnimationFrame ) {
setTimeout = window.setTimeout;
window.requestAnimationFrame = function( callback ) {
var currTime, timeToCall, id;
currTime = Date.now();
timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) );
id = setTimeout( function() {
callback( currTime + timeToCall );
}, timeToCall );
lastTime = currTime + timeToCall;
return id;
};
}
}( vendors, 0, window ) );
return window.requestAnimationFrame;
}( config_vendors );
var utils_getTime = function() {
if ( typeof window !== 'undefined' && window.performance && typeof window.performance.now === 'function' ) {
return function() {
return window.performance.now();
};
} else {
return function() {
return Date.now();
};
}
}();
// This module provides a place to store a) circular dependencies and
// b) the callback functions that require those circular dependencies
var circular = [];
var utils_removeFromArray = function( array, member ) {
var index = array.indexOf( member );
if ( index !== -1 ) {
array.splice( index, 1 );
}
};
var global_css = function( circular, isClient, removeFromArray ) {
var runloop, styleElement, head, styleSheet, inDom, prefix = '/* Ractive.js component styles */\n',
componentsInPage = {}, styles = [];
if ( !isClient ) {
return;
}
circular.push( function() {
runloop = circular.runloop;
} );
styleElement = document.createElement( 'style' );
styleElement.type = 'text/css';
head = document.getElementsByTagName( 'head' )[ 0 ];
inDom = false;
// Internet Exploder won't let you use styleSheet.innerHTML - we have to
// use styleSheet.cssText instead
styleSheet = styleElement.styleSheet;
return {
add: function( Component ) {
if ( !Component.css ) {
return;
}
if ( !componentsInPage[ Component._guid ] ) {
// we create this counter so that we can in/decrement it as
// instances are added and removed. When all components are
// removed, the style is too
componentsInPage[ Component._guid ] = 0;
styles.push( Component.css );
runloop.scheduleCssUpdate();
}
componentsInPage[ Component._guid ] += 1;
},
remove: function( Component ) {
if ( !Component.css ) {
return;
}
componentsInPage[ Component._guid ] -= 1;
if ( !componentsInPage[ Component._guid ] ) {
removeFromArray( styles, Component.css );
runloop.scheduleCssUpdate();
}
},
update: function() {
var css;
if ( styles.length ) {
css = prefix + styles.join( ' ' );
if ( styleSheet ) {
styleSheet.cssText = css;
} else {
styleElement.innerHTML = css;
}
if ( !inDom ) {
head.appendChild( styleElement );
}
} else if ( inDom ) {
head.removeChild( styleElement );
}
}
};
}( circular, config_isClient, utils_removeFromArray );
var shared_getValueFromCheckboxes = function( ractive, keypath ) {
var value, checkboxes, checkbox, len, i, rootEl;
value = [];
// TODO in edge cases involving components with inputs bound to the same keypath, this
// could get messy
// if we're still in the initial render, we need to find the inputs from the as-yet off-DOM
// document fragment. otherwise, the root element
rootEl = ractive._rendering ? ractive.fragment.docFrag : ractive.el;
checkboxes = rootEl.querySelectorAll( 'input[type="checkbox"][name="{{' + keypath + '}}"]' );
len = checkboxes.length;
for ( i = 0; i < len; i += 1 ) {
checkbox = checkboxes[ i ];
if ( checkbox.hasAttribute( 'checked' ) || checkbox.checked ) {
value.push( checkbox._ractive.value );
}
}
return value;
};
var utils_hasOwnProperty = Object.prototype.hasOwnProperty;
var shared_getInnerContext = function( fragment ) {
do {
if ( fragment.context ) {
return fragment.context;
}
} while ( fragment = fragment.parent );
return '';
};
var shared_resolveRef = function( circular, normaliseKeypath, hasOwnProperty, getInnerContext ) {
var get, ancestorErrorMessage = 'Could not resolve reference - too many "../" prefixes';
circular.push( function() {
get = circular.get;
} );
return function resolveRef( ractive, ref, fragment ) {
var context, contextKeys, keys, lastKey, postfix, parentKeypath, parentValue, wrapped, hasContextChain;
ref = normaliseKeypath( ref );
// Implicit iterators - i.e. {{.}} - are a special case
if ( ref === '.' ) {
return getInnerContext( fragment );
}
// If a reference begins with '.', it's either a restricted reference or
// an ancestor reference...
if ( ref.charAt( 0 ) === '.' ) {
// ...either way we need to get the innermost context
context = getInnerContext( fragment );
contextKeys = context ? context.split( '.' ) : [];
// ancestor references (starting "../") go up the tree
if ( ref.substr( 0, 3 ) === '../' ) {
while ( ref.substr( 0, 3 ) === '../' ) {
if ( !contextKeys.length ) {
throw new Error( ancestorErrorMessage );
}
contextKeys.pop();
ref = ref.substring( 3 );
}
contextKeys.push( ref );
return contextKeys.join( '.' );
}
// not an ancestor reference - must be a restricted reference (prepended with ".")
if ( !context ) {
return ref.substring( 1 );
}
return context + ref;
}
// Now we need to try and resolve the reference against any
// contexts set by parent list/object sections
keys = ref.split( '.' );
lastKey = keys.pop();
postfix = keys.length ? '.' + keys.join( '.' ) : '';
do {
context = fragment.context;
if ( !context ) {
continue;
}
hasContextChain = true;
parentKeypath = context + postfix;
parentValue = get( ractive, parentKeypath );
if ( wrapped = ractive._wrapped[ parentKeypath ] ) {
parentValue = wrapped.get();
}
if ( parentValue && ( typeof parentValue === 'object' || typeof parentValue === 'function' ) && lastKey in parentValue ) {
return context + '.' + ref;
}
} while ( fragment = fragment.parent );
// Still no keypath?
// If there's no context chain, and the instance is either a) isolated or
// b) an orphan, then we know that the keypath is identical to the reference
if ( !hasContextChain && ( !ractive._parent || ractive.isolated ) ) {
return ref;
}
// We need both of these - the first enables components to treat data contexts
// like lexical scopes in JavaScript functions...
if ( hasOwnProperty.call( ractive.data, ref ) ) {
return ref;
} else if ( get( ractive, ref ) !== undefined ) {
return ref;
}
};
}( circular, utils_normaliseKeypath, utils_hasOwnProperty, shared_getInnerContext );
var shared_getUpstreamChanges = function getUpstreamChanges( changes ) {
var upstreamChanges = [ '' ],
i, keypath, keys, upstreamKeypath;
i = changes.length;
while ( i-- ) {
keypath = changes[ i ];
keys = keypath.split( '.' );
while ( keys.length > 1 ) {
keys.pop();
upstreamKeypath = keys.join( '.' );
if ( upstreamChanges[ upstreamKeypath ] !== true ) {
upstreamChanges.push( upstreamKeypath );
upstreamChanges[ upstreamKeypath ] = true;
}
}
}
return upstreamChanges;
};
var shared_notifyDependants = function() {
var lastKey, starMaps = {};
lastKey = /[^\.]+$/;
function notifyDependants( ractive, keypath, onlyDirect ) {
var i;
// Notify any pattern observers
if ( ractive._patternObservers.length ) {
notifyPatternObservers( ractive, keypath, keypath, onlyDirect, true );
}
for ( i = 0; i < ractive._deps.length; i += 1 ) {
// can't cache ractive._deps.length, it may change
notifyDependantsAtPriority( ractive, keypath, i, onlyDirect );
}
}
notifyDependants.multiple = function notifyMultipleDependants( ractive, keypaths, onlyDirect ) {
var i, j, len;
len = keypaths.length;
// Notify any pattern observers
if ( ractive._patternObservers.length ) {
i = len;
while ( i-- ) {
notifyPatternObservers( ractive, keypaths[ i ], keypaths[ i ], onlyDirect, true );
}
}
for ( i = 0; i < ractive._deps.length; i += 1 ) {
if ( ractive._deps[ i ] ) {
j = len;
while ( j-- ) {
notifyDependantsAtPriority( ractive, keypaths[ j ], i, onlyDirect );
}
}
}
};
return notifyDependants;
function notifyDependantsAtPriority( ractive, keypath, priority, onlyDirect ) {
var depsByKeypath = ractive._deps[ priority ];
if ( !depsByKeypath ) {
return;
}
// update dependants of this keypath
updateAll( depsByKeypath[ keypath ] );
// If we're only notifying direct dependants, not dependants
// of downstream keypaths, then YOU SHALL NOT PASS
if ( onlyDirect ) {
return;
}
// otherwise, cascade
cascade( ractive._depsMap[ keypath ], ractive, priority );
}
function updateAll( deps ) {
var i, len;
if ( deps ) {
len = deps.length;
for ( i = 0; i < len; i += 1 ) {
deps[ i ].update();
}
}
}
function cascade( childDeps, ractive, priority, onlyDirect ) {
var i;
if ( childDeps ) {
i = childDeps.length;
while ( i-- ) {
notifyDependantsAtPriority( ractive, childDeps[ i ], priority, onlyDirect );
}
}
}
// TODO split into two functions? i.e. one for the top-level call, one for the cascade
function notifyPatternObservers( ractive, registeredKeypath, actualKeypath, isParentOfChangedKeypath, isTopLevelCall ) {
var i, patternObserver, children, child, key, childActualKeypath, potentialWildcardMatches, cascade;
// First, observers that match patterns at the same level
// or higher in the tree
i = ractive._patternObservers.length;
while ( i-- ) {
patternObserver = ractive._patternObservers[ i ];
if ( patternObserver.regex.test( actualKeypath ) ) {
patternObserver.update( actualKeypath );
}
}
if ( isParentOfChangedKeypath ) {
return;
}
// If the changed keypath is 'foo.bar', we need to see if there are
// any pattern observer dependants of keypaths below any of
// 'foo.bar', 'foo.*', '*.bar' or '*.*' (e.g. 'foo.bar.*' or 'foo.*.baz' )
cascade = function( keypath ) {
if ( children = ractive._depsMap[ keypath ] ) {
i = children.length;
while ( i-- ) {
child = children[ i ];
// foo.*.baz
key = lastKey.exec( child )[ 0 ];
// 'baz'
childActualKeypath = actualKeypath ? actualKeypath + '.' + key : key;
// 'foo.bar.baz'
notifyPatternObservers( ractive, child, childActualKeypath );
}
}
};
if ( isTopLevelCall ) {
potentialWildcardMatches = getPotentialWildcardMatches( actualKeypath );
potentialWildcardMatches.forEach( cascade );
} else {
cascade( registeredKeypath );
}
}
// This function takes a keypath such as 'foo.bar.baz', and returns
// all the variants of that keypath that include a wildcard in place
// of a key, such as 'foo.bar.*', 'foo.*.baz', 'foo.*.*' and so on.
// These are then checked against the dependants map (ractive._depsMap)
// to see if any pattern observers are downstream of one or more of
// these wildcard keypaths (e.g. 'foo.bar.*.status')
function getPotentialWildcardMatches( keypath ) {
var keys, starMap, mapper, i, result, wildcardKeypath;
keys = keypath.split( '.' );
starMap = getStarMap( keys.length );
result = [];
mapper = function( star, i ) {
return star ? '*' : keys[ i ];
};
i = starMap.length;
while ( i-- ) {
wildcardKeypath = starMap[ i ].map( mapper ).join( '.' );
if ( !result[ wildcardKeypath ] ) {
result.push( wildcardKeypath );
result[ wildcardKeypath ] = true;
}
}
return result;
}
// This function returns all the possible true/false combinations for
// a given number - e.g. for two, the possible combinations are
// [ true, true ], [ true, false ], [ false, true ], [ false, false ].
// It does so by getting all the binary values between 0 and e.g. 11
function getStarMap( num ) {
var ones = '',
max, binary, starMap, mapper, i;
if ( !starMaps[ num ] ) {
starMap = [];
while ( ones.length < num ) {
ones += 1;
}
max = parseInt( ones, 2 );
mapper = function( digit ) {
return digit === '1';
};
for ( i = 0; i <= max; i += 1 ) {
binary = i.toString( 2 );
while ( binary.length < num ) {
binary = '0' + binary;
}
starMap[ i ] = Array.prototype.map.call( binary, mapper );
}
starMaps[ num ] = starMap;
}
return starMaps[ num ];
}
}();
var shared_makeTransitionManager = function( removeFromArray ) {
var makeTransitionManager, checkComplete, remove, init;
makeTransitionManager = function( callback, previous ) {
var transitionManager = [];
transitionManager.detachQueue = [];
transitionManager.remove = remove;
transitionManager.init = init;
transitionManager._check = checkComplete;
transitionManager._callback = callback;
transitionManager._previous = previous;
if ( previous ) {
previous.push( transitionManager );
}
return transitionManager;
};
checkComplete = function() {
var element;
if ( this._ready && !this.length ) {
while ( element = this.detachQueue.pop() ) {
element.detach();
}
if ( typeof this._callback === 'function' ) {
this._callback();
}
if ( this._previous ) {
this._previous.remove( this );
}
}
};
remove = function( transition ) {
removeFromArray( this, transition );
this._check();
};
init = function() {
this._ready = true;
this._check();
};
return makeTransitionManager;
}( utils_removeFromArray );
var global_runloop = function( circular, css, removeFromArray, getValueFromCheckboxes, resolveRef, getUpstreamChanges, notifyDependants, makeTransitionManager ) {
circular.push( function() {
get = circular.get;
set = circular.set;
} );
var runloop, get, set, dirty = false,
flushing = false,
pendingCssChanges, inFlight = 0,
toFocus = null,
liveQueries = [],
decorators = [],
transitions = [],
observers = [],
attributes = [],
activeBindings = [],
evaluators = [],
computations = [],
selectValues = [],
checkboxKeypaths = {}, checkboxes = [],
radios = [],
unresolved = [],
instances = [],
transitionManager;
runloop = {
start: function( instance, callback ) {
this.addInstance( instance );
if ( !flushing ) {
inFlight += 1;
// create a new transition manager
transitionManager = makeTransitionManager( callback, transitionManager );
}
},
end: function() {
if ( flushing ) {
attemptKeypathResolution();
return;
}
if ( !--inFlight ) {
flushing = true;
flushChanges();
flushing = false;
land();
}
transitionManager.init();
transitionManager = transitionManager._previous;
},
trigger: function() {
if ( inFlight || flushing ) {
attemptKeypathResolution();
return;
}
flushing = true;
flushChanges();
flushing = false;
land();
},
focus: function( node ) {
toFocus = node;
},
addInstance: function( instance ) {
if ( instance && !instances[ instance._guid ] ) {
instances.push( instance );
instances[ instances._guid ] = true;
}
},
addLiveQuery: function( query ) {
liveQueries.push( query );
},
addDecorator: function( decorator ) {
decorators.push( decorator );
},
addTransition: function( transition ) {
transition._manager = transitionManager;
transitionManager.push( transition );
transitions.push( transition );
},
addObserver: function( observer ) {
observers.push( observer );
},
addAttribute: function( attribute ) {
attributes.push( attribute );
},
addBinding: function( binding ) {
binding.active = true;
activeBindings.push( binding );
},
scheduleCssUpdate: function() {
// if runloop isn't currently active, we need to trigger change immediately
if ( !inFlight && !flushing ) {
// TODO does this ever happen?
css.update();
} else {
pendingCssChanges = true;
}
},
// changes that may cause additional changes...
addEvaluator: function( evaluator ) {
dirty = true;
evaluators.push( evaluator );
},
addComputation: function( thing ) {
dirty = true;
computations.push( thing );
},
addSelectValue: function( selectValue ) {
dirty = true;
selectValues.push( selectValue );
},
addCheckbox: function( checkbox ) {
if ( !checkboxKeypaths[ checkbox.keypath ] ) {
dirty = true;
checkboxes.push( checkbox );
}
},
addRadio: function( radio ) {
dirty = true;
radios.push( radio );
},
addUnresolved: function( thing ) {
dirty = true;
unresolved.push( thing );
},
removeUnresolved: function( thing ) {
removeFromArray( unresolved, thing );
},
// synchronise node detachments with transition ends
detachWhenReady: function( thing ) {
transitionManager.detachQueue.push( thing );
}
};
circular.runloop = runloop;
return runloop;
function land() {
var thing, changedKeypath, changeHash;
if ( toFocus ) {
toFocus.focus();
toFocus = null;
}
while ( thing = attributes.pop() ) {
thing.update().deferred = false;
}
while ( thing = liveQueries.pop() ) {
thing._sort();
}
while ( thing = decorators.pop() ) {
thing.init();
}
while ( thing = transitions.pop() ) {
thing.init();
}
while ( thing = observers.pop() ) {
thing.update();
}
while ( thing = activeBindings.pop() ) {
thing.active = false;
}
// Change events are fired last
while ( thing = instances.pop() ) {
instances[ thing._guid ] = false;
if ( thing._changes.length ) {
changeHash = {};
while ( changedKeypath = thing._changes.pop() ) {
changeHash[ changedKeypath ] = get( thing, changedKeypath );
}
thing.fire( 'change', changeHash );
}
}
if ( pendingCssChanges ) {
css.update();
pendingCssChanges = false;
}
}
function flushChanges() {
var thing, upstreamChanges, i;
i = instances.length;
while ( i-- ) {
thing = instances[ i ];
if ( thing._changes.length ) {
upstreamChanges = getUpstreamChanges( thing._changes );
notifyDependants.multiple( thing, upstreamChanges, true );
}
}
attemptKeypathResolution();
while ( dirty ) {
dirty = false;
while ( thing = computations.pop() ) {
thing.update();
}
while ( thing = evaluators.pop() ) {
thing.update().deferred = false;
}
while ( thing = selectValues.pop() ) {
thing.deferredUpdate();
}
while ( thing = checkboxes.pop() ) {
set( thing.root, thing.keypath, getValueFromCheckboxes( thing.root, thing.keypath ) );
}
while ( thing = radios.pop() ) {
thing.update();
}
}
}
function attemptKeypathResolution() {
var array, thing, keypath;
if ( !unresolved.length ) {
return;
}
// see if we can resolve any unresolved references
array = unresolved.splice( 0, unresolved.length );
while ( thing = array.pop() ) {
if ( thing.keypath ) {
continue;
}
keypath = resolveRef( thing.root, thing.ref, thing.parentFragment );
if ( keypath !== undefined ) {
// If we've resolved the keypath, we can initialise this item
thing.resolve( keypath );
} else {
// If we can't resolve the reference, try again next time
unresolved.push( thing );
}
}
}
}( circular, global_css, utils_removeFromArray, shared_getValueFromCheckboxes, shared_resolveRef, shared_getUpstreamChanges, shared_notifyDependants, shared_makeTransitionManager );
var shared_animations = function( rAF, getTime, runloop ) {
var queue = [];
var animations = {
tick: function() {
var i, animation, now;
now = getTime();
runloop.start();
for ( i = 0; i < queue.length; i += 1 ) {
animation = queue[ i ];
if ( !animation.tick( now ) ) {
// animation is complete, remove it from the stack, and decrement i so we don't miss one
queue.splice( i--, 1 );
}
}
runloop.end();
if ( queue.length ) {
rAF( animations.tick );
} else {
animations.running = false;
}
},
add: function( animation ) {
queue.push( animation );
if ( !animations.running ) {
animations.running = true;
rAF( animations.tick );
}
},
// TODO optimise this
abort: function( keypath, root ) {
var i = queue.length,
animation;
while ( i-- ) {
animation = queue[ i ];
if ( animation.root === root && animation.keypath === keypath ) {
animation.stop();
}
}
}
};
return animations;
}( utils_requestAnimationFrame, utils_getTime, global_runloop );
var utils_isArray = function() {
var toString = Object.prototype.toString;
// thanks, http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
return function( thing ) {
return toString.call( thing ) === '[object Array]';
};
}();
var utils_clone = function( isArray ) {
return function( source ) {
var target, key;
if ( !source || typeof source !== 'object' ) {
return source;
}
if ( isArray( source ) ) {
return source.slice();
}
target = {};
for ( key in source ) {
if ( source.hasOwnProperty( key ) ) {
target[ key ] = source[ key ];
}
}
return target;
};
}( utils_isArray );
var registries_adaptors = {};
var shared_get_arrayAdaptor_getSpliceEquivalent = function( array, methodName, args ) {
switch ( methodName ) {
case 'splice':
return args;
case 'sort':
case 'reverse':
return null;
case 'pop':
if ( array.length ) {
return [ -1 ];
}
return null;
case 'push':
return [
array.length,
0
].concat( args );
case 'shift':
return [
0,
1
];
case 'unshift':
return [
0,
0
].concat( args );
}
};
var shared_get_arrayAdaptor_summariseSpliceOperation = function( array, args ) {
var start, addedItems, removedItems, balance;
if ( !args ) {
return null;
}
// figure out where the changes started...
start = +( args[ 0 ] < 0 ? array.length + args[ 0 ] : args[ 0 ] );
// ...and how many items were added to or removed from the array
addedItems = Math.max( 0, args.length - 2 );
removedItems = args[ 1 ] !== undefined ? args[ 1 ] : array.length - start;
// It's possible to do e.g. [ 1, 2, 3 ].splice( 2, 2 ) - i.e. the second argument
// means removing more items from the end of the array than there are. In these
// cases we need to curb JavaScript's enthusiasm or we'll get out of sync
removedItems = Math.min( removedItems, array.length - start );
balance = addedItems - removedItems;
return {
start: start,
balance: balance,
added: addedItems,
removed: removedItems
};
};
var config_types = {
TEXT: 1,
INTERPOLATOR: 2,
TRIPLE: 3,
SECTION: 4,
INVERTED: 5,
CLOSING: 6,
ELEMENT: 7,
PARTIAL: 8,
COMMENT: 9,
DELIMCHANGE: 10,
MUSTACHE: 11,
TAG: 12,
ATTRIBUTE: 13,
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
};
var shared_clearCache = function clearCache( ractive, keypath, dontTeardownWrapper ) {
var cacheMap, wrappedProperty;
if ( !dontTeardownWrapper ) {
// Is there a wrapped property at this keypath?
if ( wrappedProperty = ractive._wrapped[ keypath ] ) {
// Did we unwrap it?
if ( wrappedProperty.teardown() !== false ) {
ractive._wrapped[ keypath ] = null;
}
}
}
ractive._cache[ keypath ] = undefined;
if ( cacheMap = ractive._cacheMap[ keypath ] ) {
while ( cacheMap.length ) {
clearCache( ractive, cacheMap.pop() );
}
}
};
var utils_createBranch = function() {
var numeric = /^\s*[0-9]+\s*$/;
return function( key ) {
return numeric.test( key ) ? [] : {};
};
}();
var shared_set = function( circular, isEqual, createBranch, clearCache, notifyDependants ) {
var get;
circular.push( function() {
get = circular.get;
} );
function set( ractive, keypath, value, silent ) {
var keys, lastKey, parentKeypath, parentValue, computation, wrapper, evaluator, dontTeardownWrapper;
if ( isEqual( ractive._cache[ keypath ], value ) ) {
return;
}
computation = ractive._computations[ keypath ];
wrapper = ractive._wrapped[ keypath ];
evaluator = ractive._evaluators[ keypath ];
if ( computation && !computation.setting ) {
computation.set( value );
}
// If we have a wrapper with a `reset()` method, we try and use it. If the
// `reset()` method returns false, the wrapper should be torn down, and
// (most likely) a new one should be created later
if ( wrapper && wrapper.reset ) {
dontTeardownWrapper = wrapper.reset( value ) !== false;
if ( dontTeardownWrapper ) {
value = wrapper.get();
}
}
// Update evaluator value. This may be from the evaluator itself, or
// it may be from the wrapper that wraps an evaluator's result - it
// doesn't matter
if ( evaluator ) {
evaluator.value = value;
}
if ( !computation && !evaluator && !dontTeardownWrapper ) {
keys = keypath.split( '.' );
lastKey = keys.pop();
parentKeypath = keys.join( '.' );
wrapper = ractive._wrapped[ parentKeypath ];
if ( wrapper && wrapper.set ) {
wrapper.set( lastKey, value );
} else {
parentValue = wrapper ? wrapper.get() : get( ractive, parentKeypath );
if ( !parentValue ) {
parentValue = createBranch( lastKey );
set( ractive, parentKeypath, parentValue, true );
}
parentValue[ lastKey ] = value;
}
}
clearCache( ractive, keypath, dontTeardownWrapper );
if ( !silent ) {
ractive._changes.push( keypath );
notifyDependants( ractive, keypath );
}
}
circular.set = set;
return set;
}( circular, utils_isEqual, utils_createBranch, shared_clearCache, shared_notifyDependants );
var shared_get_arrayAdaptor_processWrapper = function( types, clearCache, notifyDependants, set ) {
return function( wrapper, array, methodName, spliceSummary ) {
var root, keypath, clearEnd, updateDependant, i, changed, start, end, childKeypath, lengthUnchanged;
root = wrapper.root;
keypath = wrapper.keypath;
root._changes.push( keypath );
// If this is a sort or reverse, we just do root.set()...
// TODO use merge logic?
if ( methodName === 'sort' || methodName === 'reverse' ) {
set( root, keypath, array );
return;
}
if ( !spliceSummary ) {
// (presumably we tried to pop from an array of zero length.
// in which case there's nothing to do)
return;
}
// ...otherwise we do a smart update whereby elements are added/removed
// in the right place. But we do need to clear the cache downstream
clearEnd = !spliceSummary.balance ? spliceSummary.added : array.length - Math.min( spliceSummary.balance, 0 );
for ( i = spliceSummary.start; i < clearEnd; i += 1 ) {
clearCache( root, keypath + '.' + i );
}
// Propagate changes
updateDependant = function( dependant ) {
// is this a DOM section?
if ( dependant.keypath === keypath && dependant.type === types.SECTION && !dependant.inverted && dependant.docFrag ) {
dependant.splice( spliceSummary );
} else {
dependant.update();
}
};
// Go through all dependant priority levels, finding smart update targets
root._deps.forEach( function( depsByKeypath ) {
var dependants = depsByKeypath[ keypath ];
if ( dependants ) {
dependants.forEach( updateDependant );
}
} );
// if we're removing old items and adding new ones, simultaneously, we need to force an update
if ( spliceSummary.added && spliceSummary.removed ) {
changed = Math.max( spliceSummary.added, spliceSummary.removed );
start = spliceSummary.start;
end = start + changed;
lengthUnchanged = spliceSummary.added === spliceSummary.removed;
for ( i = start; i < end; i += 1 ) {
childKeypath = keypath + '.' + i;
notifyDependants( root, childKeypath );
}
}
// length property has changed - notify dependants
// TODO in some cases (e.g. todo list example, when marking all as complete, then
// adding a new item (which should deactivate the 'all complete' checkbox
// but doesn't) this needs to happen before other updates. But doing so causes
// other mental problems. not sure what's going on...
if ( !lengthUnchanged ) {
clearCache( root, keypath + '.length' );
notifyDependants( root, keypath + '.length', true );
}
};
}( config_types, shared_clearCache, shared_notifyDependants, shared_set );
var shared_get_arrayAdaptor_patch = function( runloop, defineProperty, getSpliceEquivalent, summariseSpliceOperation, processWrapper ) {
var patchedArrayProto = [],
mutatorMethods = [
'pop',
'push',
'reverse',
'shift',
'sort',
'splice',
'unshift'
],
testObj, patchArrayMethods, unpatchArrayMethods;
mutatorMethods.forEach( function( methodName ) {
var method = function() {
var spliceEquivalent, spliceSummary, result, wrapper, i;
// push, pop, shift and unshift can all be represented as a splice operation.
// this makes life easier later
spliceEquivalent = getSpliceEquivalent( this, methodName, Array.prototype.slice.call( arguments ) );
spliceSummary = summariseSpliceOperation( this, spliceEquivalent );
// apply the underlying method
result = Array.prototype[ methodName ].apply( this, arguments );
// trigger changes
this._ractive.setting = true;
i = this._ractive.wrappers.length;
while ( i-- ) {
wrapper = this._ractive.wrappers[ i ];
runloop.start( wrapper.root );
processWrapper( wrapper, this, methodName, spliceSummary );
runloop.end();
}
this._ractive.setting = false;
return result;
};
defineProperty( patchedArrayProto, methodName, {
value: method
} );
} );
// can we use prototype chain injection?
// http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/#wrappers_prototype_chain_injection
testObj = {};
if ( testObj.__proto__ ) {
// yes, we can
patchArrayMethods = function( array ) {
array.__proto__ = patchedArrayProto;
};
unpatchArrayMethods = function( array ) {
array.__proto__ = Array.prototype;
};
} else {
// no, we can't
patchArrayMethods = function( array ) {
var i, methodName;
i = mutatorMethods.length;
while ( i-- ) {
methodName = mutatorMethods[ i ];
defineProperty( array, methodName, {
value: patchedArrayProto[ methodName ],
configurable: true
} );
}
};
unpatchArrayMethods = function( array ) {
var i;
i = mutatorMethods.length;
while ( i-- ) {
delete array[ mutatorMethods[ i ] ];
}
};
}
patchArrayMethods.unpatch = unpatchArrayMethods;
return patchArrayMethods;
}( global_runloop, utils_defineProperty, shared_get_arrayAdaptor_getSpliceEquivalent, shared_get_arrayAdaptor_summariseSpliceOperation, shared_get_arrayAdaptor_processWrapper );
var shared_get_arrayAdaptor__arrayAdaptor = function( defineProperty, isArray, patch ) {
var arrayAdaptor,
// helpers
ArrayWrapper, errorMessage;
arrayAdaptor = {
filter: function( object ) {
// wrap the array if a) b) it's an array, and b) either it hasn't been wrapped already,
// or the array didn't trigger the get() itself
return isArray( object ) && ( !object._ractive || !object._ractive.setting );
},
wrap: function( ractive, array, keypath ) {
return new ArrayWrapper( ractive, array, keypath );
}
};
ArrayWrapper = function( ractive, array, keypath ) {
this.root = ractive;
this.value = array;
this.keypath = keypath;
// if this array hasn't already been ractified, ractify it
if ( !array._ractive ) {
// define a non-enumerable _ractive property to store the wrappers
defineProperty( array, '_ractive', {
value: {
wrappers: [],
instances: [],
setting: false
},
configurable: true
} );
patch( array );
}
// store the ractive instance, so we can handle transitions later
if ( !array._ractive.instances[ ractive._guid ] ) {
array._ractive.instances[ ractive._guid ] = 0;
array._ractive.instances.push( ractive );
}
array._ractive.instances[ ractive._guid ] += 1;
array._ractive.wrappers.push( this );
};
ArrayWrapper.prototype = {
get: function() {
return this.value;
},
teardown: function() {
var array, storage, wrappers, instances, index;
array = this.value;
storage = array._ractive;
wrappers = storage.wrappers;
instances = storage.instances;
// if teardown() was invoked because we're clearing the cache as a result of
// a change that the array itself triggered, we can save ourselves the teardown
// and immediate setup
if ( storage.setting ) {
return false;
}
index = wrappers.indexOf( this );
if ( index === -1 ) {
throw new Error( errorMessage );
}
wrappers.splice( index, 1 );
// if nothing else depends on this array, we can revert it to its
// natural state
if ( !wrappers.length ) {
delete array._ractive;
patch.unpatch( this.value );
} else {
// remove ractive instance if possible
instances[ this.root._guid ] -= 1;
if ( !instances[ this.root._guid ] ) {
index = instances.indexOf( this.root );
if ( index === -1 ) {
throw new Error( errorMessage );
}
instances.splice( index, 1 );
}
}
}
};
errorMessage = 'Something went wrong in a rather interesting way';
return arrayAdaptor;
}( utils_defineProperty, utils_isArray, shared_get_arrayAdaptor_patch );
var shared_get_magicAdaptor = function( runloop, createBranch, isArray, clearCache, notifyDependants ) {
var magicAdaptor, MagicWrapper;
try {
Object.defineProperty( {}, 'test', {
value: 0
} );
} catch ( err ) {
return false;
}
magicAdaptor = {
filter: function( object, keypath, ractive ) {
var keys, key, parentKeypath, parentWrapper, parentValue;
if ( !keypath ) {
return false;
}
keys = keypath.split( '.' );
key = keys.pop();
parentKeypath = keys.join( '.' );
// If the parent value is a wrapper, other than a magic wrapper,
// we shouldn't wrap this property
if ( ( parentWrapper = ractive._wrapped[ parentKeypath ] ) && !parentWrapper.magic ) {
return false;
}
parentValue = ractive.get( parentKeypath );
// if parentValue is an array that doesn't include this member,
// we should return false otherwise lengths will get messed up
if ( isArray( parentValue ) && /^[0-9]+$/.test( key ) ) {
return false;
}
return parentValue && ( typeof parentValue === 'object' || typeof parentValue === 'function' );
},
wrap: function( ractive, property, keypath ) {
return new MagicWrapper( ractive, property, keypath );
}
};
MagicWrapper = function( ractive, value, keypath ) {
var keys, objKeypath, descriptor, siblings;
this.magic = true;
this.ractive = ractive;
this.keypath = keypath;
this.value = value;
keys = keypath.split( '.' );
this.prop = keys.pop();
objKeypath = keys.join( '.' );
this.obj = objKeypath ? ractive.get( objKeypath ) : ractive.data;
descriptor = this.originalDescriptor = Object.getOwnPropertyDescriptor( this.obj, this.prop );
// Has this property already been wrapped?
if ( descriptor && descriptor.set && ( siblings = descriptor.set._ractiveWrappers ) ) {
// Yes. Register this wrapper to this property, if it hasn't been already
if ( siblings.indexOf( this ) === -1 ) {
siblings.push( this );
}
return;
}
// No, it hasn't been wrapped
createAccessors( this, value, descriptor );
};
MagicWrapper.prototype = {
get: function() {
return this.value;
},
reset: function( value ) {
if ( this.updating ) {
return;
}
this.updating = true;
this.obj[ this.prop ] = value;
// trigger set() accessor
clearCache( this.ractive, this.keypath );
this.updating = false;
},
set: function( key, value ) {
if ( this.updating ) {
return;
}
if ( !this.obj[ this.prop ] ) {
this.updating = true;
this.obj[ this.prop ] = createBranch( key );
this.updating = false;
}
this.obj[ this.prop ][ key ] = value;
},
teardown: function() {
var descriptor, set, value, wrappers, index;
// If this method was called because the cache was being cleared as a
// result of a set()/update() call made by this wrapper, we return false
// so that it doesn't get torn down
if ( this.updating ) {
return false;
}
descriptor = Object.getOwnPropertyDescriptor( this.obj, this.prop );
set = descriptor && descriptor.set;
if ( !set ) {
// most likely, this was an array member that was spliced out
return;
}
wrappers = set._ractiveWrappers;
index = wrappers.indexOf( this );
if ( index !== -1 ) {
wrappers.splice( index, 1 );
}
// Last one out, turn off the lights
if ( !wrappers.length ) {
value = this.obj[ this.prop ];
Object.defineProperty( this.obj, this.prop, this.originalDescriptor || {
writable: true,
enumerable: true,
configurable: true
} );
this.obj[ this.prop ] = value;
}
}
};
function createAccessors( originalWrapper, value, descriptor ) {
var object, property, oldGet, oldSet, get, set;
object = originalWrapper.obj;
property = originalWrapper.prop;
// Is this descriptor configurable?
if ( descriptor && !descriptor.configurable ) {
// Special case - array length
if ( property === 'length' ) {
return;
}
throw new Error( 'Cannot use magic mode with property "' + property + '" - object is not configurable' );
}
// Time to wrap this property
if ( descriptor ) {
oldGet = descriptor.get;
oldSet = descriptor.set;
}
get = oldGet || function() {
return value;
};
set = function( v ) {
if ( oldSet ) {
oldSet( v );
}
value = oldGet ? oldGet() : v;
set._ractiveWrappers.forEach( updateWrapper );
};
function updateWrapper( wrapper ) {
var keypath, ractive;
wrapper.value = value;
if ( wrapper.updating ) {
return;
}
ractive = wrapper.ractive;
keypath = wrapper.keypath;
wrapper.updating = true;
runloop.start( ractive );
ractive._changes.push( keypath );
clearCache( ractive, keypath );
notifyDependants( ractive, keypath );
runloop.end();
wrapper.updating = false;
}
// Create an array of wrappers, in case other keypaths/ractives depend on this property.
// Handily, we can store them as a property of the set function. Yay JavaScript.
set._ractiveWrappers = [ originalWrapper ];
Object.defineProperty( object, property, {
get: get,
set: set,
enumerable: true,
configurable: true
} );
}
return magicAdaptor;
}( global_runloop, utils_createBranch, utils_isArray, shared_clearCache, shared_notifyDependants );
var shared_get_magicArrayAdaptor = function( magicAdaptor, arrayAdaptor ) {
if ( !magicAdaptor ) {
return false;
}
var magicArrayAdaptor, MagicArrayWrapper;
magicArrayAdaptor = {
filter: function( object, keypath, ractive ) {
return magicAdaptor.filter( object, keypath, ractive ) && arrayAdaptor.filter( object );
},
wrap: function( ractive, array, keypath ) {
return new MagicArrayWrapper( ractive, array, keypath );
}
};
MagicArrayWrapper = function( ractive, array, keypath ) {
this.value = array;
this.magic = true;
this.magicWrapper = magicAdaptor.wrap( ractive, array, keypath );
this.arrayWrapper = arrayAdaptor.wrap( ractive, array, keypath );
};
MagicArrayWrapper.prototype = {
get: function() {
return this.value;
},
teardown: function() {
this.arrayWrapper.teardown();
this.magicWrapper.teardown();
},