ractive
Version:
Next-generation DOM manipulation
2,144 lines (1,629 loc) • 51.7 kB
JavaScript
/*! ractive - v0.1.7 - 2013-03-23
* 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 Ractive, 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 = {};
if ( this.viewmodel === undefined ) {
this.viewmodel = new Ractive.ViewModel();
}
// bind viewmodel to this ractive instance
this.viewmodel.dependents.push( this );
// Initialise (or update) viewmodel with data
if ( this.data ) {
this.viewmodel.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 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 Ractive.DomFragment({
model: this.template,
root: this,
parentNode: el
});
},
// Teardown. This goes through the root fragment and all its children, removing observers
// and generally cleaning up after itself
teardown: function () {
this.rendered.teardown();
},
// Proxies for viewmodel `set`, `get`, `update`, `observe` and `unobserve` methods
set: function () {
this.viewmodel.set.apply( this.viewmodel, arguments );
return this;
},
get: function () {
return this.viewmodel.get.apply( this.viewmodel, arguments );
},
update: function () {
this.viewmodel.update.apply( this.viewmodel, arguments );
return this;
},
// 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;
}
};
// 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 ( A ) {
'use strict';
A.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
};
}( Ractive ));
(function ( A ) {
'use strict';
var isArray, isObject;
// 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' );
};
A._Mustache = function ( options ) {
this.root = options.root;
this.model = options.model;
this.viewmodel = options.root.viewmodel;
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;
if ( this.initialize ) {
this.initialize();
}
this.viewmodel.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 );
}
};
A._Fragment = function ( options ) {
var numItems, i, itemOptions, parentRefs, ref;
if ( this.preInit ) {
if ( this.preInit( options ) ) {
return;
}
}
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 );
}
};
A._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 = isArray( value );
// modify the array to allow updates via push, pop etc
if ( valueIsArray && this.root.modifyArrays ) {
A.modifyArray( value, this.keypath, this.root.viewmodel );
}
// 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.views.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 ( 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();
}
};
}( Ractive ));
(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 ));
// Default formatters
(function ( A ) {
'use strict';
A.formatters = {
equals: function ( a, b ) {
return a === b;
},
greaterThan: function ( a, b ) {
return a > b;
},
greaterThanEquals: function ( a, b ) {
return a >= b;
},
lessThan: function ( a, b ) {
return a < b;
},
lessThanEquals: function ( a, b ) {
return a <= b;
}
};
}( Ractive ));
(function ( A ) {
'use strict';
var splitKeypath, keypathNormaliser;
// ViewModel constructor
A.ViewModel = function ( data ) {
// Initialise with supplied data, or create an empty object
this.data = data || {};
// Create empty array for keypaths that can't be resolved initially
this.pendingResolution = [];
// Create empty object for observers
this.observers = {};
// Dependent Ractive instances
this.dependents = [];
};
A.ViewModel.prototype = {
// Update the `value` of `keypath`, and notify the observers of
// `keypath` and its descendants
set: function ( keypath, value ) {
var k, keys, key, obj, i, unresolved, resolved, normalisedKeypath;
// Allow multiple values to be set in one go
if ( typeof keypath === 'object' ) {
for ( k in keypath ) {
if ( keypath.hasOwnProperty( k ) ) {
this.set( k, keypath[k] );
}
}
return;
}
// fire events
this.dependents.forEach( function ( dep ) {
if ( dep.setting ) {
return; // short-circuit any potential infinite loops
}
dep.setting = true;
dep.fire( 'set', keypath, value );
dep.fire( 'set:' + keypath, value );
dep.setting = false;
});
// Split key path into keys (e.g. `'foo.bar[0]'` -> `['foo','bar',0]`)
keys = splitKeypath( keypath );
normalisedKeypath = keys.join( '.' );
// TODO accommodate implicit array generation
obj = this.data;
while ( keys.length > 1 ) {
key = keys.shift();
obj = obj[ key ] || {};
}
key = keys[0];
obj[ key ] = value;
// Trigger updates of views that observe `keypaths` or its descendants
this._notifyObservers( normalisedKeypath, 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 );
}
}
},
// Get the current value of `keypath`
get: function ( keypath ) {
var keys, result;
if ( !keypath ) {
return undefined;
}
keys = splitKeypath( keypath );
result = this.data;
while ( keys.length ) {
if ( result ) {
result = result[ keys.shift() ];
}
if ( result === undefined ) {
return result;
}
}
if ( typeof result === 'function' ) {
return result.call( this, keypath );
}
return result;
},
// Force notify observers of `keypath` (useful if e.g. an array or object member
// was changed without calling `ractive.set()`)
update: function ( keypath ) {
var kp;
if ( keypath ) {
this._notifyObservers( keypath, this.get( keypath ) );
}
// no keypath? update all the things
else {
for ( kp in this.data ) {
if ( this.data.hasOwnProperty( kp ) ) {
this._notifyObservers( kp, this.get( kp ) );
}
}
}
},
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 ? view.root._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 ) {
view.keypath = keypath;
// create observers
view.observerRefs = self.observe({
keypath: keypath,
priority: view.model.p || 0,
view: view
});
// pass value through formatters, if there are any
if ( view.model.fmtrs ) {
value = view.root._format( value, view.model.fmtrs );
}
view.update( value );
};
resolved = this.resolveRef( view.model.ref, view.contextStack );
if ( !resolved ) {
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;
},
_notifyObservers: function ( keypath, value ) {
var self = this, observersGroupedByLevel = this.observers[ keypath ] || [], i, j, priority, observer, actualValue;
for ( i=0; i<observersGroupedByLevel.length; i+=1 ) {
priority = observersGroupedByLevel[i];
if ( priority ) {
for ( j=0; j<priority.length; j+=1 ) {
observer = priority[j];
if ( keypath !== observer.originalAddress ) {
actualValue = self.get( observer.originalAddress );
} else {
actualValue = value;
}
// apply formatters, if there are any
if ( observer.view.model.fmtrs ) {
actualValue = observer.view.root._format( actualValue, observer.view.model.fmtrs );
}
observer.view.update( actualValue );
}
}
}
},
observe: function ( options ) {
var self = this, keypath, originalAddress = options.keypath, priority = options.priority, observerRefs = [], observe;
// Allow `observe( keypath, callback )` syntax
if ( arguments.length === 2 && typeof arguments[0] === 'string' && typeof arguments[1] === 'function' ) {
return this.observe({ keypath: arguments[0], callback: arguments[1], priority: 0 });
}
if ( !options.keypath ) {
return undefined;
}
observe = function ( keypath ) {
var observers, observer;
observers = self.observers[ keypath ] = self.observers[ keypath ] || [];
observers = observers[ priority ] = observers[ priority ] || [];
observer = {
originalAddress: originalAddress
};
// if we're given a view to update, add it to the observer - ditto callbacks
if ( options.view ) {
observer.view = options.view;
}
if ( options.callback ) {
observer.callback = options.callback;
}
observers[ observers.length ] = observer;
observerRefs[ observerRefs.length ] = {
keypath: keypath,
priority: priority,
observer: observer
};
};
keypath = options.keypath;
while ( keypath.lastIndexOf( '.' ) !== -1 ) {
observe( keypath );
// remove the last item in the keypath, so that data.set( 'parent', { child: 'newValue' } ) affects views dependent on parent.child
keypath = keypath.substr( 0, keypath.lastIndexOf( '.' ) );
}
observe( keypath );
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.observer ) {
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() );
}
}
};
if ( Array.prototype.filter ) { // Browsers that aren't unredeemable pieces of shit
A.ViewModel.prototype.cancelKeypathResolution = function ( view ) {
this.pendingResolution = this.pendingResolution.filter( function ( pending ) {
return pending.view !== view;
});
};
}
else { // Internet Exploder
A.ViewModel.prototype.cancelKeypathResolution = function ( view ) {
var i, filtered = [];
for ( i=0; i<this.pendingResolution.length; i+=1 ) {
if ( this.pendingResolution[i].view !== view ) {
filtered[ filtered.length ] = this.pendingResolution[i];
}
}
this.pendingResolution = filtered;
};
}
keypathNormaliser = /\[\s*([0-9]+)\s*\]/g;
splitKeypath = function ( keypath ) {
var normalised;
// normalise keypath (e.g. 'foo[0]' becomes 'foo.0')
normalised = keypath.replace( keypathNormaliser, '.$1' );
return normalised.split( '.' );
};
}( Ractive ));
(function ( A ) {
'use strict';
var types, insertHtml, elContains, doc,
Text, Element, Partial, Attribute, Interpolator, Triple, Section;
types = A.types;
doc = ( typeof window !== 'undefined' ? window.document : null );
elContains = function ( haystack, needle ) {
// TODO!
if ( haystack.contains ) {
return haystack.contains( needle );
}
return true;
};
insertHtml = function ( html, parent, anchor ) {
var div, i, len, nodes = [];
anchor = anchor || null;
div = doc.createElement( 'div' );
div.innerHTML = html;
len = div.childNodes.length;
for ( i=0; i<len; i+=1 ) {
nodes[i] = div.childNodes[i];
}
for ( i=0; i<len; i+=1 ) {
parent.insertBefore( nodes[i], anchor );
}
return nodes;
};
A.DomFragment = function ( options ) {
A._Fragment.call( this, options );
};
A.DomFragment.prototype = {
preInit: function ( options ) {
// if we have an HTML string, our job is easy.
if ( typeof options.model === 'string' ) {
this.nodes = insertHtml( options.model, options.parentNode, options.anchor );
return true; // prevent the rest of the init sequence
}
},
createItem: function ( options ) {
if ( typeof options.model === 'string' ) {
return new Text( options );
}
switch ( options.model.type ) {
case types.INTERPOLATOR: return new Interpolator( options );
case types.SECTION: return new Section( options );
case types.TRIPLE: return new Triple( options );
case types.ELEMENT: return new Element( options );
case types.PARTIAL: return new Partial( options );
default: throw 'WTF? not sure what happened here...';
}
},
teardown: function () {
var node;
// if this was built from HTML, we just need to remove the nodes
if ( this.nodes ) {
while ( this.nodes.length ) {
node = this.nodes.pop();
node.parentNode.removeChild( node );
}
return;
}
// otherwise we need to do a proper teardown
while ( this.items.length ) {
this.items.pop().teardown();
}
},
firstNode: function () {
if ( this.items[0] ) {
return this.items[0].firstNode();
}
if ( this.parentSection ) {
return this.parentSection.findNextNode( this );
}
return null;
},
findNextNode: function ( item ) {
var index = item.index;
if ( this.items[ index + 1 ] ) {
return this.items[ index + 1 ].firstNode();
}
if ( this.parentSection ) {
return this.parentSection.findNextNode( this );
}
return null;
}
};
// Partials
Partial = function ( options ) {
this.fragment = new A.DomFragment({
model: options.root.partials[ options.model.ref ] || [],
root: options.root,
parentNode: options.parentNode,
contextStack: options.contextStack,
anchor: options.anchor,
parent: this
});
};
Partial.prototype = {
teardown: function () {
this.fragment.teardown();
}
};
// Plain text
Text = function ( options ) {
this.node = doc.createTextNode( options.model );
this.index = options.index;
this.root = options.root;
this.parentNode = options.parentNode;
// append this.node, either at end of parent element or in front of the anchor (if defined)
this.parentNode.insertBefore( this.node, options.anchor || null );
};
Text.prototype = {
teardown: function () {
if ( elContains( this.root.el, this.node ) ) {
this.parentNode.removeChild( this.node );
}
},
firstNode: function () {
return this.node;
}
};
// Element
Element = function ( options ) {
var binding,
model,
namespace,
attr,
attrName,
attrValue;
// stuff we'll need later
model = this.model = options.model;
this.root = options.root;
this.viewmodel = options.root.viewmodel;
this.parentFragment = options.parentFragment;
this.parentNode = options.parentNode;
this.index = options.index;
// get namespace
if ( model.attrs && model.attrs.xmlns ) {
namespace = model.attrs.xmlns;
// check it's a string!
if ( typeof namespace !== 'string' ) {
throw 'Namespace attribute cannot contain mustaches';
}
} else {
namespace = this.parentNode.namespaceURI;
}
// create the DOM node
this.node = doc.createElementNS( namespace, model.tag );
// set attributes
this.attributes = [];
for ( attrName in model.attrs ) {
if ( model.attrs.hasOwnProperty( attrName ) ) {
attrValue = model.attrs[ attrName ];
attr = new Attribute({
parent: this,
name: attrName,
value: attrValue,
root: options.root,
parentNode: this.node,
contextStack: options.contextStack
});
// if two-way binding is enabled, and we've got a dynamic `value` attribute, and this is an input or textarea, set up two-way binding
if ( attrName === 'value' && this.root.twoway && ( model.tag.toLowerCase() === 'input' || model.tag.toLowerCase() === 'textarea' ) ) {
binding = attr;
}
this.attributes[ this.attributes.length ] = attr;
}
}
if ( binding ) {
this.bind( binding, options.root.lazy );
}
// append children, if there are any
if ( model.frag ) {
if ( typeof model.frag === 'string' ) {
// great! we can use innerHTML
this.node.innerHTML = model.frag;
}
else {
this.children = new A.DomFragment({
model: model.frag,
root: options.root,
parentNode: this.node,
contextStack: options.contextStack,
anchor: null,
parent: this
});
}
}
// append this.node, either at end of parent element or in front of the anchor (if defined)
this.parentNode.insertBefore( this.node, options.anchor || null );
};
Element.prototype = {
bind: function ( attribute, lazy ) {
var viewmodel = this.viewmodel, node = this.node, setValue, valid, interpolator, keypath;
// Check this is a suitable candidate for two-way binding - i.e. it is
// a single interpolator with no formatters
valid = true;
if ( !attribute.fragment ||
( attribute.fragment.items.length !== 1 ) ||
( attribute.fragment.items[0].type !== A.types.INTERPOLATOR ) ||
( attribute.fragment.items[0].model.formatters && attribute.fragment.items[0].model.formatters.length )
) {
throw 'Not a valid two-way data binding candidate - must be a single interpolator with no formatters';
}
interpolator = attribute.fragment.items[0];
// Hmmm. Not sure if this is the best way to handle this ambiguity...
//
// Let's say we were given `value="{{bar}}"`. If the context stack was
// context stack was `["foo"]`, and `foo.bar` *wasn't* `undefined`, the
// keypath would be `foo.bar`. Then, any user input would result in
// `foo.bar` being updated.
//
// If, however, `foo.bar` *was* undefined, and so was `bar`, we would be
// left with an unresolved partial keypath - so we are forced to make an
// assumption. That assumption is that the input in question should
// be forced to resolve to `bar`, and any user input would affect `bar`
// and not `foo.bar`.
//
// Did that make any sense? No? Oh. Sorry. Well the moral of the story is
// be explicit when using two-way data-binding about what keypath you're
// updating. Using it in lists is probably a recipe for confusion...
keypath = interpolator.keypath || interpolator.model.partialKeypath;
setValue = function () {
var value = node.value;
// special cases
if ( value === '0' ) {
value = 0;
}
else if ( value !== '' ) {
value = +value || value;
}
// Note: we're counting on `viewmodel.set` recognising that `value` is
// already what it wants it to be, and short circuiting the process.
// Rather than triggering an infinite loop...
viewmodel.set( keypath, value );
};
// set initial value
setValue();
// TODO support shite browsers like IE and Opera
node.addEventListener( 'change', setValue );
if ( !lazy ) {
node.addEventListener( 'keyup', setValue );
}
},
teardown: function () {
if ( elContains( this.root.el, this.node ) ) {
this.parentNode.removeChild( this.node );
}
if ( this.children ) {
this.children.teardown();
}
while ( this.attributes.length ) {
this.attributes.pop().teardown();
}
},
firstNode: function () {
return this.node;
}
};
// Attribute
Attribute = function ( options ) {
var name, value, colonIndex, namespacePrefix, namespace, ancestor;
name = options.name;
value = options.value;
this.parent = options.parent; // the element this belongs to
// are we dealing with a namespaced attribute, e.g. xlink:href?
colonIndex = name.indexOf( ':' );
if ( colonIndex !== -1 ) {
// looks like we are, yes...
namespacePrefix = name.substr( 0, colonIndex );
// ...unless it's a namespace *declaration*
if ( namespacePrefix === 'xmlns' ) {
namespace = null;
}
else {
// we need to find an ancestor element that defines this prefix
ancestor = options.parentNode;
// continue searching until there's nowhere further to go, or we've found the declaration
while ( ancestor && !namespace ) {
namespace = ancestor.getAttribute( 'xmlns:' + namespacePrefix );
// continue searching possible ancestors
ancestor = ancestor.parentNode || options.parent.parentFragment.parent.node || options.parent.parentFragment.parent.parentNode;
}
}
// if we've found a namespace, make a note of it
if ( namespace ) {
this.namespace = namespace;
}
}
// if it's just a straight key-value pair, with no mustache shenanigans, set the attribute accordingly
if ( typeof value === 'string' ) {
if ( namespace ) {
options.parentNode.setAttributeNS( namespace, name.replace( namespacePrefix + ':', '' ), value );
} else {
options.parentNode.setAttribute( name, value );
}
return;
}
// otherwise we need to do some work
this.parentNode = options.parentNode;
this.name = name;
this.children = [];
// share parentFragment with parent element
this.parentFragment = this.parent.parentFragment;
this.fragment = new A.TextFragment({
model: value,
root: options.root,
parent: this,
contextStack: options.contextStack
});
// manually trigger first update
this.ready = true;
this.update();
};
Attribute.prototype = {
teardown: function () {
// ignore non-dynamic attributes
if ( !this.children ) {
return;
}
while ( this.children.length ) {
this.children.pop().teardown();
}
},
bubble: function () {
this.update();
},
update: function () {
var prevValue = this.value;
if ( !this.ready ) {
return; // avoid items bubbling to the surface when we're still initialising
}
this.value = this.fragment.toString();
if ( this.value !== prevValue ) {
if ( this.namespace ) {
this.parentNode.setAttributeNS( this.namespace, this.name, this.value );
} else {
this.parentNode.setAttribute( this.name, this.value );
}
}
}
};
// Interpolator
Interpolator = function ( options ) {
// extend Mustache
A._Mustache.call( this, options );
};
Interpolator.prototype = {
initialize: function () {
this.node = doc.createTextNode( '' );
this.parentNode.insertBefore( this.node, this.anchor || null );
},
teardown: function () {
if ( !this.observerRefs ) {
this.viewmodel.cancelKeypathResolution( this );
} else {
this.viewmodel.unobserveAll( this.observerRefs );
}
if ( elContains( this.root.el, this.node ) ) {
this.parentNode.removeChild( this.node );
}
},
update: function ( text ) {
if ( text !== this.text ) {
this.text = text;
this.node.data = text;
}
},
firstNode: function () {
return this.node;
}
};
// Triple
Triple = function ( options ) {
A._Mustache.call( this, options );
};
Triple.prototype = {
initialize: function () {
this.nodes = [];
},
teardown: function () {
// remove child nodes from DOM
if ( elContains( this.root.el, this.parentNode ) ) {
while ( this.nodes.length ) {
this.parentNode.removeChild( this.nodes.pop() );
}
}
// kill observer(s)
if ( !this.observerRefs ) {
this.viewmodel.cancelKeypathResolution( this );
} else {
this.viewmodel.unobserveAll( this.observerRefs );
}
},
firstNode: function () {
if ( this.nodes[0] ) {
return this.nodes[0];
}
return this.parentFragment.findNextNode( this );
},
update: function ( html ) {
var anchor;
if ( html === this.html ) {
return;
}
this.html = html;
// remove existing nodes
while ( this.nodes.length ) {
this.parentNode.removeChild( this.nodes.pop() );
}
anchor = this.anchor || this.parentFragment.findNextNode( this );
// get new nodes
this.nodes = insertHtml( html, this.parentNode, anchor );
}
};
// Section
Section = function ( options ) {
A._Mustache.call( this, options );
};
Section.prototype = {
initialize: function () {
this.fragments = [];
this.length = 0; // number of times this section is rendered
},
teardown: function () {
this.unrender();
if ( !this.observerRefs ) {
this.viewmodel.cancelKeypathResolution( this );
} else {
this.viewmodel.unobserveAll( this.observerRefs );
}
},
firstNode: function () {
if ( this.fragments[0] ) {
return this.fragments[0].firstNode();
}
return this.parentFragment.findNextNode( this );
},
findNextNode: function ( fragment ) {
if ( this.fragments[ fragment.index + 1 ] ) {
return this.fragments[ fragment.index + 1 ].firstNode();
}
return this.parentFragment.findNextNode( this );
},
unrender: function () {
while ( this.fragments.length ) {
this.fragments.shift().teardown();
}
},
update: A._sectionUpdate,
createFragment: function ( options ) {
return new A.DomFragment( options );
}
};
}( Ractive ));
(function ( A ) {
'use strict';
var types,
Text, Interpolator, Triple, Section;
types = A.types;
A.TextFragment = function ( options ) {
A._Fragment.call( this, options );
};
A.TextFragment.prototype = {
init: function () {
this.value = this.items.join('');
},
createItem: function ( options ) {
if ( typeof options.model === 'string' ) {
return new Text( options.model );
}
switch ( options.model.type ) {
case types.INTERPOLATOR: return new Interpolator( options );
case types.TRIPLE: return new Triple( options );
case types.SECTION: return new Section( options );
default: throw 'Something went wrong in a rather interesting way';
}
},
bubble: function () {
this.value = this.items.join( '' );
this.parent.bubble();
},
teardown: function () {
var numItems, i;
numItems = this.items.length;
for ( i=0; i<numItems; i+=1 ) {
this.items[i].teardown();
}
},
toString: function () {
// TODO refactor this... value should already have been calculated? or maybe not. Top-level items skip the fragment and bubble straight to the attribute...
// argh, it's confusing me
return this.items.join( '' );
}
};
// Plain text
Text = function ( text ) {
this.text = text;
};
Text.prototype = {
toString: function () {
return this.text;
},
teardown: function () {} // no-op
};
// Mustaches
// Interpolator or Triple
Interpolator = function ( options ) {
A._Mustache.call( this, options );
};
Interpolator.prototype = {
update: function ( value ) {
this.value = value;
this.parent.bubble();
},
teardown: function () {
if ( !this.observerRefs ) {
this.viewmodel.cancelKeypathResolution( this );
} else {
this.viewmodel.unobserveAll( this.observerRefs );
}
},
toString: function () {
return ( this.value === undefined ? '' : this.value );
}
};
// Triples are the same as Interpolators in this context
Triple = Interpolator;
// Section
Section = function ( options ) {
A._Mustache.call( this, options );
};
Section.prototype = {
initialize: function () {
this.fragments = [];
this.length = 0;
},
teardown: function () {
this.unrender();
if ( !this.observerRefs ) {
this.viewmodel.cancelKeypathResolution( this );
} else {
this.viewmodel.unobserveAll( this.observerRefs );
}
},
unrender: function () {
while ( this.fragments.length ) {
this.fragments.shift().teardown();
}
this.length = 0;
},
bubble: function () {
this.value = this.fragments.join( '' );
this.parent.bubble();
},
update: function ( value ) {
A._sectionUpdate.call( this, value );
},
createFragment: function ( options ) {
return new A.TextFragment( options );
},
postUpdate: function () {
this.value = this.fragments.join( '' );
this.parent.bubble();
},
toString: function () {
return this.fragments.join( '' );
//return ( this.value === undefined ? '' : this.value );
}
};
}( Ractive ));
(function ( A ) {
'use strict';
A.extend = function ( childProps ) {
var Parent, Child, key;
Parent = this;
Child = function () {
A.apply( this, arguments );
if ( this.init ) {
this.init.apply( this, arguments );
}
};
// 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
for ( key in childProps ) {
if ( childProps.hasOwnProperty( key ) ) {
if ( A.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;
};
}( Ractive ));
(function ( A ) {
'use strict';
var wrapMethods;
A.modifyArray = function ( array, keypath, viewmodel ) {
var viewmodels, keypathsByIndex, viewmodelIndex, keypaths;
if ( !array._ractive ) {
array._ractive = {
viewmodels: [ viewmodel ],
keypathsByIndex: [ [ keypath ] ]
};
wrapMethods( array );
}
else {
viewmodels = array._ractive.viewmodels;
keypathsByIndex = array._ractive.keypathsByIndex;
// see if this viewmodel is currently associated with this array
viewmodelIndex = viewmodels.indexOf( viewmodel );
// if not, associate it
if ( viewmodelIndex === -1 ) {
viewmodelIndex = viewmodels.length;
viewmodels[ viewmodelIndex ] = viewmodel;
}
// find keypaths that reference this array, on this viewmodel
keypaths = keypathsByIndex[ viewmodelIndex ];
// if the current keypath isn't among them, add it
if ( keypaths.indexOf( keypath ) === -1 ) {
keypaths[ keypaths.length ] = keypath;
}
}
};
wrapMethods = function ( array ) {
var notifyDependents = function ( array ) {
var viewmodels, keypathsByIndex;
viewmodels = array._ractive.viewmodels;
keypathsByIndex = array._ractive.keypathsByIndex;
viewmodels.forEach( function ( viewmodel, i ) {
var keypaths = keypathsByIndex[i];
keypaths.forEach( function ( keypath ) {
viewmodel.set( keypath, array );
});
});
};
[ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ].forEach( function ( method ) {
array[ method ] = function () {
var result = Array.prototype[ method ].apply( this, arguments );
notifyDependents( array );
return result;
};
});
};
}( Ractive ));
(function ( A ) {
'use strict';
// --------------------------------------------------
// 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
// --------------------------------------------------
A.easing = {
easeInQuad: function(pos) {
return Math.pow(pos, 2);
},
easeOutQuad: function(pos) {
return -(Math.pow((pos-1), 2) -1);
},
easeInOutQuad: function(pos) {
if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,2);
return -0.5 * ((pos-=2)*pos - 2);
},
easeInCubic: function(pos) {
return Math.pow(pos, 3);
},
easeOutCubic: function(pos) {
return (Math.pow((pos-1), 3) +1);
},
easeInOutCubic: function(pos) {
if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,3);
return 0.5 * (Math.pow((pos-2),3) + 2);
},
easeInQuart: function(pos) {
return Math.pow(pos, 4);
},
easeOutQuart: function(pos) {
return -(Math.pow((pos-1), 4) -1);
},
easeInOutQuart: function(pos) {
if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,4);
return -0.5 * ((pos-=2)*Math.pow(pos,3) - 2);
},
easeInQuint: function(pos) {
return Math.pow(pos, 5);
},
easeOutQuint: function(pos) {
return (Math.pow((pos-1), 5) +1);
},
easeInOutQuint: function(pos) {
if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,5);
return 0.5 * (Math.pow((pos-2),5) + 2);
},
easeInSine: function(pos) {
return -Math.cos(pos * (Math.PI/2)) + 1;
},
easeOutSine: function(pos) {
return Math.sin(pos * (Math.PI/2));
},
easeInOutSine: function(pos) {
return (-0.5 * (Math.cos(Math.PI*pos) -1));
},
easeInExpo: function(pos) {
return (pos===0) ? 0 : Math.pow(2, 10 * (pos - 1));
},
easeOutExpo: function(pos) {
return (pos===1) ? 1 : -Math.pow(2, -10 * pos) + 1;
},
easeInOutExpo: function(pos) {
if(pos===0) return 0;
if(pos===1) return 1;
if((pos/=0.5) < 1) return 0.5 * Math.pow(2,10 * (pos-1));
return 0.5 * (-Math.pow(2, -10 * --pos) + 2);
},
easeInCirc: function(pos) {
return -(Math.sqrt(1 - (pos*pos)) - 1);
},
easeOutCirc: function(pos) {
return Math.sqrt(1 - Math.pow((pos-1), 2));
},
easeInOutCirc: function(pos) {
if((pos/=0.5) < 1) return -0.5 * (Math.sqrt(1 - pos*pos) - 1);
return 0.5 * (Math.sqrt(1 - (pos-=2)*pos) + 1);
},
easeOutBounce: function(pos) {
if ((pos) < (1/2.75)) {
return (7.5625*pos*pos);
} else if (pos < (2/2.75)) {
return (7.5625*(pos-=(1.5/2.75))*pos + 0.75);
} else if (pos < (2.5/2.75)) {
return (7.5625*(pos-=(2.25/2.75))*pos + 0.9375);
} else {
return (7.5625*(pos-=(2.625/2.75))*pos + 0.984375);
}
},
easeInBack: function(pos) {
var s = 1.70158;
return (pos)*pos*((s+1)*pos - s);
},
easeOutBack: function(pos) {
var s = 1.70158;
return (pos=pos-1)*pos*((s+1)*pos + s) + 1;
},
easeInOutBack: function(pos) {
var s = 1.70158;
if((pos/=0.5) < 1) return 0.5*(pos*pos*(((s*=(1.525))+1)*pos -s));
return 0.5*((pos-=2)*pos*(((s*=(1.525))+1)*pos +s) +2);
},
elastic: function(pos) {
return -1 * Math.pow(4,-8*pos) * Math.sin((pos*6-1)*(2*Math.PI)/2) + 1;
},
swingFromTo: function(pos) {
var s = 1.70158;
return ((pos/=0.5) < 1) ? 0.5*(pos*pos*(((s*=(1.525))+1)*pos - s)) :
0.5*((pos-=2)*pos*(((s*=(1.525))+1)*pos + s) + 2);
},
swingFrom: function(pos) {
var s = 1.70158;
return pos*pos*((s+1)*pos - s);
},
swingTo: function(pos) {
var s = 1.70158;
return (pos-=1)*pos*((s+1)*pos + s) + 1;
},
bounce: function(pos) {
if (pos < (1/2.75)) {
return (7.5625*pos*pos);
} else if (pos < (2/2.75)) {
return (7.5625*(pos-=(1.5/2.75))*pos + 0.75);
} else if (pos < (2.5/2.75)) {
return (7.5625*(pos-=(2.25/2.75))*pos + 0.9375);
} else {
return (7.5625*(pos-=(2.625/2.75))*pos + 0.984375);
}
},
bouncePast: function(pos) {
if (pos < (1/2.75)) {
return (7.5625*pos*pos);
} else if (pos < (2/2.75)) {
return 2 - (7.5625*(pos-=(1.5/2.75))*pos + 0.75);
} else if (pos < (2.5/2.75)) {
return 2 - (7.5625*(pos-=(2.25/2.75))*pos + 0.9375);
} else {
return 2 - (7.5625*(pos-=(2.625/2.75))*pos + 0.984375);
}
},
easeFromTo: function(pos) {
if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,4);
return -0.5 * ((pos-=2)*Math.pow(pos,3) - 2);
},
easeFrom: function(pos) {
return Math.pow(pos,4);
},
easeTo: function(pos) {
return Math.pow(pos,0.25);
}
};
}( Ractive ));
(function ( A ) {
'use strict';
var Animation, animationCollection, global;
global = ( typeof window !== 'undefined' ? window : {} );
// https://gist.github.com/paulirish/1579671
(function( vendors, lastTime, window ) {
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 ));
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.delta = this.to - this.from;
this.running = true;
};
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();
}
}
};
Animation.prototype = {
tick: function () {
var elapsed, t, value, timeNow;
if ( this.running ) {
timeNow = Date.now();
elapsed = timeNow - this.startTime;
if ( elapsed >= this.duration ) {
this.viewmodel.set( this.keypath, this.to );
if ( this.complete ) {
this.complete( 1 );
}
this.running = false;
return false;
}
t = this.easing ? this.easing ( elapsed / this.duration ) : ( elapsed / this.duration );
value = this.from + ( t * this.delta );
this.viewmodel.set( this.keypath, value );
if ( this.step ) {
this.step( t, value );
}
return true;
}
return