UNPKG

ractive

Version:

Next-generation DOM manipulation

2,139 lines (1,651 loc) 134 kB
/*! 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