UNPKG

ractive

Version:

Next-generation DOM manipulation

1,867 lines (1,421 loc) 110 kB
/*! Ractive - v0.2.2 - 2013-05-07 * 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, _internal; (function () { 'use strict'; var getEl; Ractive = function ( options ) { var defaults, key, partial; // Options // ------- if ( options ) { for ( key in options ) { if ( options.hasOwnProperty( key ) ) { this[ key ] = options[ key ]; } } } defaults = { preserveWhitespace: false, append: false, twoway: true, modifiers: {}, modifyArrays: true, data: {} }; for ( key in defaults ) { if ( defaults.hasOwnProperty( key ) && this[ key ] === undefined ) { this[ key ] = defaults[ key ]; } } // 'formatters' is deprecated, but support it for the time being if ( options && options.formatters ) { this.modifiers = options.formatters; if ( typeof console !== 'undefined' ) { console.warn( 'The \'formatters\' option is deprecated as of v0.2.2 and will be removed in a future version - use \'modifiers\' instead (same thing, more accurate name)' ); } } // Initialization // -------------- if ( this.el !== undefined ) { this.el = getEl( this.el ); // turn ID string into DOM element } // Set up event bus this._subs = {}; // Set up cache this._cache = {}; this._cacheMap = {}; // Set up observers this._observers = {}; this._pendingResolution = []; // Create an array for deferred attributes this._defAttrs = []; // If we were given uncompiled partials, compile them if ( this.partials ) { for ( key in this.partials ) { if ( this.partials.hasOwnProperty( key ) ) { partial = this.partials[ key ]; if ( typeof partial === 'string' ) { if ( !Ractive.compile ) { throw new Error( 'Missing Ractive.compile - cannot compile partial "' + key + '". Either precompile or use the version that includes the compiler' ); } partial = Ractive.compile( partial, this ); // all compiler options are present on `this`, so just passing `this` } // 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 compiled already if ( typeof this.template === 'string' ) { if ( !Ractive.compile ) { throw new Error( 'Missing Ractive.compile - cannot compile template. Either precompile or use the version that includes the compiler' ); } this.template = Ractive.compile( this.template, this ); } // If the template was an array with a single string member, that means // we can use innerHTML - we just need to unpack it if ( this.template && ( this.template.length === 1 ) && ( typeof this.template[0] === 'string' ) ) { this.template = this.template[0]; } // If passed an element, render immediately if ( this.el ) { this.render({ el: this.el, append: this.append }); } }; // Prototype methods // ================= Ractive.prototype = { // Render instance to element specified here or at initialization 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 _internal.DomFragment({ descriptor: this.template, root: this, parentNode: el }); el.appendChild( this.rendered.docFrag ); }, // Teardown. This goes through the root fragment and all its children, removing observers // and generally cleaning up after itself 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._cacheMap ) { if ( this._cacheMap.hasOwnProperty( keypath ) ) { this._clearCache( keypath ); } } // Then a second time to mop up the rest for ( keypath in this._cache ) { if ( this._cache.hasOwnProperty( keypath ) ) { this._clearCache( keypath ); } } }, set: function ( keypath, value ) { if ( _internal.isObject( keypath ) ) { this._setMultiple( keypath ); } else { this._setSingle( keypath, value ); } // 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._defAttrs.length ) { // Update the attribute, then deflag it this._defAttrs.pop().update().deferred = false; } }, _setSingle: function ( keypath, value ) { var keys, key, obj, normalised, i, unresolved; if ( _internal.isArray( keypath ) ) { keys = keypath.slice(); } else { keys = _internal.splitKeypath( keypath ); } normalised = keys.join( '.' ); // Clear cache this._clearCache( normalised ); // update data obj = this.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; // Fire set event if ( !this.setting ) { this.setting = true; // short-circuit any potential infinite loops this.fire( 'set', normalised, value ); this.fire( 'set:' + normalised, value ); this.setting = false; } // Trigger updates of mustaches that observe `keypaths` or its descendants this._notifyObservers( normalised ); // See if we can resolve any of the unresolved keypaths (if such there be) i = this._pendingResolution.length; while ( i-- ) { // Work backwards, so we don't go in circles! unresolved = this._pendingResolution.splice( i, 1 )[0]; // If we can't resolve the reference, add to the back of // the queue (this is why we're working backwards) if ( !this._resolveRef( unresolved ) ) { this._pendingResolution[ this._pendingResolution.length ] = unresolved; } } }, _setMultiple: function ( map ) { var keypath; for ( keypath in map ) { if ( map.hasOwnProperty( keypath ) ) { this._setSingle( keypath, map[ keypath ] ); } } }, _clearCache: function ( keypath ) { var value, children = this._cacheMap[ keypath ]; // is this a modified array, which shouldn't fire set events on this keypath anymore? if ( this.modifyArrays ) { value = this._cache[ keypath ]; if ( _internal.isArray( value ) && !value._ractive.setting ) { _internal.removeKeypath( value, keypath, this ); } } delete this._cache[ keypath ]; if ( !children ) { return; } while ( children.length ) { this._clearCache( children.pop() ); } }, get: function ( keypath ) { var keys, normalised, key, match, parentKeypath, parentValue, value, modifiers; if ( _internal.isArray( keypath ) ) { keys = keypath.slice(); // clone normalised = keys.join( '.' ); } else { // cache hit? great if ( this._cache.hasOwnProperty( keypath ) ) { return this._cache[ keypath ]; } keys = _internal.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 ); // is this a set of modifiers? if ( match = /^⭆(.+)⭅$/.exec( key ) ) { modifiers = _internal.getModifiersFromString( match[1] ); value = this._modify( parentValue, modifiers ); } else { if ( typeof parentValue !== 'object' || !parentValue.hasOwnProperty( key ) ) { return; } value = parentValue[ key ]; } // update cacheMap if ( keys.length ) { parentKeypath = keys.join( '.' ); if ( !this._cacheMap[ parentKeypath ] ) { this._cacheMap[ parentKeypath ] = []; } this._cacheMap[ parentKeypath ].push( normalised ); } // Allow functions as values if ( typeof value === 'function' ) { value = value(); } // Is this an array that needs to be wrapped? else if ( this.modifyArrays ) { if ( _internal.isArray( value ) && ( !value.ractive || !value._ractive.setting ) ) { _internal.addKeypath( value, normalised, this ); } } // Update cache this._cache[ normalised ] = value; return value; }, update: function ( keypath ) { this._clearCache( keypath ); this._notifyObservers( keypath ); this.fire( 'update:' + keypath ); this.fire( 'update', keypath ); return this; }, link: function ( keypath ) { var self = this; return function ( value ) { self.set( keypath, value ); }; }, _registerMustache: function ( mustache ) { var resolved, value, index; if ( mustache.parentFragment && ( mustache.parentFragment.indexRefs.hasOwnProperty( mustache.descriptor.r ) ) ) { // This isn't a real keypath, it's an index reference index = mustache.parentFragment.indexRefs[ mustache.descriptor.r ]; value = ( mustache.descriptor.m ? this._modify( index, mustache.descriptor.m ) : index ); mustache.update( value ); return; // This value will never change, and doesn't have a keypath } // See if we can resolve a keypath from this mustache's reference (e.g. // does 'bar' in {{#foo}}{{bar}}{{/foo}} mean 'bar' or 'foo.bar'?) resolved = this._resolveRef( mustache ); if ( !resolved ) { // We may still need to do an update, event with unresolved // references, if the mustache has modifiers that (for example) // provide a fallback value from undefined if ( mustache.descriptor.m ) { mustache.update( this._modify( undefined, mustache.descriptor.m ) ); } this._pendingResolution[ this._pendingResolution.length ] = mustache; } }, // 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 ( mustache ) { var ref, contextStack, keys, lastKey, innerMostContext, contextKeys, parentValue, keypath; ref = mustache.descriptor.r; contextStack = mustache.contextStack; // Implicit iterators - i.e. {{.}} - are a special case if ( ref === '.' ) { keypath = contextStack[ contextStack.length - 1 ]; } else { keys = _internal.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 = _internal.splitKeypath( innerMostContext ); parentValue = this.get( contextKeys.concat( keys ) ); if ( typeof parentValue === 'object' && parentValue.hasOwnProperty( lastKey ) ) { keypath = innerMostContext + '.' + ref; break; } } if ( !keypath && this.get( ref ) !== undefined ) { keypath = ref; } } // If we have any modifiers, we need to append them to the keypath if ( keypath ) { mustache.keypath = ( mustache.descriptor.m ? keypath + '.' + _internal.stringifyModifiers( mustache.descriptor.m ) : keypath ); mustache.keys = _internal.splitKeypath( mustache.keypath ); mustache.observerRefs = this._observe( mustache ); mustache.update( this.get( mustache.keypath ) ); return true; // indicate success } return false; // failure }, _cancelKeypathResolution: function ( mustache ) { var index = this._pendingResolution.indexOf( mustache ); if ( index !== -1 ) { this._pendingResolution.splice( index, 1 ); } }, // Internal method to modify a value, using modifiers passed in at initialization _modify: function ( value, modifiers ) { var i, numModifiers, modifier, name, args, fn; // If there are no modifiers, groovy - just return the value unchanged if ( !modifiers ) { return value; } // Otherwise go through each in turn, applying sequentially numModifiers = modifiers.length; for ( i=0; i<numModifiers; i+=1 ) { modifier = modifiers[i]; name = modifier.d; args = modifier.g || []; // If a modifier was passed in, use it, otherwise see if there's a default // one with this name fn = this.modifiers[ name ] || Ractive.modifiers[ name ]; if ( fn ) { value = fn.apply( this, [ value ].concat( args ) ); } } return value; }, _notifyObservers: function ( keypath ) { var self = this, observersGroupedByPriority = this._observers[ keypath ] || [], i, j, priorityGroup, observer; for ( i=0; i<observersGroupedByPriority.length; i+=1 ) { priorityGroup = observersGroupedByPriority[i]; if ( priorityGroup ) { for ( j=0; j<priorityGroup.length; j+=1 ) { observer = priorityGroup[j]; observer.update( self.get( observer.keys ) ); } } } }, _observe: function ( mustache ) { var self = this, observerRefs = [], observe, keys, priority = mustache.descriptor.p || 0; observe = function ( keypath ) { var observers; observers = self._observers[ keypath ] = self._observers[ keypath ] || []; observers = observers[ priority ] = observers[ priority ] || []; observers[ observers.length ] = mustache; observerRefs[ observerRefs.length ] = { keypath: keypath, priority: priority, mustache: mustache }; }; keys = _internal.splitKeypath( mustache.keypath ); while ( keys.length > 1 ) { observe( keys.join( '.' ) ); // remove the last item in the keypath, so that `data.set( 'parent', { child: 'newValue' } )` // affects mustaches dependent on `parent.child` keys.pop(); } observe( keys[0] ); return observerRefs; }, _unobserve: function ( observerRef ) { var priorityGroups, observers, index, i, len; priorityGroups = this._observers[ observerRef.keypath ]; if ( !priorityGroups ) { // nothing to unobserve return; } observers = priorityGroups[ observerRef.priority ]; if ( !observers ) { // nothing to unobserve return; } if ( observers.indexOf ) { index = observers.indexOf( observerRef.observer ); } else { // fuck you IE for ( i=0, len=observers.length; i<len; i+=1 ) { if ( observers[i] === observerRef.mustache ) { index = i; break; } } } if ( index === -1 ) { // nothing to unobserve return; } // remove the observer from the list... observers.splice( index, 1 ); // ...then tidy up if necessary if ( observers.length === 0 ) { delete priorityGroups[ observerRef.priority ]; } if ( priorityGroups.length === 0 ) { delete this._observers[ observerRef.keypath ]; } }, _unobserveAll: function ( observerRefs ) { while ( observerRefs.length ) { this._unobserve( observerRefs.shift() ); } } }; // helper functions getEl = function ( input ) { var output, doc; if ( typeof window === 'undefined' ) { return; } doc = window.document; if ( !input ) { throw new Error( 'No container element specified' ); } // We already have a DOM node - no work to do if ( input.tagName ) { return input; } // Get node from string if ( typeof input === 'string' ) { // try ID first output = doc.getElementById( input ); // then as selector, if possible if ( !output && doc.querySelector ) { output = doc.querySelector( input ); } // did it work? if ( output.tagName ) { return output; } } // If we've been given a collection (jQuery, Zepto etc), extract the first item if ( input[0] && input[0].tagName ) { return input[0]; } throw new Error( 'Could not find container element' ); }; return Ractive; }()); (function () { 'use strict'; var modifiersCache = {}, keypathCache = {}; _internal = { // thanks, http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/ isArray: function ( obj ) { return Object.prototype.toString.call( obj ) === '[object Array]'; }, isObject: function ( obj ) { return ( Object.prototype.toString.call( obj ) === '[object Object]' ) && ( typeof obj !== 'function' ); }, // http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric isNumeric: function ( n ) { return !isNaN( parseFloat( n ) ) && isFinite( n ); }, splitKeypath: function ( keypath ) { var hasModifiers, modifiers, i, index, startIndex, keys, remaining, part; // We should only have to do all the heavy regex stuff once... caching FTW if ( keypathCache[ keypath ] ) { return keypathCache[ keypath ].concat(); } // If this string contains no escaped dots or modifiers, // we can just split on dots, after converting from array notation hasModifiers = /⭆.+⭅/.test( keypath ); if ( !( /\\\./.test( keypath ) ) && !hasModifiers ) { keypathCache[ keypath ] = keypath.replace( /\[\s*([0-9]+)\s*\]/g, '.$1' ).split( '.' ); return keypathCache[ keypath ].concat(); } keys = []; remaining = keypath; // first, blank modifiers in case they contain dots, but store them // so we can reinstate them later if ( hasModifiers ) { modifiers = []; remaining = remaining.replace( /⭆(.+)⭅/g, function ( match, $1 ) { modifiers[ modifiers.length ] = $1; return '⭆x⭅'; }); } startIndex = 0; // Split into keys while ( remaining.length ) { // Find next dot index = remaining.indexOf( '.', startIndex ); // Final part? if ( index === -1 ) { part = remaining; remaining = ''; } else { // If this dot is preceded by a backslash, which isn't // itself preceded by a backslash, we consider it escaped if ( remaining.charAt( index - 1) === '\\' && remaining.charAt( index - 2 ) !== '\\' ) { // we don't want to keep this part, we want to keep looking // for the separator startIndex = index + 1; continue; } // Otherwise, we have our next part part = remaining.substr( 0, index ); startIndex = 0; } if ( /\[/.test( part ) ) { keys = keys.concat( part.replace( /\[\s*([0-9]+)\s*\]/g, '.$1' ).split( '.' ) ); } else { keys[ keys.length ] = part; } remaining = remaining.substring( index + 1 ); } // Then, reinstate modifiers if ( hasModifiers ) { i = keys.length; while ( i-- ) { if ( keys[i] === '⭆x⭅' ) { keys[i] = '⭆' + modifiers.pop() + '⭅'; } } } keypathCache[ keypath ] = keys; return keys.concat(); }, getModifiersFromString: function ( str ) { var modifiers, raw; if ( modifiersCache[ str ] ) { return modifiersCache[ str ]; } raw = str.split( '⤋' ); modifiers = raw.map( function ( str ) { var index; index = str.indexOf( '[' ); if ( index === -1 ) { return { d: str, g: [] }; } return { d: str.substr( 0, index ), g: JSON.parse( str.substring( index ) ) }; }); modifiersCache[ str ] = modifiers; return modifiers; }, stringifyModifiers: function ( modifiers ) { var stringified = modifiers.map( function ( modifier ) { if ( modifier.g && modifier.g.length ) { return modifier.d + JSON.stringify( modifier.g ); } return modifier.d; }); return '⭆' + stringified.join( '⤋' ) + '⭅'; }, eventDefns: {} }; }()); _internal.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, ATTR_VALUE_TOKEN: 13 }; (function ( _internal ) { 'use strict'; _internal.Mustache = function ( options ) { this.root = options.root; this.descriptor = options.descriptor; this.parent = options.parent; this.parentFragment = options.parentFragment; this.contextStack = options.contextStack || []; this.index = options.index || 0; // DOM only if ( options.parentNode || options.anchor ) { this.parentNode = options.parentNode; this.anchor = options.anchor; } this.type = options.descriptor.t; this.root._registerMustache( this ); // if we have a failed keypath lookup, and this is an inverted section, // we need to trigger this.update() so the contents are rendered if ( !this.keypath && this.descriptor.n ) { // test both section-hood and inverticity in one go this.update( this.descriptor.m ? this.root._modify( false, this.descriptor.m ) : false ); } }; _internal.Fragment = function ( options ) { var numItems, i, itemOptions, parentRefs, ref; this.parent = options.parent; this.index = options.index; this.items = []; this.indexRefs = {}; if ( this.parent && this.parent.parentFragment ) { parentRefs = this.parent.parentFragment.indexRefs; for ( ref in parentRefs ) { if ( parentRefs.hasOwnProperty( ref ) ) { this.indexRefs[ ref ] = parentRefs[ ref ]; } } } if ( options.indexRef ) { this.indexRefs[ options.indexRef ] = options.index; } itemOptions = { root: options.root, parentFragment: this, parent: this, 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; // this.items[ this.items.length ] = createView( itemOptions ); this.items[ this.items.length ] = this.createItem( itemOptions ); } }; _internal.sectionUpdate = function ( value ) { var fragmentOptions, valueIsArray, emptyArray, i, itemsToRemove; fragmentOptions = { descriptor: this.descriptor.f, root: this.root, parentNode: this.parentNode, parent: this }; // TODO if DOM type, need to know anchor if ( this.parentNode ) { fragmentOptions.anchor = this.parentFragment.findNextNode( this ); } valueIsArray = _internal.isArray( value ); // treat empty arrays as false values if ( valueIsArray && value.length === 0 ) { emptyArray = true; } // if section is inverted, only check for truthiness/falsiness if ( this.descriptor.n ) { if ( value && !emptyArray ) { if ( this.length ) { this.unrender(); this.length = 0; } } else { if ( !this.length ) { // no change to context stack in this situation fragmentOptions.contextStack = this.contextStack; fragmentOptions.index = 0; this.fragments[0] = this.createFragment( fragmentOptions ); this.length = 1; return; } } return; } // otherwise we need to work out what sort of section we're dealing with // if value is an array, iterate through if ( valueIsArray ) { // if the array is shorter than it was previously, remove items if ( value.length < this.length ) { itemsToRemove = this.fragments.splice( value.length, this.length - value.length ); while ( itemsToRemove.length ) { itemsToRemove.pop().teardown(); } } // otherwise... else { if ( value.length > this.length ) { // add any new ones for ( i=this.length; i<value.length; i+=1 ) { // append list item to context stack fragmentOptions.contextStack = this.contextStack.concat( this.keypath + '.' + i ); fragmentOptions.index = i; if ( this.descriptor.i ) { fragmentOptions.indexRef = this.descriptor.i; } this.fragments[i] = this.createFragment( fragmentOptions ); } } } this.length = value.length; } // if value is a hash... else if ( _internal.isObject( value ) ) { // ...then if it isn't rendered, render it, adding this.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 ( !this.length ) { // append this section to the context stack fragmentOptions.contextStack = this.contextStack.concat( this.keypath ); fragmentOptions.index = 0; this.fragments[0] = this.createFragment( fragmentOptions ); this.length = 1; } } // otherwise render if value is truthy, unrender if falsy else { if ( value && !emptyArray ) { if ( !this.length ) { // no change to context stack fragmentOptions.contextStack = this.contextStack; fragmentOptions.index = 0; this.fragments[0] = this.createFragment( fragmentOptions ); this.length = 1; } } else { if ( this.length ) { this.unrender(); this.length = 0; } } } }; }( _internal )); (function ( proto ) { 'use strict'; 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 ); } }; }; 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.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 ); } }; }( Ractive.prototype )); (function ( Ractive, _internal ) { 'use strict'; Ractive.defineEvent = function ( eventName, definition ) { _internal.eventDefns[ eventName ] = definition; }; Ractive.defineEvent( '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, up, move, cancel; x = event.clientX; y = event.clientY; up = function ( event ) { fire( 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]; finger = touch.identifier; up = function ( event ) { if ( event.changedTouches.length !== 1 || event.touches[0].identifier !== finger ) { cancel(); } else { fire( 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 ( event ) { 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 ); } }; }); }( Ractive, _internal )); // Ractive.compile // =============== // // Takes in a string, and returns an object representing the compiled template. // A compiled template is an array of 1 or more 'descriptors', which in some // cases have children. // // The format is optimised for size, not readability, however for reference the // keys for each descriptor are as follows: // // * r - Reference, e.g. 'mustache' in {{mustache}} // * t - Type, as according to _internal.types (e.g. 1 is text, 2 is interpolator...) // * f - Fragment. Contains a descriptor's children // * e - Element name // * a - map of element Attributes // * n - indicates an iNverted section // * p - Priority. Higher priority items are updated before lower ones on model changes // * m - Modifiers // * d - moDifier name // * g - modifier arGuments // * i - Index reference, e.g. 'num' in {{#section:num}}content{{/section}} // * x - event proXies (i.e. when user e.g. clicks on a node, fire proxy event) var Ractive = Ractive || {}, _internal = _internal || {}; // in case we're not using the runtime (function ( R, _internal ) { 'use strict'; var FragmentStub, getFragmentStubFromTokens, TextStub, ElementStub, SectionStub, MustacheStub, decodeCharacterReferences, htmlEntities, getModifier, types, voidElementNames, allElementNames, closedByParentClose, implicitClosersByTagName, proxyPattern; R.compile = function ( template, options ) { var tokens, fragmentStub, json; options = options || {}; if ( options.sanitize === true ) { options.sanitize = { // blacklist from https://code.google.com/p/google-caja/source/browse/trunk/src/com/google/caja/lang/html/html4-elements-whitelist.json elements: 'applet base basefont body frame frameset head html isindex link meta noframes noscript object param script style title'.split( ' ' ), eventAttributes: true }; } // If delimiters are specified use them, otherwise reset to defaults R.delimiters = options.delimiters || [ '{{', '}}' ]; R.tripleDelimiters = options.tripleDelimiters || [ '{{{', '}}}' ]; tokens = _internal.tokenize( template ); fragmentStub = getFragmentStubFromTokens( tokens, 0, options, options.preserveWhitespace ); json = fragmentStub.toJson(); if ( typeof json === 'string' ) { return [ json ]; // signal that this shouldn't be recompiled } return json; }; types = _internal.types; voidElementNames = 'area base br col command embed hr img input keygen link meta param source track wbr'.split( ' ' ); allElementNames = 'a abbr acronym address applet area b base basefont bdo big blockquote body br button caption center cite code col colgroup dd del dfn dir div dl dt em fieldset font form frame frameset h1 h2 h3 h4 h5 h6 head hr html i iframe img input ins isindex kbd label legend li link map menu meta noframes noscript object ol optgroup option p param pre q s samp script select small span strike strong style sub sup table tbody td textarea tfoot th thead title tr tt u ul var article aside audio bdi canvas command data datagrid datalist details embed eventsource figcaption figure footer header hgroup keygen mark meter nav output progress ruby rp rt section source summary time track video wbr'.split( ' ' ); closedByParentClose = 'li dd rt rp optgroup option tbody tfoot tr td th'.split( ' ' ); implicitClosersByTagName = { li: [ 'li' ], dt: [ 'dt', 'dd' ], dd: [ 'dt', 'dd' ], p: 'address article aside blockquote dir div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr menu nav ol p pre section table ul'.split( ' ' ), rt: [ 'rt', 'rp' ], rp: [ 'rp', 'rt' ], optgroup: [ 'optgroup' ], option: [ 'option', 'optgroup' ], thead: [ 'tbody', 'tfoot' ], tbody: [ 'tbody', 'tfoot' ], tr: [ 'tr' ], td: [ 'td', 'th' ], th: [ 'td', 'th' ] }; getModifier = function ( str ) { var name, argsStr, args, openIndex; openIndex = str.indexOf( '[' ); if ( openIndex !== -1 ) { name = str.substr( 0, openIndex ); argsStr = str.substring( openIndex, str.length ); try { args = JSON.parse( argsStr ); } catch ( err ) { throw 'Could not parse arguments (' + argsStr + ') using JSON.parse'; } return { d: name, g: args }; } return { d: str }; }; htmlEntities = { quot: 34, amp: 38, apos: 39, lt: 60, gt: 62, nbsp: 160, iexcl: 161, cent: 162, pound: 163, curren: 164, yen: 165, brvbar: 166, sect: 167, uml: 168, copy: 169, ordf: 170, laquo: 171, not: 172, shy: 173, reg: 174, macr: 175, deg: 176, plusmn: 177, sup2: 178, sup3: 179, acute: 180, micro: 181, para: 182, middot: 183, cedil: 184, sup1: 185, ordm: 186, raquo: 187, frac14: 188, frac12: 189, frac34: 190, iquest: 191, Agrave: 192, Aacute: 193, Acirc: 194, Atilde: 195, Auml: 196, Aring: 197, AElig: 198, Ccedil: 199, Egrave: 200, Eacute: 201, Ecirc: 202, Euml: 203, Igrave: 204, Iacute: 205, Icirc: 206, Iuml: 207, ETH: 208, Ntilde: 209, Ograve: 210, Oacute: 211, Ocirc: 212, Otilde: 213, Ouml: 214, times: 215, Oslash: 216, Ugrave: 217, Uacute: 218, Ucirc: 219, Uuml: 220, Yacute: 221, THORN: 222, szlig: 223, agrave: 224, aacute: 225, acirc: 226, atilde: 227, auml: 228, aring: 229, aelig: 230, ccedil: 231, egrave: 232, eacute: 233, ecirc: 234, euml: 235, igrave: 236, iacute: 237, icirc: 238, iuml: 239, eth: 240, ntilde: 241, ograve: 242, oacute: 243, ocirc: 244, otilde: 245, ouml: 246, divide: 247, oslash: 248, ugrave: 249, uacute: 250, ucirc: 251, uuml: 252, yacute: 253, thorn: 254, yuml: 255, OElig: 338, oelig: 339, Scaron: 352, scaron: 353, Yuml: 376, fnof: 402, circ: 710, tilde: 732, Alpha: 913, Beta: 914, Gamma: 915, Delta: 916, Epsilon: 917, Zeta: 918, Eta: 919, Theta: 920, Iota: 921, Kappa: 922, Lambda: 923, Mu: 924, Nu: 925, Xi: 926, Omicron: 927, Pi: 928, Rho: 929, Sigma: 931, Tau: 932, Upsilon: 933, Phi: 934, Chi: 935, Psi: 936, Omega: 937, alpha: 945, beta: 946, gamma: 947, delta: 948, epsilon: 949, zeta: 950, eta: 951, theta: 952, iota: 953, kappa: 954, lambda: 955, mu: 956, nu: 957, xi: 958, omicron: 959, pi: 960, rho: 961, sigmaf: 962, sigma: 963, tau: 964, upsilon: 965, phi: 966, chi: 967, psi: 968, omega: 969, thetasym: 977, upsih: 978, piv: 982, ensp: 8194, emsp: 8195, thinsp: 8201, zwnj: 8204, zwj: 8205, lrm: 8206, rlm: 8207, ndash: 8211, mdash: 8212, lsquo: 8216, rsquo: 8217, sbquo: 8218, ldquo: 8220, rdquo: 8221, bdquo: 8222, dagger: 8224, Dagger: 8225, bull: 8226, hellip: 8230, permil: 8240, prime: 8242, Prime: 8243, lsaquo: 8249, rsaquo: 8250, oline: 8254, frasl: 8260, euro: 8364, image: 8465, weierp: 8472, real: 8476, trade: 8482, alefsym: 8501, larr: 8592, uarr: 8593, rarr: 8594, darr: 8595, harr: 8596, crarr: 8629, lArr: 8656, uArr: 8657, rArr: 8658, dArr: 8659, hArr: 8660, forall: 8704, part: 8706, exist: 8707, empty: 8709, nabla: 8711, isin: 8712, notin: 8713, ni: 8715, prod: 8719, sum: 8721, minus: 8722, lowast: 8727, radic: 8730, prop: 8733, infin: 8734, ang: 8736, and: 8743, or: 8744, cap: 8745, cup: 8746, 'int': 8747, there4: 8756, sim: 8764, cong: 8773, asymp: 8776, ne: 8800, equiv: 8801, le: 8804, ge: 8805, sub: 8834, sup: 8835, nsub: 8836, sube: 8838, supe: 8839, oplus: 8853, otimes: 8855, perp: 8869, sdot: 8901, lceil: 8968, rceil: 8969, lfloor: 8970, rfloor: 8971, lang: 9001, rang: 9002, loz: 9674, spades: 9824, clubs: 9827, hearts: 9829, diams: 9830 }; decodeCharacterReferences = function ( html ) { var result; // named entities result = html.replace( /&([a-zA-Z]+);/, function ( match, name ) { if ( htmlEntities[ name ] ) { return String.fromCharCode( htmlEntities[ name ] ); } return match; }); // hex references result = result.replace( /&#x([0-9]+);/, function ( match, hex ) { return String.fromCharCode( parseInt( hex, 16 ) ); }); // decimal references result = result.replace( /&#([0-9]+);/, function ( match, num ) { return String.fromCharCode( num ); }); return result; }; TextStub = function ( token ) { this.type = types.TEXT; this.text = token.value; }; TextStub.prototype = { toJson: function () { // this will be used as text, so we need to decode things like &amp; return this.decoded || ( this.decoded = decodeCharacterReferences( this.text) ); }, toString: function () { // this will be used as straight text return this.text; }, decodeCharacterReferences: function () { } }; proxyPattern = /^proxy-([a-z]+)$/; ElementStub = function ( token, parentFragment ) { var items, item, name, attributes, numAttributes, i, attribute, proxies, proxy, preserveWhitespace, match; this.type = types.ELEMENT; this.tag = token.tag; this.parentFragment = parentFragment; this.parentElement = parentFragment.parentElement; items = token.attributes.items; i = items.length; if ( i ) { attributes = []; proxies = []; while ( i-- ) { item = items[i]; name = item.name.value; // sanitize if ( parentFragment.options.sanitize && parentFragment.options.sanitize.eventAttributes ) { if ( name.toLowerCase().substr( 0, 2 ) === 'on' ) { continue; } } // event proxy? if ( match = proxyPattern.exec( name ) ) { proxy = { name: match[1], value: getFragmentStubFromTokens( item.value.tokens, this.parentFragment.priority + 1 ) }; proxies[ proxies.length ] = proxy; } else { attribute = { name: name }; if ( !item.value.isNull ) { attribute.value = getFragmentStubFromTokens( item.value.tokens, this.parentFragment.priority + 1 ); } attributes[ attributes.length ] = attribute; } } if ( attributes.length ) { this.attributes = attributes; } if ( proxies.length ) { this.proxies = proxies; } } // if this is a void element, or a self-closing tag, seal the element if ( token.isSelfClosingTag || voidElementNames.indexOf( token.tag.toLowerCase() ) !== -1 ) { return; } // preserve whitespace if parent fragment has preserveWhitespace flag, or // if this is a <pre> element preserveWhitespace = parentFragment.preserveWhitespace || this.tag.toLowerCase() === 'pre'; this.fragment = new FragmentStub( this, parentFragment.priority + 1, parentFragment.options, preserveWhitespace ); }; ElementStub.prototype = { read: function ( token ) { return this.fragment && this.fragment.read( token ); }, toJson: function ( noStringify ) { var json, attrName, attrValue, str, proxy, i; json = { t: types.ELEMENT, e: this.tag }; if ( this.attributes ) { json.a = {}; i = this.attributes.length; while ( i-- ) { attrName = this.attributes[i].name; // empty attributes (e.g. autoplay, checked) if( this.attributes[i].value === undefined ) { attrValue = null; } else { // can we stringify the value? str = this.attributes[i].value.toString(); if ( str !== false ) { // need to explicitly check, as '' === false attrValue = str; } else { attrValue = this.attributes[i].value.toJson(); } } json.a[ attrName ] = attrValue; } } if ( this.fragment && this.fragment.items.length ) { json.f = this.fragment.toJson( noStringify ); } if ( this.proxies ) { json.x = {}; i = this.proxies.length; while ( i-- ) { proxy = this.proxies[i]; // can we stringify the value? if ( str = proxy.value.toString() ) { json.x[ proxy.name ] = str; } else { json.x[ proxy.name ] = proxy.value.toJson(); } } } return json; }, toString: function () { var str, i, len, attrStr, attrValueStr, fragStr, isVoid; // if this isn't an HTML element, it can't be stringified (since the only reason to stringify an // element is to use with innerHTML, and SVG doesn't support that method if ( allElementNames.indexOf( this.tag.toLowerCase() ) === -1 ) { return false; } // see if children can be stringified (i.e. don't contain mustaches) fragStr = ( this.fragment ? this.fragment.toString() : '' ); if ( fragStr === false ) { return false; } // do we have proxies? if so we can't use innerHTML if ( this.proxies ) { return false; } // is this a void element? isVoid = ( voidElementNames.indexOf( this.tag.toLowerCase() ) !== -1 ); str = '<' + this.tag; if ( this.attributes ) { for ( i=0, len=this.attributes.length; i<len; i+=1 ) { // does this look like a namespaced attribute? if so we can't stringify it if ( this.attributes[i].name.indexOf( ':' ) !== -1 ) { return false; } attrStr = ' ' + this.attributes[i].name; // empty attributes if ( this.attributes[i].value !== undefined ) { attrValueStr = this.attributes[i].value.toString(); if ( attrValueStr === false ) { return false; } if ( attrValueStr !== '' ) { attrStr += '='; // does it need to be quoted? if ( /[\s"'=<>`]/.test( attrValueStr ) ) { attrStr += '"' + attrValueStr.replace( /"/g, '&quot;' ) + '"'; } else { attrStr += attrValueStr; } } } str += attrStr; } } // if this isn't a void tag, but is self-closing, add a solidus. Aaaaand, we're done if ( this.isSelfClosing && !isVoid ) { str += '/>'; return str; } str += '>'; // void element? we're done if ( isVoid ) { return str; } // if this has children, add them str += fragStr; str += '</' + this.tag + '>'; return str; } }; SectionStub = function ( token, parentFragment ) { this.type = types.SECTION; this.parentFragment = parentFragment; this.ref = token.ref; this.inverted = ( token.type === types.INVERTED ); this.modifiers = token.modifiers; this.i = token.i; this.fragment = new FragmentStub( this, parentFragment.priority + 1, parentFragment.options, parentFragment.preserveWhitespace ); }; SectionStub.prototype = { read: function ( token ) { return this.fragment.read( token ); }, toJson: function ( noStringify ) { var json; json = { t: types.SECTION, r: this.ref }; if ( this.fragment ) { json.f = this.fragment.toJson( noStringify ); } if ( this.modifiers && this.modifiers.length ) { json.m = this.modifiers.map( getModifier ); } if ( this.inverted ) { json.n = true; } if ( this.priority ) { json.p = this.parentFragment.priority; } if ( this.i ) { json.i = this.i; } return json; }, toString: function () { // sections cannot be stringified return false; } }; MustacheStub = function ( token, priority ) { this.type = token.type; this.priority = priority; this.ref = token.ref; this.modifiers = token.modifiers; }; MustacheStub.prototype = { toJson: function () { var json = { t: this.type, r: this.ref }; if ( this.modifiers ) { json.m = this.modifiers.map( getModifier ); } if ( this.priority ) { json.p = this.priority; } return json; }, toString: function () { // mustaches cannot be stringified return false; } }; FragmentStub = function ( owner, priority, options, preserveWhitespace ) { this.owner = owner; this.items = []; this.options = options; this.preserveWhitespace = preserveWhitespace; if ( owner ) { this.parentElement = ( owner.type === types.ELEMENT ? owner : owner.parentElement ); } this.priority = priority; }; FragmentStub.prototype = { read: function ( token ) { if ( this.sealed ) { return false; } // does this token implicitly close this fragment? (e.g. an <li> without a </li> being closed by another <li>) if ( this.isImplicitlyClosedBy( token ) ) { this.seal(); return false; } // do we have an open child section/element? if ( this.currentChild ) { // can it use this token? if ( this.currentChild.read( token ) ) { return true; } // if not, we no longer have an open child this.currentChild = null; } // does this token explicitly close this fragment? if ( this.isExplicitlyClosedBy( token ) ) { this.seal(); return true; } // time to create a new child... // (...unless this is a section closer or a delimiter change or a comment) if ( token.type === types.CLOSING || token.type === types.DELIMCHANGE || token.type === types.COMMENT ) { return false; } // section? if ( token.type === types.SECTION || token.type === types.INVERTED ) { this.currentChild = new SectionStub( token, this ); this.items[ this.items.length ] = this.currentChild; return true; } // element? if ( token.type === types.TAG ) { this.currentChild = new ElementStub( token, this ); // sanitize if ( this.options.sanitize && this.options.sanitize.elements && this.options.sanitize.elements.indexOf( token.tag.toLowerCase() ) !== -1 ) { return true; } this.items[ this.items.length ] = this.currentChild; return true; } // text or attribute value? if ( token.type === types.TEXT || token.type === types.ATTR_VALUE_TOKEN ) { this.items[ this.items.length ] = new TextStub( token ); return true; } // none of the above? must be a mustache this.items[ this.items.length ] = new MustacheStub( token, this.priority ); return true; }, isClosedBy: function ( token ) { return this.isImplicitlyClosedBy( token ) || this.isExplicitlyClosedBy( token ); }, isImplicitlyClosedBy: function ( token ) { var implicitClosers, element, parentElement, thisTag, tokenTag; if ( !token.tag || !this.owner || ( this.owner.type !== types.ELEMENT ) ) { return false; } thisTag = this.owner.tag.toLowerCase(); tokenTag = token.tag.toLowerCase(); element = this.owner; parentElement = element.parentElement || null; // if this is an element whose end tag can be omitted if followed by an element // which is an 'implicit closer', return true implicitClosers = implicitClosersByTagName[ thisTag ]; if ( implicitClosers ) { if ( !token.isClosingTag && implicitClosers.indexOf( tokenTag ) !== -1 ) { return true; } } // if this is an element that is closed when its parent closes, return true if ( closedByParentClose.indexOf( thisTag ) !== -1 ) { if ( parentElement && parentElement.fragment.isClosedBy( token ) ) { return true; } } // special cases // p element end tag can be omitted when parent closes if it is not an a element if ( thisTag === 'p' ) { if ( parentElement && parentElement.tag.toLowerCase() === 'a' && parentElement.fragment.isClosedBy( token ) ) { return true; } } }, isExplicitlyClosedBy: function ( token ) { if ( !this.owner ) { return false; } if ( this.owner.type === types.SECTION ) { if ( token.type === types.CLOSING && token.ref === this.owner.ref ) { return true; } } if ( this.owner.type === types.ELEMENT && this.owner ) { if ( token.isClosingTag && ( token.tag.toLowerCase() === this.owner.tag.toLowerCase() ) ) { return true; } } }, toJson: function ( noStringify ) { var result = [], i, len, str; // can we stringify this? if ( !noStringify ) { str = this.toString(); if ( str !== false ) { return str; } } for ( i=0, len=this.items.length; i<len; i+=1 ) { result[i] = this.items[i].toJson( noStringify ); } return result; }, toString: function () { var str = '', i, len, itemStr; for ( i=0, len=this.items.length; i<len; i+=1 ) { itemStr = this.items[i].toString(); // if one of the child items cannot be stringified (i.e. contains a mustache) return false if ( itemStr === false ) { return false; } str += itemStr; } return str; }, seal: function () { var first, last, i, item; this.sealed = true; // if this is an element fragment, remove leading and trailing whitespace if ( !this.preserveWhitespace ) { if ( this.owner.type === types.ELEMENT ) { first = this.items[0]; if ( first && first.type === types.TEXT ) { first.text = first.text.replace