UNPKG

ractive

Version:

Next-generation DOM manipulation

1,703 lines (1,616 loc) 385 kB
/* 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(); },