UNPKG

ractive

Version:

Next-generation DOM manipulation

2,128 lines (1,642 loc) 139 kB
(function ( doc ) { // Shims for older browsers if ( !Date.now ) { Date.now = function () { return +new Date(); }; } if ( !doc.createElementNS ) { doc.createElementNS = function ( ns, type ) { if ( ns !== null && ns !== 'http://www.w3.org/1999/xhtml' ) { throw 'This browser does not support namespaces other than http://www.w3.org/1999/xhtml'; } return doc.createElement( type ); }; } if ( !Element.prototype.contains ) { Element.prototype.contains = function ( el ) { while ( el.parentNode ) { if ( el.parentNode === this ) { return true; } el = el.parentNode; } return false; }; } if ( !String.prototype.trim ) { String.prototype.trim = function () { return this.replace(/^\s+/, '').replace(/\s+$/, ''); }; } // https://gist.github.com/jonathantneal/3748027 if ( !window.addEventListener ) { (function ( WindowPrototype, DocumentPrototype, ElementPrototype, addEventListener, removeEventListener, dispatchEvent, registry ) { WindowPrototype[addEventListener] = DocumentPrototype[addEventListener] = ElementPrototype[addEventListener] = function (type, listener) { var target = this; registry.unshift([target, type, listener, function (event) { event.currentTarget = target; event.preventDefault = function () { event.returnValue = false; }; event.stopPropagation = function () { event.cancelBubble = true; }; event.target = event.srcElement || target; listener.call(target, event); }]); this.attachEvent("on" + type, registry[0][3]); }; WindowPrototype[removeEventListener] = DocumentPrototype[removeEventListener] = ElementPrototype[removeEventListener] = function (type, listener) { var index, register; for ( index = 0, register; register = registry[index]; ++index ) { if ( register[0] === this && register[1] === type && register[2] === listener ) { return this.detachEvent("on" + type, registry.splice(index, 1)[0][3]); } } }; WindowPrototype[dispatchEvent] = DocumentPrototype[dispatchEvent] = ElementPrototype[dispatchEvent] = function (eventObject) { return this.fireEvent("on" + eventObject.type, eventObject); }; }( Window.prototype, HTMLDocument.prototype, Element.prototype, "addEventListener", "removeEventListener", "dispatchEvent", [] )); } // Array extras if ( !Array.prototype.indexOf ) { Array.prototype.indexOf = function ( needle, i ) { var len; if ( i === undefined ) { i = 0; } if ( i < 0 ) { i+= this.length; } if ( i < 0 ) { i = 0; } for ( len = this.length; i<len; i++ ) { if ( this.hasOwnProperty( i ) && this[i] === needle ) { return i; } } return -1; }; } if ( !Array.prototype.forEach ) { Array.prototype.forEach = function ( callback, context ) { var i, len; for ( i=0, len=this.length; i<len; i+=1 ) { if ( this.hasOwnProperty( i ) ) { callback.call( context, this[i], i, this ); } } }; } if ( !Array.prototype.map ) { Array.prototype.map = function ( mapper, context ) { var i, len, mapped = []; for ( i=0, len=this.length; i<len; i+=1 ) { if ( this.hasOwnProperty( i ) ) { mapped[i] = mapper.call( context, this[i], i, this ); } } return mapped; }; } if ( !Array.prototype.map ) { Array.prototype.map = function ( filter, context ) { var i, len, filtered = []; for ( i=0, len=this.length; i<len; i+=1 ) { if ( this.hasOwnProperty( i ) && filter.call( context, this[i], i, this ) ) { filtered[ filtered.length ] = this[i]; } } return filtered; }; } }( document )); /*! Ractive - v0.3.0 - 2013-05-30 * Faster, easier, better interactive web development * 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, proto = {}, // properties of the public Ractive object adaptors = {}, eventDefinitions = {}, easing, extend, interpolate, interpolators, // internal utils splitKeypath, isArray, isObject, isNumeric, isEqual, getEl, // internally used caches keypathCache = {}, // internally used constructors DomFragment, TextFragment, Evaluator, Animation, // internally used regexes leadingWhitespace = /^\s+/, trailingWhitespace = /\s+$/, // other bits and pieces initMustache, updateMustache, resolveMustache, evaluateMustache, initFragment, updateSection, animationCollection, // array modification registerKeypathToArray, unregisterKeypathFromArray, // parser and tokenizer 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, REFERENCE = 30, REFINEMENT = 31, MEMBER = 32, PREFIX_OPERATOR = 33, BRACKETED = 34, CONDITIONAL = 35, INFIX_OPERATOR = 36, INVOCATION = 40, // 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/' }; proto.animate = function ( keypath, to, options ) { var easing, duration, animation, i, keys; options = options || {}; // 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(); } } // easing function if ( options.easing ) { if ( typeof options.easing === 'function' ) { easing = options.easing; } else { if ( this.easing && this.easing[ options.easing ] ) { // use instance easing function first easing = this.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 ); keys = splitKeypath( keypath ); animation = new Animation({ keys: keys, from: this.get( keys ), to: to, root: this, duration: duration, easing: easing, step: options.step, complete: options.complete }); animationCollection.push( animation ); }; proto.bind = function ( adaptor ) { var bound = this._bound; if ( bound.indexOf( adaptor ) === -1 ) { bound[ bound.length ] = adaptor; adaptor.init( this ); } }; 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 proto.get = function ( keypath, dontNormalise ) { var keys, normalised, key, parentKeypath, parentValue, value; if ( !keypath ) { return this.data; } if ( isArray( keypath ) ) { keys = keypath.slice(); // clone normalised = keys.join( '.' ); } else { // cache hit? great if ( this._cache.hasOwnProperty( keypath ) ) { return this._cache[ keypath ]; } keys = splitKeypath( keypath ); normalised = keys.join( '.' ); } // we may have a cache hit now that it's been normalised if ( this._cache.hasOwnProperty( normalised ) ) { return this._cache[ normalised ]; } // otherwise it looks like we need to do some work key = keys.pop(); parentValue = ( keys.length ? this.get( keys ) : this.data ); if ( typeof parentValue !== 'object' || !parentValue.hasOwnProperty( key ) ) { return; } value = parentValue[ key ]; // update map of dependants parentKeypath = keys.join( '.' ); if ( !this._depsMap[ parentKeypath ] ) { this._depsMap[ parentKeypath ] = []; } if ( this._depsMap[ parentKeypath ].indexOf( normalised ) === -1 ) { this._depsMap[ parentKeypath ].push( normalised ); } // 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 this._cache[ normalised ] = value; return value; }; var teardown, clearCache, registerDependant, unregisterDependant, notifyDependants, resolveRef; 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 dependency unregisterDependant( thing.root, thing.keypath, thing, thing.priority || 0 ); } if ( thing.evaluator ) { thing.evaluator.teardown(); } }; clearCache = function ( root, keypath ) { var value, dependants = root._depsMap[ keypath ], i; // is this a modified array, which shouldn't fire set events on this keypath anymore? if ( root.modifyArrays ) { if ( keypath.charAt( 0 ) !== '(' ) { // expressions don't get wrapped value = root._cache[ keypath ]; if ( isArray( value ) && !value._ractive.setting ) { unregisterKeypathFromArray( value, keypath, root ); } } } delete root._cache[ keypath ]; if ( !dependants ) { return; } i = dependants.length; while ( i-- ) { clearCache( root, dependants[i] ); } }; registerDependant = function ( root, keypath, dependant, priority ) { var deps; if ( !root._deps[ keypath ] ) { root._deps[ keypath ] = []; } deps = root._deps[ keypath ]; if ( !deps[ priority ] ) { deps[ priority ] = [ dependant ]; return; } deps = deps[ priority ]; if ( deps.indexOf( dependant ) === -1 ) { deps[ deps.length ] = dependant; } }; unregisterDependant = function ( root, keypath, dependant, priority ) { var deps, i, keep; deps = root._deps[ keypath ][ priority ]; deps.splice( deps.indexOf( dependant ), 1 ); if ( !deps.length ) { root._deps[ keypath ][ priority ] = null; } // can we forget this keypath altogether? i = root._deps[ keypath ].length; while ( i-- ) { if ( root._deps[ keypath ][i] ) { keep = true; break; } } if ( !keep ) { // yes, we can forget it root._deps[ keypath ] = null; } }; notifyDependants = function ( root, keypath, onlyDirect ) { var depsByPriority, deps, i, j, len, childDeps; depsByPriority = root._deps[ keypath ]; if ( depsByPriority ) { len = depsByPriority.length; for ( i=0; i<len; i+=1 ) { deps = depsByPriority[i]; if ( deps ) { j = deps.length; while ( j-- ) { deps[j].update(); } } } } // If we're only notifying direct dependants, not dependants // of downstream keypaths, then YOU SHALL NOT PASS if ( onlyDirect ) { return; } // cascade childDeps = root._depsMap[ keypath ]; if ( childDeps ) { i = childDeps.length; while ( i-- ) { notifyDependants( root, childDeps[i] ); // TODO at some point, no-longer extant dependants need to be removed // from the map. However a keypath can have no direct dependants yet // still have downstream dependants, so it's not entirely straightforward } } }; // 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 ( root, ref, contextStack ) { var keys, lastKey, innerMostContext, contextKeys, parentValue, keypath; // Implicit iterators - i.e. {{.}} - are a special case if ( ref === '.' ) { return contextStack[ contextStack.length - 1 ]; } 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 = root.get( contextKeys.concat( keys ) ); if ( typeof parentValue === 'object' && parentValue.hasOwnProperty( lastKey ) ) { keypath = innerMostContext + '.' + ref; break; } } if ( !keypath && root.get( ref ) !== undefined ) { keypath = ref; } return keypath; }; proto.link = function ( keypath ) { var self = this; return function ( value ) { self.set( keypath, value ); }; }; (function ( proto ) { var observe, updateObserver; proto.observe = function ( keypath, callback, options ) { var observers = [], k; if ( typeof keypath === 'object' ) { options = callback; for ( k in keypath ) { if ( keypath.hasOwnProperty( 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, lastValue, context; options = options || {}; context = options.context || root; observer = { update: function () { var value; // TODO create, and use, an internal get method instead - we can skip checks value = root.get( keypath, true ); if ( !isEqual( value, lastValue ) ) { callback.call( context, value, lastValue ); lastValue = value; } }, cancel: function () { unregisterDependant( root, keypath, observer, 0 ); } }; if ( options.init !== false ) { observer.update(); } registerDependant( root, keypath, observer, 0 ); return observer; }; }( 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 ( eventName.hasOwnProperty( 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 ); } }; }; // Render instance to element specified here or at initialization proto.render = function ( options ) { var el = ( options.el ? getEl( options.el ) : this.el ); if ( !el ) { throw new Error( 'You must specify a DOM element to render to' ); } // Clear the element, unless `append` is `true` if ( !options.append ) { el.innerHTML = ''; } if ( options.callback ) { this.callback = options.callback; } // Render our *root fragment* this.rendered = new DomFragment({ descriptor: this.template, root: this, owner: this, // saves doing `if ( this.parent ) { /*...*/ }` later on parentNode: el }); el.appendChild( this.rendered.docFrag ); }; (function ( proto ) { var set, attemptKeypathResolution; proto.set = function ( keypath, value ) { var notificationQueue, upstreamQueue, k, normalised, keys, previous; if ( !this.setting ) { this.setting = true; // short-circuit any potential infinite loops this.fire( 'set', keypath, value ); this.setting = false; } upstreamQueue = []; notificationQueue = []; // setting multiple values in one go if ( isObject( keypath ) ) { for ( k in keypath ) { if ( keypath.hasOwnProperty( 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, notify dependants and attempt to resolve // any unresolved keypaths if ( notificationQueue.length ) { while ( notificationQueue.length ) { notifyDependants( this, notificationQueue.pop() ); } attemptKeypathResolution( this ); } // notify direct descendants of upstream keypaths while ( upstreamQueue.length ) { notifyDependants( this, upstreamQueue.pop(), true ); } // 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 while ( this._def.length ) { // Update the attribute, then deflag it this._def.pop().update().deferred = false; } }; set = function ( root, keypath, keys, value, queue, upstreamQueue ) { var previous, key, obj, keysClone; keysClone = keys.slice(); previous = root.get( keypath ); // update the model, if necessary if ( previous !== value ) { // update data obj = root.data; while ( keys.length > 1 ) { key = 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 ] ) { 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, keypath ); // add this keypath to the notification queue queue[ queue.length ] = keypath; // add upstream keypaths to the upstream notification queue while ( keysClone.length ) { 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 () { var keypath; this.rendered.teardown(); // Clear cache - this has the side-effect of unregistering keypaths from modified arrays. // Once with keypaths that have dependents... for ( keypath in this._depsMap ) { if ( this._depsMap.hasOwnProperty( keypath ) ) { clearCache( this, keypath ); } } // Then a second time to mop up the rest for ( keypath in this._cache ) { if ( this._cache.hasOwnProperty( keypath ) ) { clearCache( this, keypath ); } } // Teardown any bindings while ( this._bound.length ) { this.unbind( this._bound.pop() ); } }; 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 ) { clearCache( this, keypath || '' ); notifyDependants( this, keypath || '' ); this.fire( 'update:' + keypath ); this.fire( 'update', keypath ); 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 ( attrs.hasOwnProperty( 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; path = ( path ? path + '.' : '' ); return { init: function ( view ) { setView = function ( keypath, value ) { if ( !settingModel ) { settingView = true; view.set( keypath, value ); settingView = false; } }; setModel = function ( keypath, value ) { if ( !settingView ) { settingModel = true; model.set( keypath, value ); settingModel = false; } }; model.on( 'set', setView ); view.on( 'set', setModel ); // initialise view.set( model.get() ); }, teardown: function ( view ) { model.off( 'change', setView ); view.off( 'set', setModel ); } }; }; // These are a subset of the easing equations found at // https://raw.github.com/danro/easing-js - license info // follows: // -------------------------------------------------- // easing.js v0.5.4 // Generic set of easing functions with AMD support // https://github.com/danro/easing-js // This code may be freely distributed under the MIT license // http://danro.mit-license.org/ // -------------------------------------------------- // All functions adapted from Thomas Fuchs & Jeremy Kahn // Easing Equations (c) 2003 Robert Penner, BSD license // https://raw.github.com/danro/easing-js/master/LICENSE // -------------------------------------------------- // In that library, the functions named easeIn, easeOut, and // easeInOut below are named easeInCubic, easeOutCubic, and // (you guessed it) easeInOutCubic. // // You can add additional easing functions to this list, and they // will be globally available. easing = { linear: function ( pos ) { return pos; }, easeIn: function ( pos ) { return Math.pow( pos, 3 ); }, easeOut: function ( pos ) { return ( Math.pow( ( pos - 1 ), 3 ) + 1 ); }, easeInOut: function ( pos ) { if ( ( pos /= 0.5 ) < 1 ) { return ( 0.5 * Math.pow( pos, 3 ) ); } return ( 0.5 * ( Math.pow( ( pos - 2 ), 3 ) + 2 ) ); } }; eventDefinitions.tap = function ( el, fire ) { var mousedown, touchstart, distanceThreshold, timeThreshold; distanceThreshold = 5; // maximum pixels pointer can move before cancel timeThreshold = 400; // maximum milliseconds between down and up before cancel mousedown = function ( event ) { var x, y, currentTarget, up, move, cancel; x = event.clientX; y = event.clientY; currentTarget = this; up = function ( event ) { fire.call( currentTarget, event ); cancel(); }; move = function ( event ) { if ( ( Math.abs( event.clientX - x ) >= distanceThreshold ) || ( Math.abs( event.clientY - y ) >= distanceThreshold ) ) { cancel(); } }; cancel = function () { window.removeEventListener( 'mousemove', move ); window.removeEventListener( 'mouseup', up ); }; window.addEventListener( 'mousemove', move ); window.addEventListener( 'mouseup', up ); setTimeout( cancel, timeThreshold ); }; el.addEventListener( 'mousedown', mousedown ); touchstart = function ( event ) { var x, y, touch, finger, move, up, cancel; if ( event.touches.length !== 1 ) { return; } touch = event.touches[0]; x = touch.clientX; y = touch.clientY; finger = touch.identifier; up = function ( event ) { var touch; touch = event.changedTouches[0]; if ( touch.identifier !== finger ) { cancel(); } fire.call( touch.target, event ); }; move = function ( event ) { var touch; if ( event.touches.length !== 1 || event.touches[0].identifier !== finger ) { cancel(); } touch = event.touches[0]; if ( ( Math.abs( touch.clientX - x ) >= distanceThreshold ) || ( Math.abs( touch.clientY - y ) >= distanceThreshold ) ) { cancel(); } }; cancel = function () { window.removeEventListener( 'touchmove', move ); window.removeEventListener( 'touchend', up ); window.removeEventListener( 'touchcancel', cancel ); }; window.addEventListener( 'touchmove', move ); window.addEventListener( 'touchend', up ); window.addEventListener( 'touchcancel', cancel ); setTimeout( cancel, timeThreshold ); }; return { teardown: function () { el.removeEventListener( 'mousedown', mousedown ); el.removeEventListener( 'touchstart', touchstart ); } }; }; var fillGaps = function ( target, source ) { var key; for ( key in source ) { if ( source.hasOwnProperty( key ) && !target.hasOwnProperty( key ) ) { target[ key ] = source[ key ]; } } }; extend = function ( childProps ) { var Parent, Child, key, inheritedOptions, blacklist, template, partials, partial; Parent = this; inheritedOptions = [ 'el', 'preserveWhitespace', 'append', 'twoway', 'modifyArrays' ]; blacklist = inheritedOptions.concat( 'data', 'template' ); // Parse template if ( childProps.template ) { if ( typeof childProps.template === 'string' ) { if ( !Ractive.parse ) { throw new Error( missingParser ); } template = Ractive.parse( childProps.template ); } else { template = childProps.template; } } // Parse partials, if necessary if ( childProps.partials ) { partials = {}; for ( key in childProps.partials ) { if ( childProps.partials.hasOwnProperty( key ) ) { if ( typeof childProps.partials[ key ] === 'string' ) { if ( !Ractive.parse ) { throw new Error( missingParser ); } partial = Ractive.parse( childProps.partials[ key ], childProps ); } else { partial = childProps.partials[ key ]; } partials[ key ] = partial; } } } Child = function ( options ) { var key, i, optionName; // Add template to options, if necessary if ( !options.template && template ) { options.template = template; } // Extend subclass data with instance data if ( !options.data ) { options.data = {}; } fillGaps( options.data, childProps.data ); // Add in preparsed partials if ( partials ) { if ( !options.partials ) { options.partials = {}; } fillGaps( options.partials, partials ); } i = inheritedOptions.length; while ( i-- ) { optionName = inheritedOptions[i]; if ( !options.hasOwnProperty( optionName ) && childProps.hasOwnProperty( optionName ) ) { options[ optionName ] = childProps[ optionName ]; } } Ractive.call( this, options ); if ( this.init ) { this.init.call( this, options ); } }; // extend child with parent methods for ( key in Parent.prototype ) { if ( Parent.prototype.hasOwnProperty( key ) ) { Child.prototype[ key ] = Parent.prototype[ key ]; } } // Extend child with specified methods, as long as they don't override Ractive.prototype methods. // Blacklisted properties don't extend the child, as they are part of the initialisation options for ( key in childProps ) { if ( childProps.hasOwnProperty( key ) && blacklist.indexOf( key ) === -1 ) { if ( Ractive.prototype.hasOwnProperty( key ) ) { throw new Error( 'Cannot override "' + key + '" method or property of Ractive prototype' ); } Child.prototype[ key ] = childProps[ key ]; } } Child.extend = Parent.extend; return Child; }; interpolate = function ( from, to ) { if ( isNumeric( from ) && isNumeric( to ) ) { return Ractive.interpolators.number( +from, +to ); } if ( isArray( from ) && isArray( to ) ) { return Ractive.interpolators.array( from, to ); } if ( isObject( from ) && isObject( to ) ) { return Ractive.interpolators.object( from, to ); } return function () { return to; }; }; interpolators = { number: function ( from, to ) { var delta = to - from; if ( !delta ) { return function () { return from; }; } return function ( t ) { return from + ( t * delta ); }; }, array: function ( from, to ) { var intermediate, interpolators, len, i; intermediate = []; interpolators = []; i = len = Math.min( from.length, to.length ); while ( i-- ) { interpolators[i] = Ractive.interpolate( from[i], to[i] ); } // surplus values - don't interpolate, but don't exclude them either for ( i=len; i<from.length; i+=1 ) { intermediate[i] = from[i]; } for ( i=len; i<to.length; i+=1 ) { intermediate[i] = to[i]; } return function ( t ) { var i = len; while ( i-- ) { intermediate[i] = interpolators[i]( t ); } return intermediate; }; }, object: function ( from, to ) { var properties = [], len, interpolators, intermediate, prop; intermediate = {}; interpolators = {}; for ( prop in from ) { if ( from.hasOwnProperty( prop ) ) { if ( to.hasOwnProperty( prop ) ) { properties[ properties.length ] = prop; interpolators[ prop ] = Ractive.interpolate( from[ prop ], to[ prop ] ); } else { intermediate[ prop ] = from[ prop ]; } } } for ( prop in to ) { if ( to.hasOwnProperty( prop ) && !from.hasOwnProperty( prop ) ) { intermediate[ prop ] = to[ prop ]; } } len = properties.length; return function ( t ) { var i = len, prop; while ( i-- ) { prop = properties[i]; intermediate[ prop ] = interpolators[ prop ]( t ); } return intermediate; }; } }; var defaultOptions = { preserveWhitespace: false, append: false, twoway: true, modifyArrays: true, data: {} }; Ractive = function ( options ) { var key, partial, i, template, templateEl; // Options // ------- for ( key in defaultOptions ) { if ( defaultOptions.hasOwnProperty( key ) && !options.hasOwnProperty( key ) ) { options[ key ] = defaultOptions[ key ]; } } // Initialization // -------------- this.el = getEl( options.el ); // add data this.data = options.data || {}; // Set up event bus this._subs = {}; // Set up cache this._cache = {}; // Set up dependants this._deps = {}; this._depsMap = {}; // Node registry this.nodes = {}; // Set up observers this._pendingResolution = []; // Create an array for deferred attributes this._def = []; // Cache proxy event handlers - allows efficient reuse this._proxies = {}; // Keep a list of used expressions, so we don't duplicate them this._expressions = []; // Set up bindings this._bound = []; if ( options.bindings ) { if ( isArray( options.bindings ) ) { for ( i=0; i<options.bindings.length; i+=1 ) { this.bind( options.bindings[i] ); } } else { this.bind( options.bindings ); } } // If we were given unparsed partials, parse them if ( options.partials ) { this.partials = {}; for ( key in options.partials ) { if ( options.partials.hasOwnProperty( key ) ) { partial = options.partials[ key ]; if ( typeof partial === 'string' ) { if ( !Ractive.parse ) { throw new Error( missingParser ); } partial = Ractive.parse( partial, options ); } // If the partial was an array with a single string member, that means // we can use innerHTML - we just need to unpack it if ( partial.length === 1 && typeof partial[0] === 'string' ) { partial = partial[0]; } this.partials[ key ] = partial; } } } // Compile template, if it hasn't been parsed already template = options.template; if ( typeof template === 'string' ) { if ( !Ractive.parse ) { throw new Error( missingParser ); } if ( template.charAt( 0 ) === '#' ) { // assume this is an ID of a <script type='text/template'> tag templateEl = document.getElementById( template.substring( 1 ) ); if ( templateEl ) { this.template = Ractive.parse( templateEl.innerHTML, options ); } else { throw new Error( 'Could not find template element (' + template + ')' ); } } else { template = Ractive.parse( template, options ); } } // If the template was an array with a single string member, that means // we can use innerHTML - we just need to unpack it if ( template && ( template.length === 1 ) && ( typeof template[0] === 'string' ) ) { this.template = template[0]; } else { this.template = template; } // If passed an element, render immediately if ( this.el ) { this.render({ el: this.el, append: options.append }); } }; Animation = function ( options ) { var key; this.startTime = Date.now(); // from and to for ( key in options ) { if ( options.hasOwnProperty( key ) ) { this[ key ] = options[ key ]; } } this.interpolator = Ractive.interpolate( this.from, this.to ); this.running = true; }; Animation.prototype = { tick: function () { var elapsed, t, value, timeNow; if ( this.running ) { timeNow = Date.now(); elapsed = timeNow - this.startTime; if ( elapsed >= this.duration ) { this.root.set( this.keys, this.to ); if ( this.step ) { this.step( 1, this.to ); } if ( this.complete ) { this.complete( 1, this.to ); } this.running = false; return false; } t = this.easing ? this.easing ( elapsed / this.duration ) : ( elapsed / this.duration ); value = this.interpolator( t ); this.root.set( this.keys, value ); if ( this.step ) { this.step( t, value ); } return true; } return false; }, stop: function () { this.running = false; } }; animationCollection = { animations: [], tick: function () { var i, animation; for ( i=0; i<this.animations.length; i+=1 ) { animation = this.animations[i]; if ( !animation.tick() ) { // animation is complete, remove it from the stack, and decrement i so we don't miss one this.animations.splice( i--, 1 ); } } if ( this.animations.length ) { global.requestAnimationFrame( this.boundTick ); } else { this.running = false; } }, // bind method to animationCollection boundTick: function () { animationCollection.tick(); }, push: function ( animation ) { this.animations[ this.animations.length ] = animation; if ( !this.running ) { this.running = true; this.tick(); } } }; // https://gist.github.com/paulirish/1579671 (function( vendors, lastTime, global ) { var x; for ( x = 0; x < vendors.length && !global.requestAnimationFrame; ++x ) { global.requestAnimationFrame = global[vendors[x]+'RequestAnimationFrame']; global.cancelAnimationFrame = global[vendors[x]+'CancelAnimationFrame'] || global[vendors[x]+'CancelRequestAnimationFrame']; } if ( !global.requestAnimationFrame ) { global.requestAnimationFrame = function(callback) { var currTime, timeToCall, id; currTime = Date.now(); timeToCall = Math.max( 0, 16 - (currTime - lastTime ) ); id = global.setTimeout( function() { callback(currTime + timeToCall); }, timeToCall ); lastTime = currTime + timeToCall; return id; }; } if ( !global.cancelAnimationFrame ) { global.cancelAnimationFrame = function( id ) { global.clearTimeout( id ); }; } }( ['ms', 'moz', 'webkit', 'o'], 0, global )); (function () { var define, notifyDependents, wrapArray, unwrapArray, WrappedArrayProto, testObj, mutatorMethods; // just in case we don't have Object.defineProperty, we can use this - it doesn't // allow us to set non-enumerable properties, but if you're doing for ... in loops on // an array then you deserve what's coming anyway if ( !Object.defineProperty ) { define = function ( obj, prop, desc ) { obj[ prop ] = desc.value; }; } else { define = Object.defineProperty; } // Register a keypath to this array. When any of this array's mutator methods are called, // it will `set` that keypath on the given Ractive instance registerKeypathToArray = function ( array, keypath, root ) { var roots, keypathsByIndex, rootIndex, keypaths; // If this array hasn't been wrapped, we need to wrap it if ( !array._ractive ) { define( array, '_ractive', { value: { roots: [ root ], // there may be more than one Ractive instance depending on this keypathsByIndex: [ [ keypath ] ] }, configurable: true }); wrapArray( array ); } else { roots = array._ractive.roots; keypathsByIndex = array._ractive.keypathsByIndex; // Does this Ractive instance currently depend on this array? rootIndex = roots.indexOf( root ); // If not, associate them if ( rootIndex === -1 ) { rootIndex = roots.length; roots[ rootIndex ] = root; } // Find keypaths that reference this array, on this Ractive instance if ( !keypathsByIndex[ rootIndex ] ) { keypathsByIndex[ rootIndex ] = []; } keypaths = keypathsByIndex[ rootIndex ]; // If the current keypath isn't among them, add it if ( keypaths.indexOf( keypath ) === -1 ) { keypaths[ keypaths.length ] = keypath; } } }; // Unregister keypath from array unregisterKeypathFromArray = function ( array, keypath, root ) { var roots, keypathsByIndex, rootIndex, keypaths, keypathIndex; if ( !array._ractive ) { throw new Error( 'Attempted to remove keypath from non-wrapped array. This error is unexpected - please send a bug report to @rich_harris' ); } roots = array._ractive.roots; rootIndex = roots.indexOf( root ); if ( rootIndex === -1 ) { throw new Error( 'Ractive instance was not listed as a dependent of this array. This error is unexpected - please send a bug report to @rich_harris' ); } keypathsByIndex = array._ractive.keypathsByIndex; keypaths = keypathsByIndex[ rootIndex ]; keypathIndex = keypaths.indexOf( keypath ); if ( keypathIndex === -1 ) { throw new Error( 'Attempted to unlink non-linked keypath from array. This error is unexpected - please send a bug report to @rich_harris' ); } keypaths.splice( keypathIndex, 1 ); if ( !keypaths.length ) { roots.splice( rootIndex, 1 ); } if ( !roots.length ) { unwrapArray( array ); // It's good to clean up after ourselves } }; // Call `set` on each dependent Ractive instance, for each dependent keypath notifyDependents = function ( array ) { var roots, keypathsByIndex, root, keypaths, i, j; roots = array._ractive.roots; keypathsByIndex = array._ractive.keypathsByIndex; i = roots.length; while ( i-- ) { root = roots[i]; keypaths = keypathsByIndex[i]; j = keypaths.length; while ( j-- ) { root.set( keypaths[j], array ); } } }; WrappedArrayProto = []; mutatorMethods = [ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ]; mutatorMethods.forEach( function ( methodName ) { var method = function () { var result = Array.prototype[ methodName ].apply( this, arguments ); this._ractive.setting = true; notifyDependents( this ); this._ractive.setting = false; return result; }; define( WrappedArrayProto, 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 wrapArray = function ( array ) { array.__proto__ = WrappedArrayProto; }; unwrapArray = function ( array ) { delete array._ractive; array.__proto__ = Array.prototype; }; } else { // no, we can't wrapArray = function ( array ) { var i, methodName; i = mutatorMethods.length; while ( i-- ) { methodName = mutatorMethods[i]; define( array, methodName, { value: WrappedArrayProto[ methodName ] }); } }; unwrapArray = function ( array ) { var i; i = mutatorMethods.length; while ( i-- ) { delete array[ mutatorMethods[i] ]; } delete array._ractive; }; } }()); (function ( evaluators, functionCache ) { var Reference, getFunctionFromString; getFunctionFromString = function ( functionString, i ) { var fn, args; if ( functionCache[ functionString ] ) { return functionCache[ functionString ]; } args = []; while ( i-- ) { args[i] = '_' + i; } fn = new Function( args.join( ',' ), 'return(' + functionString + ')' ); functionCache[ functionString ] = fn; return fn; }; Evaluator = function ( root, mustache, contextStack, indexRefs, descriptor ) { var i; this.root = root; this.mustache = mustache; this.priority = mustache.priority; this.str = descriptor.s; this.keypaths = []; this.override = []; // need to override index refs when creating a keypath this.values = []; if ( !descriptor.r ) { // no references - we can init immediately this.init(); } else { this.resolvers = []; this.unresolved = this.numRefs = i = descriptor.r.length; while ( i-- ) { // index ref? if ( indexRefs && indexRefs.hasOwnProperty( descriptor.r[i] ) ) { this.values[i] = this.override[i] = indexRefs[ descriptor.r[i] ]; this.unresolved -= 1; // because we don't need to resolve the reference } else { this.resolvers[ this.resolvers.length ] = new Reference( root, descriptor.r[i], contextStack, i, this ); } } // if this only has one reference (and therefore only one dependency) it can // update its mustache whenever that dependency changes. Otherwise, it should // wait until all the information is in before re-evaluating (same principle // as element attributes) if ( this.resolvers.length <= 1 ) { this.selfUpdating = true; } // if we have no unresolved references, but we haven't initialised (because // one or more of the references were index references), initialise now if ( !this.unresolved && !this.resolved ) { this.init(); } } }; Evaluator.prototype = { init: function () { var self = this, functionString; // we're ready! this.resolved = true; this.keypath = '(' + this.str.replace( /❖([0-9]+)/g, function ( match, $1 ) { if ( self.override.hasOwnProperty( $1 ) ) { return self.override[ $1 ]; } return self.keypaths[ $1 ]; }) + ')'; // is this the first of its kind? if ( this.root._expressions.indexOf( this.keypath ) === -1 ) { // yes functionString = this.str.replace( /❖([0-9]+)/g, function ( match, $1 ) { return '_' + $1; }); this.fn = getFunctionFromString( functionString, this.numRefs || 0 ); this.update(); this.root._expressions.push( this.keypath ); } else { // no. tear it down! our mustache will be taken care of by the other expression // with the same virtual keypath this.teardown(); } this.mustache.resolve( this.keypath ); }, teardown: function () { if ( this.resolvers ) { while ( this.resolvers.length ) { this.resolvers.pop().teardown(); } } }, resolve: function ( ref, argNum, keypath ) { var self = this; this.keypaths[ argNum ] = keypath; this.unresolved -= 1; if ( !this.unresolved ) { this.init(); } }, 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 resovled, obviously else if ( !this.deferred && this.resolved ) { this.root._def[ this.root._def.length ] = this; this.deferred = true; } }, update: function () { var value; if ( !this.resolved ) { return this; } try { value = this.getter(); } catch ( err ) { if ( this.root.debug ) { throw err; } else { value = undefined; } } if ( !isEqual( value, this._lastValue ) ) { clearCache( this.root, this.keypath ); this.root._cache[ this.keypath ] = value; notifyDependants( this.root, this.keypath ); this._lastValue = value; } return this; }, getter: function () { return this.fn.apply( null, this.values ); } }; Reference = function ( root, ref, contextStack, argNum, evaluator ) { var keypath; this.ref = ref; this.root = root; this.evaluator = evaluator; this.priority = evaluator.priority; this.argNum = argNum; keypath = resolveRef( root, ref, contextStack ); if ( keypath ) { this.resolve( keypath ); } else { this.contextStack = contextStack; root._pendingResolution[ root._pendingResolution.length ] = this; } }; Reference.prototype = { teardown: function () { teardown( this ); }, resolve: function ( keypath ) { this.keypath = keypath; registerDependant( this.root, keypath, this, this.priority ); this.update(); this.evaluator.resolve( this.ref, this.argNum, keypath ); }, update: function () { var value = this.root.get( this.keypath ); if ( !isEqual( value, this._lastValue ) ) { this.evaluator.values[ this.argNum ] = value; this.evaluator.bubble(); this._lastValue = value; } } }; }({}, {})); initFragment = function ( fragment, options ) { var numItems, i, itemOptions, parentRefs, ref; // The item that owns this fragment - an element, section, partial, or attribute fragment.owner = options.owner; // 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 ( fragment.owner.parentFragment ) { parentRefs = fragment.owner.parentFragment.indexRefs; if ( parentRefs ) { fragment.indexRefs = {}; for ( ref in parentRefs ) { if ( parentRefs.hasOwnProperty( ref ) ) { fragment.indexRefs[ ref ] = parentRefs[ ref ]; } } } } 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 = { root: options.root, parentFragment: fragment, parentNode: options.parentNode, contextStack: options.contextStack }; 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; mustache.root = options.root; mustache.descriptor = options.descriptor; mustache.parentFragment = options.parentFragment; mustache.contextStack = options.contextStack || []; mustache.index = options.index || 0; mustache.priority = options.descriptor.p || 0; // DOM only if ( options.parentNode || options.anchor ) { mustache.parentNode = options.parentNode; mustache.anchor = options.anchor; } 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 ( mustache.parentFragment.indexRefs && mustache.parentFragment.indexRefs.hasOwnProperty( options.descriptor.r ) ) { index = mustache.parentFragment.indexRefs[ options.descriptor.r ]; mustache.render( index ); } 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.roo