UNPKG

ractive

Version:

Next-generation DOM manipulation

2,088 lines (1,613 loc) 234 kB
/*! Ractive - v0.3.7 - 2013-10-14 * Next-generation DOM manipulation * http://ractivejs.org * Copyright (c) 2013 Rich Harris; Licensed MIT */ (function ( global ) { 'use strict'; var Ractive, // current version VERSION = '0.3.7', doc = global.document || null, // Ractive prototype proto = {}, // properties of the public Ractive object adaptors = {}, eventDefinitions = {}, easing, extend, parse, interpolate, interpolators, transitions = {}, // internal utils - instance-specific teardown, clearCache, registerDependant, unregisterDependant, notifyDependants, notifyMultipleDependants, notifyDependantsByPriority, resolveRef, processDeferredUpdates, // internal utils toString, isArray, isObject, isNumeric, isEqual, getEl, insertHtml, reassignFragments, executeTransition, getPartialDescriptor, getComponentConstructor, isStringFragmentSimple, makeTransitionManager, requestAnimationFrame, defineProperty, defineProperties, create, createFromNull, hasOwn = {}.hasOwnProperty, noop = function () {}, addEventProxies, addEventProxy, appendElementChildren, bindElement, createElementAttributes, getElementNamespace, updateAttribute, bindAttribute, console = global.console || { log: noop, warn: noop }, // internally used constructors DomFragment, DomElement, DomAttribute, DomPartial, DomComponent, DomInterpolator, DomTriple, DomSection, DomText, StringFragment, StringInterpolator, StringSection, StringText, ExpressionResolver, Evaluator, Animation, // internally used regexes leadingWhitespace = /^\s+/, trailingWhitespace = /\s+$/, // other bits and pieces render, initMustache, updateMustache, resolveMustache, initFragment, updateSection, animationCollection, // array modification registerKeypathToArray, unregisterKeypathFromArray, // parser and tokenizer getFragmentStubFromTokens, getToken, tokenize, stripCommentTokens, stripHtmlComments, stripStandalones, // error messages missingParser = 'Missing Ractive.parse - cannot parse template. Either preparse or use the version that includes the parser', // constants TEXT = 1, INTERPOLATOR = 2, TRIPLE = 3, SECTION = 4, INVERTED = 5, CLOSING = 6, ELEMENT = 7, PARTIAL = 8, COMMENT = 9, DELIMCHANGE = 10, MUSTACHE = 11, TAG = 12, COMPONENT = 15, NUMBER_LITERAL = 20, STRING_LITERAL = 21, ARRAY_LITERAL = 22, OBJECT_LITERAL = 23, BOOLEAN_LITERAL = 24, GLOBAL = 26, KEY_VALUE_PAIR = 27, REFERENCE = 30, REFINEMENT = 31, MEMBER = 32, PREFIX_OPERATOR = 33, BRACKETED = 34, CONDITIONAL = 35, INFIX_OPERATOR = 36, INVOCATION = 40, testDiv = ( doc ? doc.createElement( 'div' ) : null ), noMagic, // namespaces namespaces = { html: 'http://www.w3.org/1999/xhtml', mathml: 'http://www.w3.org/1998/Math/MathML', svg: 'http://www.w3.org/2000/svg', xlink: 'http://www.w3.org/1999/xlink', xml: 'http://www.w3.org/XML/1998/namespace', xmlns: 'http://www.w3.org/2000/xmlns/' }; // we're creating a defineProperty function here - we don't want to add // this to _legacy.js since it's not a polyfill. It won't allow us to set // non-enumerable properties. That shouldn't be a problem, unless you're // using for...in on a (modified) array, in which case you deserve what's // coming anyway try { try { Object.defineProperty({}, 'test', { value: 0 }); Object.defineProperties({}, { test: { value: 0 } }); } catch ( err ) { noMagic = true; throw err; } if ( doc ) { Object.defineProperty( testDiv, 'test', { value: 0 }); Object.defineProperties( testDiv, { test: { value: 0 } }); } defineProperty = Object.defineProperty; defineProperties = Object.defineProperties; } catch ( err ) { // Object.defineProperty doesn't exist, or we're in IE8 where you can // only use it with DOM objects (what the fuck were you smoking, MSFT?) defineProperty = function ( obj, prop, desc ) { obj[ prop ] = desc.value; }; defineProperties = function ( obj, props ) { var prop; for ( prop in props ) { if ( props.hasOwnProperty( prop ) ) { defineProperty( obj, prop, props[ prop ] ); } } }; } try { Object.create( null ); create = Object.create; createFromNull = function () { return Object.create( null ); }; } catch ( err ) { // sigh create = (function () { var F = function () {}; return function ( proto, props ) { var obj; F.prototype = proto; obj = new F(); if ( props ) { Object.defineProperties( obj, props ); } return obj; }; }()); createFromNull = function () { return {}; // hope you're not modifying the Object prototype }; } var hyphenate = function ( str ) { return str.replace( /[A-Z]/g, function ( match ) { return '-' + match.toLowerCase(); }); }; // determine some facts about our environment var cssTransitionsEnabled, transition, transitionend; (function () { if ( !doc ) { return; } if ( testDiv.style.transition !== undefined ) { transition = 'transition'; transitionend = 'transitionend'; cssTransitionsEnabled = true; } else if ( testDiv.style.webkitTransition !== undefined ) { transition = 'webkitTransition'; transitionend = 'webkitTransitionEnd'; cssTransitionsEnabled = true; } else { cssTransitionsEnabled = false; } }()); // Internet Explorer derp. Methods that should be attached to Node.prototype // are instead attached to HTMLElement.prototype, which means SVG elements // can't use them. Remember kids, friends don't let friends use IE. if ( global.Node && !global.Node.prototype.contains && global.HTMLElement && global.HTMLElement.prototype.contains ) { global.Node.prototype.contains = global.HTMLElement.prototype.contains; } (function () { var getInterpolator, updateModel, getBinding, inheritProperties, MultipleSelectBinding, SelectBinding, RadioNameBinding, CheckboxNameBinding, CheckedBinding, FileListBinding, GenericBinding; bindAttribute = function () { var node = this.parentNode, interpolator, binding, bindings; if ( !this.fragment ) { return false; // report failure } interpolator = getInterpolator( this ); if ( !interpolator ) { return false; // report failure } this.interpolator = interpolator; // 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... this.keypath = interpolator.keypath || interpolator.descriptor.r; binding = getBinding( this ); if ( !binding ) { return false; } node._ractive.binding = binding; this.twoway = true; // register this with the root, so that we can force an update later bindings = this.root._twowayBindings[ this.keypath ] || ( this.root._twowayBindings[ this.keypath ] = [] ); bindings[ bindings.length ] = binding; return true; }; // This is the handler for DOM events that would lead to a change in the model // (i.e. change, sometimes, input, and occasionally click and keyup) updateModel = function () { this._ractive.binding.update(); }; getInterpolator = function ( attribute ) { var item; // TODO refactor this? Couldn't the interpolator have got a keypath via an expression? // Check this is a suitable candidate for two-way binding - i.e. it is // a single interpolator, which isn't an expression if ( attribute.fragment.items.length !== 1 ) { return null; } item = attribute.fragment.items[0]; if ( item.type !== INTERPOLATOR ) { return null; } if ( !item.keypath && !item.ref ) { return null; } return item; }; getBinding = function ( attribute ) { var node = attribute.parentNode; if ( node.tagName === 'SELECT' ) { return ( node.multiple ? new MultipleSelectBinding( attribute, node ) : new SelectBinding( attribute, node ) ); } if ( node.type === 'checkbox' || node.type === 'radio' ) { if ( attribute.propertyName === 'name' ) { if ( node.type === 'checkbox' ) { return new CheckboxNameBinding( attribute, node ); } if ( node.type === 'radio' ) { return new RadioNameBinding( attribute, node ); } } if ( attribute.propertyName === 'checked' ) { return new CheckedBinding( attribute, node ); } return null; } if ( attribute.propertyName !== 'value' ) { console.warn( 'This is... odd' ); } if ( attribute.parentNode.type === 'file' ) { return new FileListBinding( attribute, node ); } return new GenericBinding( attribute, node ); }; MultipleSelectBinding = function ( attribute, node ) { var valueFromModel; inheritProperties( this, attribute, node ); node.addEventListener( 'change', updateModel, false ); valueFromModel = this.root.get( this.keypath ); if ( valueFromModel === undefined ) { // get value from DOM, if possible this.update(); } }; MultipleSelectBinding.prototype = { value: function () { var value, options, i, len; value = []; options = this.node.options; len = options.length; for ( i=0; i<len; i+=1 ) { if ( options[i].selected ) { value[ value.length ] = options[i]._ractive.value; } } return value; }, update: function () { var attribute, previousValue, value; attribute = this.attr; previousValue = attribute.value; value = this.value(); if ( previousValue === undefined || !arrayContentsMatch( value, previousValue ) ) { // either length or contents have changed, so we update the model attribute.receiving = true; attribute.value = value; this.root.set( this.keypath, value ); attribute.receiving = false; } }, teardown: function () { this.node.removeEventListener( 'change', updateModel, false ); } }; SelectBinding = function ( attribute, node ) { var valueFromModel; inheritProperties( this, attribute, node ); node.addEventListener( 'change', updateModel, false ); valueFromModel = this.root.get( this.keypath ); if ( valueFromModel === undefined ) { // get value from DOM, if possible this.update(); } }; SelectBinding.prototype = { value: function () { var options, i, len; options = this.node.options; len = options.length; for ( i=0; i<len; i+=1 ) { if ( options[i].selected ) { return options[i]._ractive.value; } } }, update: function () { var value = this.value(); this.attr.receiving = true; this.attr.value = value; this.root.set( this.keypath, value ); this.attr.receiving = false; }, teardown: function () { this.node.removeEventListener( 'change', updateModel, false ); } }; RadioNameBinding = function ( attribute, node ) { var valueFromModel; this.radioName = true; // so that updateModel knows what to do with this inheritProperties( this, attribute, node ); node.name = '{{' + attribute.keypath + '}}'; node.addEventListener( 'change', updateModel, false ); if ( node.attachEvent ) { node.addEventListener( 'click', updateModel, false ); } valueFromModel = this.root.get( this.keypath ); if ( valueFromModel !== undefined ) { node.checked = ( valueFromModel === node._ractive.value ); } else { this.root._defRadios[ this.root._defRadios.length ] = this; } }; RadioNameBinding.prototype = { value: function () { return this.node._ractive ? this.node._ractive.value : this.node.value; }, update: function () { var node = this.node; if ( node.checked ) { this.attr.receiving = true; this.root.set( this.keypath, this.value() ); this.attr.receiving = false; } }, teardown: function () { this.node.removeEventListener( 'change', updateModel, false ); this.node.removeEventListener( 'click', updateModel, false ); } }; CheckboxNameBinding = function ( attribute, node ) { var valueFromModel, checked; this.checkboxName = true; // so that updateModel knows what to do with this inheritProperties( this, attribute, node ); node.name = '{{' + this.keypath + '}}'; node.addEventListener( 'change', updateModel, false ); // in case of IE emergency, bind to click event as well if ( node.attachEvent ) { node.addEventListener( 'click', updateModel, false ); } valueFromModel = this.root.get( this.keypath ); // if the model already specifies this value, check/uncheck accordingly if ( valueFromModel !== undefined ) { checked = valueFromModel.indexOf( node._ractive.value ) !== -1; node.checked = checked; } // otherwise make a note that we will need to update the model later else { if ( this.root._defCheckboxes.indexOf( this.keypath ) === -1 ) { this.root._defCheckboxes[ this.root._defCheckboxes.length ] = this.keypath; } } }; CheckboxNameBinding.prototype = { changed: function () { return this.node.checked !== !!this.checked; }, update: function () { this.checked = this.node.checked; this.attr.receiving = true; this.root.set( this.keypath, getValueFromCheckboxes( this.root, this.keypath ) ); this.attr.receiving = false; }, teardown: function () { this.node.removeEventListener( 'change', updateModel, false ); this.node.removeEventListener( 'click', updateModel, false ); } }; CheckedBinding = function ( attribute, node ) { inheritProperties( this, attribute, node ); node.addEventListener( 'change', updateModel, false ); if ( node.attachEvent ) { node.addEventListener( 'click', updateModel, false ); } }; CheckedBinding.prototype = { value: function () { return this.node.checked; }, update: function () { this.attr.receiving = true; this.root.set( this.keypath, this.value() ); this.attr.receiving = false; }, teardown: function () { this.node.removeEventListener( 'change', updateModel, false ); this.node.removeEventListener( 'click', updateModel, false ); } }; FileListBinding = function ( attribute, node ) { inheritProperties( this, attribute, node ); node.addEventListener( 'change', updateModel, false ); }; FileListBinding.prototype = { value: function () { return this.attr.parentNode.files; }, update: function () { this.attr.root.set( this.attr.keypath, this.value() ); }, teardown: function () { this.node.removeEventListener( 'change', updateModel, false ); } }; GenericBinding = function ( attribute, node ) { inheritProperties( this, attribute, node ); node.addEventListener( 'change', updateModel, false ); if ( !this.root.lazy ) { node.addEventListener( 'input', updateModel, false ); if ( node.attachEvent ) { node.addEventListener( 'keyup', updateModel, false ); } } }; GenericBinding.prototype = { value: function () { var value = this.attr.parentNode.value; // if the value is numeric, treat it as a number. otherwise don't if ( ( +value + '' === value ) && value.indexOf( 'e' ) === -1 ) { value = +value; } return value; }, update: function () { var attribute = this.attr, value = this.value(); attribute.receiving = true; attribute.root.set( attribute.keypath, value ); attribute.receiving = false; }, teardown: function () { this.node.removeEventListener( 'change', updateModel, false ); this.node.removeEventListener( 'input', updateModel, false ); this.node.removeEventListener( 'keyup', updateModel, false ); } }; inheritProperties = function ( binding, attribute, node ) { binding.attr = attribute; binding.node = node; binding.root = attribute.root; binding.keypath = attribute.keypath; }; }()); (function () { var updateFileInputValue, deferSelect, initSelect, updateSelect, updateMultipleSelect, updateRadioName, updateCheckboxName, updateEverythingElse; // There are a few special cases when it comes to updating attributes. For this reason, // the prototype .update() method points to updateAttribute, which waits until the // attribute has finished initialising, then replaces the prototype method with a more // suitable one. That way, we save ourselves doing a bunch of tests on each call updateAttribute = function () { var node; if ( !this.ready ) { return this; // avoid items bubbling to the surface when we're still initialising } node = this.parentNode; // special case - selects if ( node.tagName === 'SELECT' && this.name === 'value' ) { this.update = deferSelect; this.deferredUpdate = initSelect; // we don't know yet if it's a select-one or select-multiple return this.update(); } // special case - <input type='file' value='{{fileList}}'> if ( this.isFileInputValue ) { this.update = updateFileInputValue; // save ourselves the trouble next time return this; } // special case - <input type='radio' name='{{twoway}}' value='foo'> if ( this.twoway && this.name === 'name' ) { if ( node.type === 'radio' ) { this.update = updateRadioName; return this.update(); } if ( node.type === 'checkbox' ) { this.update = updateCheckboxName; return this.update(); } } this.update = updateEverythingElse; return this.update(); }; updateFileInputValue = function () { return this; // noop - file inputs are readonly }; initSelect = function () { // we're now in a position to decide whether this is a select-one or select-multiple this.deferredUpdate = ( this.parentNode.multiple ? updateMultipleSelect : updateSelect ); this.deferredUpdate(); }; deferSelect = function () { // because select values depend partly on the values of their children, and their // children may be entering and leaving the DOM, we wait until updates are // complete before updating this.root._defSelectValues.push( this ); return this; }; updateSelect = function () { var value = this.fragment.getValue(), options, option, i; this.value = value; options = this.parentNode.options; i = options.length; while ( i-- ) { option = options[i]; if ( option._ractive.value === value ) { option.selected = true; return this; } } // if we're still here, it means the new value didn't match any of the options... // TODO figure out what to do in this situation return this; }; updateMultipleSelect = function () { var value = this.fragment.getValue(), options, i; if ( !isArray( value ) ) { value = [ value ]; } options = this.parentNode.options; i = options.length; while ( i-- ) { options[i].selected = ( value.indexOf( options[i]._ractive.value ) !== -1 ); } this.value = value; return this; }; updateRadioName = function () { var node, value; node = this.parentNode; value = this.fragment.getValue(); node.checked = ( value === node._ractive.value ); return this; }; updateCheckboxName = function () { var node, value; node = this.parentNode; value = this.fragment.getValue(); if ( !isArray( value ) ) { node.checked = ( value === node._ractive.value ); return this; } node.checked = ( value.indexOf( node._ractive.value ) !== -1 ); return this; }; updateEverythingElse = function () { var node, value; node = this.parentNode; value = this.fragment.getValue(); // store actual value, so it doesn't get coerced to a string if ( this.isValueAttribute ) { node._ractive.value = value; } if ( value === undefined ) { value = ''; } if ( value !== this.value ) { if ( this.useProperty ) { // with two-way binding, only update if the change wasn't initiated by the user // otherwise the cursor will often be sent to the wrong place if ( !this.receiving ) { node[ this.propertyName ] = value; } this.value = value; return this; } if ( this.namespace ) { node.setAttributeNS( this.namespace, this.name, value ); this.value = value; return this; } if ( this.name === 'id' ) { if ( this.value !== undefined ) { this.root.nodes[ this.value ] = undefined; } this.root.nodes[ value ] = node; } node.setAttribute( this.name, value ); this.value = value; } return this; }; }()); addEventProxies = function ( element, proxies ) { var i, eventName, eventNames; for ( eventName in proxies ) { if ( hasOwn.call( proxies, eventName ) ) { eventNames = eventName.split( '-' ); i = eventNames.length; while ( i-- ) { addEventProxy( element, eventNames[i], proxies[ eventName ], element.parentFragment.contextStack ); } } } }; (function () { var MasterEventHandler, ProxyEvent, firePlainEvent, fireEventWithArgs, fireEventWithDynamicArgs, customHandlers, genericHandler, getCustomHandler; addEventProxy = function ( element, triggerEventName, proxyDescriptor, contextStack, indexRefs ) { var events, master; events = element.ractify().events; master = events[ triggerEventName ] || ( events[ triggerEventName ] = new MasterEventHandler( element, triggerEventName, contextStack, indexRefs ) ); master.add( proxyDescriptor ); }; MasterEventHandler = function ( element, eventName, contextStack ) { var definition; this.element = element; this.root = element.root; this.node = element.node; this.name = eventName; this.contextStack = contextStack; // TODO do we need to pass contextStack down everywhere? Doesn't it belong to the parentFragment? this.proxies = []; if ( definition = ( this.root.eventDefinitions[ eventName ] || Ractive.eventDefinitions[ eventName ] ) ) { this.custom = definition( this.node, getCustomHandler( eventName ) ); } else { this.node.addEventListener( eventName, genericHandler, false ); } }; MasterEventHandler.prototype = { add: function ( proxy ) { this.proxies[ this.proxies.length ] = new ProxyEvent( this.element, this.root, proxy, this.contextStack ); }, // TODO teardown when element torn down teardown: function () { var i; if ( this.custom ) { this.custom.teardown(); } else { this.node.removeEventListener( this.name, genericHandler, false ); } i = this.proxies.length; while ( i-- ) { this.proxies[i].teardown(); } }, fire: function ( event ) { var i = this.proxies.length; while ( i-- ) { this.proxies[i].fire( event ); } } }; ProxyEvent = function ( element, ractive, descriptor, contextStack ) { var name; this.root = ractive; name = descriptor.n || descriptor; if ( typeof name === 'string' ) { this.n = name; } else { this.n = new StringFragment({ descriptor: descriptor.n, root: this.root, owner: element, contextStack: contextStack }); } if ( descriptor.a ) { this.a = descriptor.a; this.fire = fireEventWithArgs; return; } if ( descriptor.d ) { this.d = new StringFragment({ descriptor: descriptor.d, root: this.root, owner: element, contextStack: contextStack }); this.fire = fireEventWithDynamicArgs; return; } this.fire = firePlainEvent; }; ProxyEvent.prototype = { teardown: function () { if ( this.n.teardown) { this.n.teardown(); } if ( this.d ) { this.d.teardown(); } }, bubble: noop // TODO can we get rid of this? }; // the ProxyEvent instance fire method could be any of these firePlainEvent = function ( event ) { this.root.fire( this.n.toString(), event ); }; fireEventWithArgs = function ( event ) { this.root.fire( this.n.toString(), event, this.a ); }; fireEventWithDynamicArgs = function ( event ) { this.root.fire( this.n.toString(), event, this.d.toJSON() ); }; // all native DOM events dealt with by Ractive share a single handler genericHandler = function ( event ) { var storage = this._ractive; storage.events[ event.type ].fire({ node: this, original: event, index: storage.index, keypath: storage.keypath, context: storage.root.get( storage.keypath ) }); }; customHandlers = {}; getCustomHandler = function ( eventName ) { if ( customHandlers[ eventName ] ) { return customHandlers[ eventName ]; } return customHandlers[ eventName ] = function ( event ) { var storage = event.node._ractive; event.index = storage.index; event.keypath = storage.keypath; event.context = storage.root.get( storage.keypath ); storage.events[ eventName ].fire( event ); }; }; }()); appendElementChildren = function ( element, node, descriptor, docFrag ) { if ( typeof descriptor.f === 'string' && ( !node || ( !node.namespaceURI || node.namespaceURI === namespaces.html ) ) ) { // great! we can use innerHTML element.html = descriptor.f; if ( docFrag ) { node.innerHTML = element.html; } } else { // once again, everyone has to suffer because of IE bloody 8 if ( descriptor.e === 'style' && node.styleSheet !== undefined ) { element.fragment = new StringFragment({ descriptor: descriptor.f, root: element.root, contextStack: element.parentFragment.contextStack, owner: element }); if ( docFrag ) { element.bubble = function () { node.styleSheet.cssText = element.fragment.toString(); }; } } else { element.fragment = new DomFragment({ descriptor: descriptor.f, root: element.root, parentNode: node, contextStack: element.parentFragment.contextStack, owner: element }); if ( docFrag ) { node.appendChild( element.fragment.docFrag ); } } } }; bindElement = function ( element, attributes ) { element.ractify(); // an element can only have one two-way attribute switch ( element.descriptor.e ) { case 'select': case 'textarea': if ( attributes.value ) { attributes.value.bind(); } return; case 'input': if ( element.node.type === 'radio' || element.node.type === 'checkbox' ) { // we can either bind the name attribute, or the checked attribute - not both if ( attributes.name && attributes.name.bind() ) { return; } if ( attributes.checked && attributes.checked.bind() ) { return; } } if ( attributes.value && attributes.value.bind() ) { return; } } }; createElementAttributes = function ( element, attributes ) { var attrName, attrValue, attr; element.attributes = []; for ( attrName in attributes ) { if ( hasOwn.call( attributes, attrName ) ) { attrValue = attributes[ attrName ]; attr = new DomAttribute({ element: element, name: attrName, value: attrValue, root: element.root, parentNode: element.node, contextStack: element.parentFragment.contextStack }); element.attributes[ element.attributes.length ] = attr; // name, value and checked attributes are potentially bindable if ( attrName === 'value' || attrName === 'name' || attrName === 'checked' ) { element.attributes[ attrName ] = attr; } // The name attribute is a special case - it is the only two-way attribute that updates // the viewmodel based on the value of another attribute. For that reason it must wait // until the node has been initialised, and the viewmodel has had its first two-way // update, before updating itself (otherwise it may disable a checkbox or radio that // was enabled in the template) if ( attrName !== 'name' ) { attr.update(); } } } return element.attributes; }; getElementNamespace = function ( descriptor, parentNode ) { // if the element has an xmlns attribute, use that if ( descriptor.a && descriptor.a.xmlns ) { return descriptor.a.xmlns; } // otherwise, use the svg namespace if this is an svg element, or inherit namespace from parent return ( descriptor.e.toLowerCase() === 'svg' ? namespaces.svg : parentNode.namespaceURI ); }; executeTransition = function ( descriptor, root, owner, contextStack, isIntro ) { var transitionName, transitionParams, fragment, transitionManager, transition; if ( !root.transitionsEnabled ) { return; } if ( typeof descriptor === 'string' ) { transitionName = descriptor; } else { transitionName = descriptor.n; if ( descriptor.a ) { transitionParams = descriptor.a; } else if ( descriptor.d ) { fragment = new StringFragment({ descriptor: descriptor.d, root: root, owner: owner, contextStack: owner.parentFragment.contextStack }); transitionParams = fragment.toJSON(); fragment.teardown(); } } transition = root.transitions[ transitionName ] || Ractive.transitions[ transitionName ]; if ( transition ) { transitionManager = root._transitionManager; transitionManager.push( owner.node ); transition.call( root, owner.node, function () { transitionManager.pop( owner.node ); }, transitionParams, isIntro ); } }; getComponentConstructor = function ( root, name ) { // TODO... write this properly! return root.components[ name ]; }; (function () { var elementCache = {}; insertHtml = function ( html, tagName, docFrag ) { var container, nodes = []; container = elementCache[ tagName ] || ( elementCache[ tagName ] = doc.createElement( tagName ) ); container.innerHTML = html; while ( container.firstChild ) { nodes[ nodes.length ] = container.firstChild; docFrag.appendChild( container.firstChild ); } return nodes; }; }()); (function () { var reassignFragment, reassignElement, reassignMustache; reassignFragments = function ( root, section, start, end, by ) { var i, fragment, indexRef, oldIndex, newIndex, oldKeypath, newKeypath; indexRef = section.descriptor.i; for ( i=start; i<end; i+=1 ) { fragment = section.fragments[i]; oldIndex = i - by; newIndex = i; oldKeypath = section.keypath + '.' + ( i - by ); newKeypath = section.keypath + '.' + i; // change the fragment index fragment.index += by; reassignFragment( fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); } processDeferredUpdates( root ); }; reassignFragment = function ( fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ) { var i, item, context; if ( fragment.indexRefs && fragment.indexRefs[ indexRef ] !== undefined ) { fragment.indexRefs[ indexRef ] = newIndex; } // fix context stack i = fragment.contextStack.length; while ( i-- ) { context = fragment.contextStack[i]; if ( context.substr( 0, oldKeypath.length ) === oldKeypath ) { fragment.contextStack[i] = context.replace( oldKeypath, newKeypath ); } } i = fragment.items.length; while ( i-- ) { item = fragment.items[i]; switch ( item.type ) { case ELEMENT: reassignElement( item, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); break; case PARTIAL: reassignFragment( item.fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); break; case SECTION: case INTERPOLATOR: case TRIPLE: reassignMustache( item, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); break; } } }; reassignElement = function ( element, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ) { var i, attribute, storage, masterEventName, proxies, proxy; i = element.attributes.length; while ( i-- ) { attribute = element.attributes[i]; if ( attribute.fragment ) { reassignFragment( attribute.fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); if ( attribute.twoway ) { attribute.updateBindings(); } } } if ( storage = element.node._ractive ) { if ( storage.keypath.substr( 0, oldKeypath.length ) === oldKeypath ) { storage.keypath = storage.keypath.replace( oldKeypath, newKeypath ); } if ( indexRef !== undefined ) { storage.index[ indexRef ] = newIndex; } for ( masterEventName in storage.events ) { proxies = storage.events[ masterEventName ].proxies; i = proxies.length; while ( i-- ) { proxy = proxies[i]; if ( typeof proxy.n === 'object' ) { reassignFragment( proxy.a, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); } if ( proxy.d ) { reassignFragment( proxy.d, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); } } } if ( storage.binding ) { if ( storage.binding.keypath.substr( 0, oldKeypath.length ) === oldKeypath ) { storage.binding.keypath = storage.binding.keypath.replace( oldKeypath, newKeypath ); } } } // reassign children if ( element.fragment ) { reassignFragment( element.fragment, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); } }; reassignMustache = function ( mustache, indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ) { var i; // expression mustache? if ( mustache.descriptor.x ) { if ( mustache.keypath ) { unregisterDependant( mustache ); } if ( mustache.expressionResolver ) { mustache.expressionResolver.teardown(); } mustache.expressionResolver = new ExpressionResolver( mustache ); } // normal keypath mustache? if ( mustache.keypath ) { if ( mustache.keypath.substr( 0, oldKeypath.length ) === oldKeypath ) { mustache.resolve( mustache.keypath.replace( oldKeypath, newKeypath ) ); } } // index ref mustache? else if ( mustache.indexRef === indexRef ) { mustache.value = newIndex; mustache.render( newIndex ); } // otherwise, it's an unresolved reference. the context stack has been updated // so it will take care of itself // if it's a section mustache, we need to go through any children if ( mustache.fragments ) { i = mustache.fragments.length; while ( i-- ) { reassignFragment( mustache.fragments[i], indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); } } }; }()); (function ( cache ) { var Reference, SoftReference, getFunctionFromString, thisPattern, wrapFunction; Evaluator = function ( root, keypath, functionStr, args, priority ) { var i, arg; this.root = root; this.keypath = keypath; this.priority = priority; this.dependants = 0; this.fn = getFunctionFromString( functionStr, args.length ); this.values = []; this.refs = []; i = args.length; while ( i-- ) { if ( arg = args[i] ) { if ( arg[0] ) { // this is an index ref... we don't need to register a dependant this.values[i] = arg[1]; } else { this.refs[ this.refs.length ] = new Reference( root, arg[1], this, i, priority ); } } else { this.values[i] = undefined; } } this.selfUpdating = ( this.refs.length <= 1 ); }; Evaluator.prototype = { wake: function () { this.awake = true; this.update(); }, sleep: function () { this.awake = false; }, bubble: function () { if ( !this.awake ) { return; } // If we only have one reference, we can update immediately... if ( this.selfUpdating ) { this.update(); } // ...otherwise we want to register it as a deferred item, to be // updated once all the information is in, to prevent unnecessary // cascading. Only if we're already resolved, obviously else if ( !this.deferred ) { this.root._defEvals[ this.root._defEvals.length ] = this; this.deferred = true; } }, update: function () { var value; // prevent infinite loops if ( this.evaluating ) { return this; } this.evaluating = true; try { value = this.fn.apply( null, this.values ); } catch ( err ) { if ( this.root.debug ) { throw err; } else { value = undefined; } } if ( !isEqual( value, this.value ) ) { clearCache( this.root, this.keypath ); this.root._cache[ this.keypath ] = value; notifyDependants( this.root, this.keypath ); this.value = value; } this.evaluating = false; return this; }, // TODO should evaluators ever get torn down? At present, they don't... teardown: function () { while ( this.refs.length ) { this.refs.pop().teardown(); } clearCache( this.root, this.keypath ); this.root._evaluators[ this.keypath ] = null; }, // This method forces the evaluator to sync with the current model // in the case of a smart update refresh: function () { if ( !this.selfUpdating ) { this.deferred = true; } var i = this.refs.length; while ( i-- ) { this.refs[i].update(); } if ( this.deferred ) { this.update(); this.deferred = false; } }, updateSoftDependencies: function ( softDeps ) { var i, keypath, ref; if ( !this.softRefs ) { this.softRefs = []; } // teardown any references that are no longer relevant i = this.softRefs.length; while ( i-- ) { ref = this.softRefs[i]; if ( !softDeps[ ref.keypath ] ) { this.softRefs.splice( i, 1 ); this.softRefs[ ref.keypath ] = false; ref.teardown(); } } // add references for any new soft dependencies i = softDeps.length; while ( i-- ) { keypath = softDeps[i]; if ( !this.softRefs[ keypath ] ) { ref = new SoftReference( this.root, keypath, this ); this.softRefs[ this.softRefs.length ] = ref; this.softRefs[ keypath ] = true; } } this.selfUpdating = ( this.refs.length + this.softRefs.length <= 1 ); } }; Reference = function ( root, keypath, evaluator, argNum, priority ) { var value; this.evaluator = evaluator; this.keypath = keypath; this.root = root; this.argNum = argNum; this.type = REFERENCE; this.priority = priority; value = root.get( keypath ); if ( typeof value === 'function' ) { value = value._wrapped || wrapFunction( value, root, evaluator ); } this.value = evaluator.values[ argNum ] = value; registerDependant( this ); }; Reference.prototype = { update: function () { var value = this.root.get( this.keypath ); if ( typeof value === 'function' && !value._nowrap ) { value = value[ '_' + this.root._guid ] || wrapFunction( value, this.root, this.evaluator ); } if ( !isEqual( value, this.value ) ) { this.evaluator.values[ this.argNum ] = value; this.evaluator.bubble(); this.value = value; } }, teardown: function () { unregisterDependant( this ); } }; SoftReference = function ( root, keypath, evaluator ) { this.root = root; this.keypath = keypath; this.priority = evaluator.priority; this.evaluator = evaluator; registerDependant( this ); }; SoftReference.prototype = { update: function () { var value = this.root.get( this.keypath ); if ( !isEqual( value, this.value ) ) { this.evaluator.bubble(); this.value = value; } }, teardown: function () { unregisterDependant( this ); } }; getFunctionFromString = function ( str, i ) { var fn, args; str = str.replace( /\$\{([0-9]+)\}/g, '_$1' ); if ( cache[ str ] ) { return cache[ str ]; } args = []; while ( i-- ) { args[i] = '_' + i; } fn = new Function( args.join( ',' ), 'return(' + str + ')' ); cache[ str ] = fn; return fn; }; thisPattern = /this/; wrapFunction = function ( fn, ractive, evaluator ) { var prop; // if the function doesn't refer to `this`, we don't need // to set the context if ( !thisPattern.test( fn.toString() ) ) { defineProperty( fn, '_nowrap', { // no point doing this every time value: true }); return fn; } // otherwise, we do defineProperty( fn, '_' + ractive._guid, { value: function () { var originalGet, result, softDependencies; originalGet = ractive.get; ractive.get = function ( keypath ) { if ( !softDependencies ) { softDependencies = []; } if ( !softDependencies[ keypath ] ) { softDependencies[ softDependencies.length ] = keypath; softDependencies[ keypath ] = true; } return originalGet.call( ractive, keypath ); }; result = fn.apply( ractive, arguments ); if ( softDependencies ) { evaluator.updateSoftDependencies( softDependencies ); } // reset ractive.get = originalGet; return result; }, writable: true }); for ( prop in fn ) { if ( hasOwn.call( fn, prop ) ) { fn[ '_' + ractive._guid ][ prop ] = fn[ prop ]; } } return fn[ '_' + ractive._guid ]; }; }({})); (function () { var ReferenceScout, getKeypath; ExpressionResolver = function ( mustache ) { var expression, i, len, ref, indexRefs; this.root = mustache.root; this.mustache = mustache; this.args = []; this.scouts = []; expression = mustache.descriptor.x; indexRefs = mustache.parentFragment.indexRefs; this.str = expression.s; // send out scouts for each reference len = this.unresolved = this.args.length = ( expression.r ? expression.r.length : 0 ); if ( !len ) { this.resolved = this.ready = true; this.bubble(); // some expressions don't have references. edge case, but, yeah. return; } for ( i=0; i<len; i+=1 ) { ref = expression.r[i]; // is this an index ref? if ( indexRefs && indexRefs[ ref ] !== undefined ) { this.resolveRef( i, true, indexRefs[ ref ] ); } else { this.scouts[ this.scouts.length ] = new ReferenceScout( this, ref, mustache.contextStack, i ); } } this.ready = true; this.bubble(); }; ExpressionResolver.prototype = { bubble: function () { if ( !this.ready ) { return; } this.keypath = getKeypath( this.str, this.args ); this.createEvaluator(); this.mustache.resolve( this.keypath ); }, teardown: function () { while ( this.scouts.length ) { this.scouts.pop().teardown(); } }, resolveRef: function ( argNum, isIndexRef, value ) { this.args[ argNum ] = [ isIndexRef, value ]; this.bubble(); // when all references have been resolved, we can flag the entire expression // as having been resolved this.resolved = !( --this.unresolved ); }, createEvaluator: function () { // only if it doesn't exist yet! if ( !this.root._evaluators[ this.keypath ] ) { this.root._evaluators[ this.keypath ] = new Evaluator( this.root, this.keypath, this.str, this.args, this.mustache.priority ); } else { // we need to trigger a refresh of the evaluator, since it // will have become de-synced from the model if we're in a // reassignment cycle this.root._evaluators[ this.keypath ].refresh(); } } }; ReferenceScout = function ( resolver, ref, contextStack, argNum ) { var keypath, root; root = this.root = resolver.root; keypath = resolveRef( root, ref, contextStack ); if ( keypath ) { resolver.resolveRef( argNum, false, keypath ); } else { this.ref = ref; this.argNum = argNum; this.resolver = resolver; this.contextStack = contextStack; root._pendingResolution[ root._pendingResolution.length ] = this; } }; ReferenceScout.prototype = { resolve: function ( keypath ) { this.keypath = keypath; this.resolver.resolveRef( this.argNum, false, keypath ); }, teardown: function () { // if we haven't found a keypath yet, we can // stop the search now if ( !this.keypath ) { teardown( this ); } } }; getKeypath = function ( str, args ) { var unique; // get string that is unique to this expression unique = str.replace( /\$\{([0-9]+)\}/g, function ( match, $1 ) { return args[ $1 ] ? args[ $1 ][1] : 'undefined'; }); // then sanitize by removing any periods or square brackets. Otherwise // we can't split the keypath into keys! return '(' + unique.replace( /[\.\[\]]/g, '-' ) + ')'; }; }()); (function () { var getPartialFromRegistry, unpack; getPartialDescriptor = function ( root, name ) { var el, partial; // If the partial was specified on this instance, great if ( partial = getPartialFromRegistry( root, name ) ) { return partial; } // If not, is it a global partial? if ( partial = getPartialFromRegistry( Ractive, name ) ) { return partial; } // Does it exist on the page as a script tag? if ( doc ) { el = doc.getElementById( name ); if ( el && el.tagName === 'SCRIPT' ) { if ( !Ractive.parse ) { throw new Error( missingParser ); } Ractive.partials[ name ] = Ractive.parse( el.innerHTML ); } } partial = Ractive.partials[ name ]; // No match? Return an empty array if ( !partial ) { if ( root.debug && console && console.warn ) { console.warn( 'Could not find descriptor for partial "' + name + '"' ); } return []; } return unpack( partial ); }; getPartialFromRegistry = function ( registry, name ) { var partial, key; if ( registry.partials[ name ] ) { // If this was added manually to the registry, but hasn't been parsed, // parse it now if ( typeof registry.partials[ name ] === 'string' ) { if ( !Ractive.parse ) { throw new Error( missingParser ); } partial = Ractive.parse( registry.partials[ name ], registry.parseOptions ); if ( isObject( partial ) ) { registry.partials[ name ] = partial.main; for ( key in partial.partials ) { if ( partial.partials.hasOwnProperty( key ) ) { registry.partials[ key ] = partial.partials[ key ]; } } } else { registry.partials[ name ] = partial; } } return unpack( registry.partials[ name ] ); } }; unpack = function ( partial ) { // Unpack string, if necessary if ( partial.length === 1 && typeof partial[0] === 'string' ) { return partial[0]; } return partial; }; }()); initFragment = function ( fragment, options ) { var numItems, i, parentFragment, parentRefs, ref; // The item that owns this fragment - an element, section, partial, or attribute fragment.owner = options.owner; parentFragment = fragment.owner.parentFragment; // inherited properties fragment.root = options.root; fragment.parentNode = options.parentNode; fragment.contextStack = options.contextStack || []; // If parent item is a section, this may not be the only fragment // that belongs to it - we need to make a note of the index if ( fragment.owner.type === SECTION ) { fragment.index = options.index; } // index references (the 'i' in {{#section:i}}<!-- -->{{/section}}) need to cascade // down the tree if ( parentFragment ) { parentRefs = parentFragment.indexRefs; if ( parentRefs ) { fragment.indexRefs = createFromNull(); // avoids need for hasOwnProperty for ( ref in parentRefs ) { fragment.indexRefs[ ref ] = parentRefs[ ref ]; } } } // inherit priority fragment.priority = ( parentFragment ? parentFragment.priority + 1 : 1 ); if ( options.indexRef ) { if ( !fragment.indexRefs ) { fragment.indexRefs = {}; } fragment.indexRefs[ options.indexRef ] = options.index; } // Time to create this fragment's child items; fragment.items = []; numItems = ( options.descriptor ? options.descriptor.length : 0 ); for ( i=0; i<numItems; i+=1 ) { fragment.items[ fragment.items.length ] = fragment.createItem({ parentFragment: fragment, descriptor: options.descriptor[i], index: i }); } }; isStringFragmentSimple = function ( fragment ) { var i, item, containsInterpolator; i = fragment.items.length; while ( i-- ) { item = fragment.items[i]; if ( item.type === TEXT ) { continue; } // we can only have one interpolator and still be self-updating if ( item.type === INTERPOLATOR ) { if ( containsInterpolator ) { return false; } else { containsInterpolator = true; continue; } } // anything that isn't text or an interpolator (i.e. a section) // and we can't self-update return false; } return true; }; initMustache = function ( mustache, options ) { var keypath, indexRef, parentFragment; parentFragment = mustache.parentFragment = options.parentFragment; mustache.root = parentFragment.root; mustache.contextStack = parentFragment.contextStack; mustache.descriptor = options.descriptor; mustache.index = options.index || 0; mustache.priority = parentFragment.priority; // DOM only if ( parentFragment.parentNode ) { mustache.parentNode = parentFragment.parentNode; } mustache.type = options.descriptor.t; // if this is a simple mustache, with a reference, we just need to resolve // the reference to a keypath if ( options.descriptor.r ) { if ( parentFragment.indexRefs && parentFragment.indexRefs[ options.descriptor.r ] !== undefined ) { indexRef = parentFragment.indexRefs[ options.descriptor.r ]; mustache.indexRef = options.descriptor.r; mustache.value = indexRef; mustache.render( mustache.value ); } else { keypath = resolveRef( mustache.root, options.descriptor.r, mustache.contextStack ); if ( keypath ) { mustache.resolve( keypath ); } else { mustache.ref = options.descriptor.r; mustache.root._pendingResolution[ mustache.root._pendingResolution.length ] = mustache; // inverted section? initialise if ( mustache.descriptor.n ) { mustache.render( false ); } } } } // if it's an expression, we have a bit more work to do if ( options.descriptor.x ) { mustache.expressionResolver = new ExpressionResolver( mustache ); } }; // methods to add to individual mustache prototypes updateMustache = function () { var value = this.root.get( this.keypath, true ); if ( !isEqual( value, this.value ) ) { this.