ractive
Version:
Next-generation DOM manipulation
1,911 lines (1,442 loc) • 102 kB
JavaScript
(function () {
'use strict';
// Shims for older browsers
if ( !Date.now ) {
Date.now = function () { return +new Date(); };
}
if ( !document.createElementNS ) {
document.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 document.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) {
for (var 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 ( i in this && 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 ( i in this ) {
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 ( i in this ) {
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 ( i in this && filter.call( context, this[i], i, this ) ) {
filtered[ filtered.length ] = this[i];
}
}
return filtered;
};
}
}());
/*! Ractive - v0.2.0 - 2013-04-18
* 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;
(function () {
'use strict';
var getEl;
Ractive = function ( options ) {
var defaults, key;
// Options
// -------
if ( options ) {
for ( key in options ) {
if ( options.hasOwnProperty( key ) ) {
this[ key ] = options[ key ];
}
}
}
defaults = {
preserveWhitespace: false,
append: false,
twoway: true,
formatters: {},
modifyArrays: true
};
for ( key in defaults ) {
if ( defaults.hasOwnProperty( key ) && this[ key ] === undefined ) {
this[ key ] = defaults[ key ];
}
}
// 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.deferredAttributes = [];
// Initialise (or update) viewmodel with data
if ( this.data ) {
this.set( this.data );
}
// If we were given uncompiled partials, compile them
if ( this.partials ) {
for ( key in this.partials ) {
if ( this.partials.hasOwnProperty( key ) ) {
if ( typeof this.partials[ key ] === 'string' ) {
if ( !Ractive.compile ) {
throw new Error( 'Missing Ractive.compile - cannot compile partial "' + key + '". Either precompile or use the version that includes the compiler' );
}
this.partials[ key ] = Ractive.compile( this.partials[ key ], this ); // all compiler options are present on `this`, so just passing `this`
}
}
}
}
// 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, callback: this.callback, 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 _private.DomFragment({
model: 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 () {
this.rendered.teardown();
},
set: function ( keypath, value ) {
if ( _private.isObject( keypath ) ) {
this._setMultiple( keypath );
} else {
this._setSingle( keypath, value );
}
while ( this.deferredAttributes.length ) {
this.deferredAttributes.pop().update().updateDeferred = false;
}
},
_setSingle: function ( keypath, value ) {
var keys, key, obj, normalised, i, resolved, unresolved;
if ( _private.isArray( keypath ) ) {
keys = keypath.slice();
} else {
keys = _private.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 /^[0-9]+$/, assume we want an array branch rather
// than an object
if ( !obj[ key ] ) {
obj[ key ] = ( /^[0-9]+$/.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 views that observe `keypaths` or its descendants
this._notifyObservers( normalised, value );
// 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];
resolved = this.resolveRef( unresolved.view.model.ref, unresolved.view.contextStack );
// If we were able to find a keypath, initialise the view
if ( resolved ) {
unresolved.callback( resolved.keypath, resolved.value );
}
// Otherwise add to the back of the queue (this is why we're working backwards)
else {
this.registerUnresolvedKeypath( unresolved );
}
}
},
_setMultiple: function ( map ) {
var keypath;
for ( keypath in map ) {
if ( map.hasOwnProperty( keypath ) ) {
this._setSingle( keypath, map[ keypath ] );
}
}
},
_clearCache: function ( keypath ) {
var children = this._cacheMap[ keypath ];
delete this._cache[ keypath ];
if ( !children ) {
return;
}
while ( children.length ) {
this._clearCache( children.pop() );
}
},
get: function ( keypath ) {
var keys, normalised, lastDotIndex, formula, match, parentKeypath, parentValue, propertyName, unformatted, unformattedKeypath, value, formatters;
if ( _private.isArray( keypath ) ) {
keys = keypath.slice(); // clone
normalised = keys.join( '.' );
}
else {
// cache hit? great
if ( keypath in this._cache ) {
return this._cache[ keypath ];
}
keys = _private.splitKeypath( keypath );
normalised = keys.join( '.' );
}
// we may have a cache hit now that it's been normalised
if ( normalised in this._cache ) {
return this._cache[ normalised ];
}
// otherwise it looks like we need to do some work
if ( keys.length > 1 ) {
formula = keys.pop();
parentValue = this.get( keys );
} else {
formula = keys.pop();
parentValue = this.data;
}
// is this a set of formatters?
if ( match = /^⭆(.+)⭅$/.exec( formula ) ) {
formatters = _private.getFormattersFromString( match[1] );
value = this._format( parentValue, formatters );
}
else {
if ( typeof parentValue !== 'object' ) {
return;
}
value = parentValue[ formula ];
}
// update cacheMap
if ( keys.length ) {
parentKeypath = keys.join( '.' );
if ( !this._cacheMap[ parentKeypath ] ) {
this._cacheMap[ parentKeypath ] = [];
}
this._cacheMap[ parentKeypath ].push( normalised );
}
// allow functions as values
// TODO allow arguments, same as formatters?
if ( typeof value === 'function' ) {
value = value();
}
// update cache
this._cache[ normalised ] = value;
return value;
},
update: function () {
// TODO
throw new Error( 'not implemented yet!' );
return this;
},
link: function ( keypath ) {
var self = this;
return function ( value ) {
self.set( keypath, value );
};
},
registerView: function ( view ) {
var self = this, resolved, initialUpdate, value, index;
if ( view.parentFragment && ( view.parentFragment.indexRefs.hasOwnProperty( view.model.ref ) ) ) {
// this isn't a real keypath, it's an index reference
index = view.parentFragment.indexRefs[ view.model.ref ];
value = ( view.model.fmtrs ? this._format( index, view.model.fmtrs ) : index );
view.update( value );
return; // this value will never change, and doesn't have a keypath
}
initialUpdate = function ( keypath, value ) {
if ( view.model.fmtrs ) {
view.keypath = keypath + '.' + _private.stringifyFormatters( view.model.fmtrs );
} else {
view.keypath = keypath;
}
// create observers
view.observerRefs = self.observe( view.model.p || 0, view );
view.update( self.get( view.keypath ) );
};
resolved = this.resolveRef( view.model.ref, view.contextStack );
if ( !resolved ) {
// we may still need to do an update, if the view has formatters
// that e.g. offer an alternative to undefined
if ( view.model.fmtrs ) {
view.update( this._format( undefined, view.model.fmtrs ) );
}
this.registerUnresolvedKeypath({
view: view,
callback: initialUpdate
});
} else {
initialUpdate( resolved.keypath, resolved.value );
}
},
// 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 ( ref, contextStack ) {
var innerMost, keypath, value;
// Implicit iterators - i.e. {{.}} - are a special case
if ( ref === '.' ) {
keypath = contextStack[ contextStack.length - 1 ];
value = this.get( keypath );
return { keypath: keypath, value: value };
}
// 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 ) {
innerMost = contextStack.pop();
keypath = innerMost + '.' + ref;
value = this.get( keypath );
if ( value !== undefined ) {
return { keypath: keypath, value: value };
}
}
value = this.get( ref );
if ( value !== undefined ) {
return { keypath: ref, value: value };
}
},
registerUnresolvedKeypath: function ( unresolved ) {
this.pendingResolution[ this.pendingResolution.length ] = unresolved;
},
// Internal method to format a value, using formatters passed in at initialization
_format: function ( value, formatters ) {
var i, numFormatters, formatter, name, args, fn;
// If there are no formatters, groovy - just return the value unchanged
if ( !formatters ) {
return value;
}
// Otherwise go through each in turn, applying sequentially
numFormatters = formatters.length;
for ( i=0; i<numFormatters; i+=1 ) {
formatter = formatters[i];
name = formatter.name;
args = formatter.args || [];
// If a formatter was passed in, use it, otherwise see if there's a default
// one with this name
fn = this.formatters[ name ] || Ractive.formatters[ name ];
if ( fn ) {
value = fn.apply( this, [ value ].concat( args ) );
}
}
return value;
},
_notifyObservers: function ( keypath, value ) {
var self = this, observersGroupedByPriority = this.observers[ keypath ] || [], i, j, priorityGroup, observer, actualValue;
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.keypath ) );
}
}
}
},
observe: function ( priority, view ) {
var self = this, keypath, originalKeypath = view.keypath, observerRefs = [], observe, keys;
if ( !originalKeypath ) {
return undefined;
}
observe = function ( keypath ) {
var observers, observer;
observers = self.observers[ keypath ] = self.observers[ keypath ] || [];
observers = observers[ priority ] = observers[ priority ] || [];
observers[ observers.length ] = view;
observerRefs[ observerRefs.length ] = {
keypath: keypath,
priority: priority,
view: view
};
};
keys = _private.splitKeypath( view.keypath );
while ( keys.length > 1 ) {
observe( keys.join( '.' ) );
// remove the last item in the keypath, so that data.set( 'parent', { child: 'newValue' } ) affects views dependent on parent.child
keys.pop();
}
observe( keys[0] );
return observerRefs;
},
unobserve: function ( observerRef ) {
var priorities, observers, index, i, len;
priorities = this.observers[ observerRef.keypath ];
if ( !priorities ) {
// nothing to unobserve
return;
}
observers = priorities[ 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.view ) {
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 priorities[ observerRef.priority ];
}
if ( priorities.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;
}());
var _private;
(function () {
'use strict';
var formattersCache = {};
_private = {
// 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]';
},
// TODO what about non-POJOs?
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 );
},
// TODO this is a bit regex-heavy... could be optimised maybe?
splitKeypath: function ( keypath ) {
var result, hasEscapedDots, hasFormatters, formatters, split, i, replacer, index, startIndex, key, keys, remaining, blanked, part;
// if this string contains no escaped dots or formatters,
// we can just split on dots, after converting from array notation
if ( !( hasEscapedDots = /\\\./.test( keypath ) ) && !( hasFormatters = /⭆.+⭅/.test( keypath ) ) ) {
return keypath.replace( /\[\s*([0-9]+)\s*\]/g, '.$1' ).split( '.' );
}
keys = [];
remaining = keypath;
// first, blank formatters in case they contain dots, but store them
// so we can reinstate them later
if ( hasFormatters ) {
formatters = [];
remaining = remaining.replace( /⭆(.+)⭅/g, function ( match, $1 ) {
var blanked, i;
formatters[ formatters.length ] = $1;
return '⭆x⭅';
});
}
startIndex = 0;
// split into keys
while ( remaining.length ) {
// find next dot
index = remaining.indexOf( '.', startIndex );
// final part?
if ( index === -1 ) {
// TODO tidy up!
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 formatters
if ( hasFormatters ) {
replacer = function ( match ) {
return '⭆' + formatters.pop() + '⭅';
};
i = keys.length;
while ( i-- ) {
if ( keys[i] === '⭆x⭅' ) {
keys[i] = '⭆' + formatters.pop() + '⭅';
}
}
}
return keys;
},
getFormattersFromString: function ( str ) {
var formatters, raw, remaining;
if ( formattersCache[ str ] ) {
return formattersCache[ str ];
}
raw = str.split( '⤋' );
formatters = raw.map( function ( str ) {
var index;
index = str.indexOf( '[' );
if ( index === -1 ) {
return {
name: str,
args: []
};
}
return {
name: str.substr( 0, index ),
args: JSON.parse( str.substring( index ) )
};
});
formattersCache[ str ] = formatters;
return formatters;
},
stringifyFormatters: function ( formatters ) {
var stringified = formatters.map( function ( formatter ) {
if ( formatter.args && formatter.args.length ) {
return formatter.name + JSON.stringify( formatter.args );
}
return formatter.name;
});
return '⭆' + stringified.join( '⤋' ) + '⭅';
}
};
}());
_private.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 ( _private ) {
'use strict';
_private._Mustache = function ( options ) {
this.root = options.root;
this.model = options.model;
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.model.type;
this.root.registerView( 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.model.inv ) { // test both section-hood and inverticity in one go
this.update( false );
}
};
_private._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.model ? options.model.length : 0 );
for ( i=0; i<numItems; i+=1 ) {
itemOptions.model = options.model[i];
itemOptions.index = i;
// this.items[ this.items.length ] = createView( itemOptions );
this.items[ this.items.length ] = this.createItem( itemOptions );
}
};
_private._sectionUpdate = function ( value ) {
var fragmentOptions, valueIsArray, emptyArray, i, itemsToRemove;
fragmentOptions = {
model: this.model.frag,
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 = _private.isArray( value );
// modify the array to allow updates via push, pop etc
if ( valueIsArray && this.root.modifyArrays ) {
_private.modifyArray( value, this.keypath, this.root );
}
// treat empty arrays as false values
if ( valueIsArray && value.length === 0 ) {
emptyArray = true;
}
// if section is inverted, only check for truthiness/falsiness
if ( this.model.inv ) {
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;
}
}
if ( this.postUpdate ) {
this.postUpdate();
}
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.model.i ) {
fragmentOptions.indexRef = this.model.i;
}
this.fragments[i] = this.createFragment( fragmentOptions );
}
}
}
this.length = value.length;
}
// if value is a hash...
else if ( _private.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;
}
}
}
if ( this.postUpdate ) {
this.postUpdate();
}
};
}( _private ));
(function ( proto ) {
'use strict';
proto.on = function ( eventName, callback ) {
var self = this;
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 ));
var Ractive = Ractive || {}, _private = _private || {}; // in case we're not using the runtime
(function ( R, _private ) {
'use strict';
var FragmentStub,
getFragmentStubFromTokens,
TextStub,
ElementStub,
SectionStub,
MustacheStub,
decodeCharacterReferences,
htmlEntities,
getFormatter,
types,
voidElementNames,
allElementNames,
closedByParentClose,
implicitClosersByTagName;
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' ],
eventAttributes: true
};
}
// If delimiters are specified use them, otherwise reset to defaults
R.delimiters = options.delimiters || [ '{{', '}}' ];
R.tripleDelimiters = options.tripleDelimiters || [ '{{{', '}}}' ];
tokens = _private.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 = _private.types;
voidElementNames = [ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ];
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' ];
closedByParentClose = [ 'li', 'dd', 'rt', 'rp', 'optgroup', 'option', 'tbody', 'tfoot', 'tr', 'td', 'th' ];
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' ],
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' ]
};
getFormatter = 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 {
name: name,
args: args
};
}
return {
name: 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 &
return this.decoded || ( this.decoded = decodeCharacterReferences( this.text) );
},
toString: function () {
// this will be used as straight text
return this.text;
},
decodeCharacterReferences: function () {
}
};
ElementStub = function ( token, parentFragment ) {
var items, attributes, numAttributes, i, attribute, preserveWhitespace;
this.type = types.ELEMENT;
this.tag = token.tag;
this.parentFragment = parentFragment;
this.parentElement = parentFragment.parentElement;
items = token.attributes.items;
numAttributes = items.length;
if ( numAttributes ) {
attributes = [];
for ( i=0; i<numAttributes; i+=1 ) {
// sanitize
if ( parentFragment.options.sanitize && parentFragment.options.sanitize.eventAttributes ) {
if ( items[i].name.value.toLowerCase().substr( 0, 2 ) === 'on' ) {
continue;
}
}
attribute = {
name: items[i].name.value
};
if ( !items[i].value.isNull ) {
attribute.value = getFragmentStubFromTokens( items[i].value.tokens, this.parentFragment.priority + 1 );
}
attributes[i] = attribute;
}
this.attributes = attributes;
}
// 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, i;
json = {
type: types.ELEMENT,
tag: this.tag
};
if ( this.attributes ) {
json.attrs = {};
for ( i=0; i<this.attributes.length; i+=1 ) {
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.attrs[ attrName ] = attrValue;
}
}
if ( this.fragment && this.fragment.items.length ) {
json.frag = this.fragment.toJson( noStringify );
}
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;
}
// 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, '"' ) + '"';
} 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.formatters = token.formatters;
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 = {
type: types.SECTION,
ref: this.ref
};
if ( this.fragment ) {
json.frag = this.fragment.toJson( noStringify );
}
if ( this.formatters && this.formatters.length ) {
json.fmtrs = this.formatters.map( getFormatter );
}
if ( this.inverted ) {
json.inv = 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.formatters = token.formatters;
};
MustacheStub.prototype = {
toJson: function () {
var json = {
type: this.type,
ref: this.ref
};
if ( this.formatters ) {
json.fmtrs = this.formatters.map( getFormatter );
}
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( /^\s*/, '' );
if ( first.text === '' ) {
this.items.shift();
}
}
last = this.items[ this.items.length - 1 ];
if ( last && last.type === types.TEXT ) {
last.text = last.text.replace( /\s*$/, '' );
if ( last.text === '' ) {
this.items.pop();
}
}
}
// collapse multiple whitespace characters
i = this.items.length;
while ( i-- ) {
item = this.items[i];
if ( item.type === types.TEXT ) {
item.text = item.text.replace( /\s{2,}/g, ' ' );
}
}
}
if ( !this.items.length ) {
delete this.owner.fragment;
}
}
};
getFragmentStubFromTokens = function ( tokens, priority, options, preserveWhitespace ) {
var fragStub = new FragmentStub( null, priority, options, preserveWhitespace ), token;
while ( tokens.length ) {
token = tokens.shift();
fragStub.read( token );
}
return fragStub;
};
}( Ractive, _private ));
(function ( R, _private ) {
'use strict';
var types,
whitespace,
stripHtmlComments,
stripStandalones,
stripCommentTokens,
TokenStream,
MustacheBuffer,
TextToken,
MustacheToken,
TripleToken,
TagToken,
AttributeValueToken,
mustacheTypes,
OpeningBracket,
TagName,
AttributeCollection,
Solidus,
ClosingBracket,
Attribute,
AttributeName,
AttributeValue;
_private.tokenize = function ( template ) {
var stream = TokenStream.fromString( stripHtmlComments( template ) );
return stripCommentTokens( stripStandalones( stream.tokens ) );
};
// TokenStream generates an array of tokens from an HTML string
TokenStream = function () {
this.tokens = [];
this.buffer = new MustacheBuffer();
};
TokenStream.prototype = {
read: function ( char ) {
var mustacheToken, bufferValue;
// if we're building a tag or mustache, send everything to it including delimiter characters
if ( this.currentToken && this.currentToken.type !== types.TEXT ) {
if ( this.currentToken.read( char ) ) {
return true;
}
}
// either we're not building a tag, or the character was rejected
// send to buffer. if accepted, we don't need to do anything else
if ( this.buffer.read( char ) ) {
return true;
}
// can we convert the buffer to a mustache or triple?
mustacheToken = this.buffer.convert();
if ( mustacheToken ) {
// if we were building a token, seal it
if ( this.currentTo