UNPKG

ractive

Version:

Next-generation DOM manipulation

2,118 lines (1,633 loc) 224 kB
(function ( global ) { 'use strict'; var Ractive, // current version VERSION = '0.3.6', 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 splitKeypath, 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 caches keypathCache = {}, // 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, UNSET = { unset: true }, 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 { Object.defineProperty({}, 'test', { value: 0 }); Object.defineProperties({}, { test: { value: 0 } }); 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 ] ); } } }; noMagic = true; } 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; } }()); (function ( win ) { var doc = win.document; if ( !doc ) { return; } // Shims for older browsers if ( !Date.now ) { Date.now = function () { return +new Date(); }; } if ( doc && !doc.createElementNS ) { doc.createElementNS = function ( ns, type ) { if ( ns && ns !== 'http://www.w3.org/1999/xhtml' ) { throw 'This browser does not support namespaces other than http://www.w3.org/1999/xhtml'; } return doc.createElement( type ); }; } if ( !String.prototype.trim ) { String.prototype.trim = function () { return this.replace(/^\s+/, '').replace(/\s+$/, ''); }; } // 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 ( hasOwn.call( this, i ) && this[i] === needle ) { return i; } } return -1; }; } if ( !Array.prototype.forEach ) { Array.prototype.forEach = function ( callback, context ) { var i, len; for ( i=0, len=this.length; i<len; i+=1 ) { if ( hasOwn.call( this, i ) ) { callback.call( context, this[i], i, this ); } } }; } if ( !Array.prototype.map ) { Array.prototype.map = function ( mapper, context ) { var i, len, mapped = []; for ( i=0, len=this.length; i<len; i+=1 ) { if ( hasOwn.call( this, i ) ) { mapped[i] = mapper.call( context, this[i], i, this ); } } return mapped; }; } if ( !Array.prototype.filter ) { Array.prototype.filter = function ( filter, context ) { var i, len, filtered = []; for ( i=0, len=this.length; i<len; i+=1 ) { if ( hasOwn.call( this, i ) && filter.call( context, this[i], i, this ) ) { filtered[ filtered.length ] = this[i]; } } return filtered; }; } // https://gist.github.com/Rich-Harris/6010282 via https://gist.github.com/jonathantneal/2869388 // addEventListener polyfill IE6+ if ( !win.addEventListener ) { (function ( win, doc ) { var Event, addEventListener, removeEventListener, head, style; Event = function ( e, element ) { var property, instance = this; for ( property in e ) { instance[ property ] = e[ property ]; } instance.currentTarget = element; instance.target = e.srcElement || element; instance.timeStamp = +new Date(); instance.preventDefault = function () { e.returnValue = false; }; instance.stopPropagation = function () { e.cancelBubble = true; }; }; addEventListener = function ( type, listener ) { var element = this, listeners, i; listeners = element.listeners || ( element.listeners = [] ); i = listeners.length; listeners[i] = [ listener, function (e) { listener.call( element, new Event( e, element ) ); }]; element.attachEvent( 'on' + type, listeners[i][1] ); }; removeEventListener = function ( type, listener ) { var element = this, listeners, i; if ( !element.listeners ) { return; } listeners = element.listeners; i = listeners.length; while ( i-- ) { if (listeners[i][0] === listener) { element.detachEvent( 'on' + type, listeners[i][1] ); } } }; win.addEventListener = doc.addEventListener = addEventListener; win.removeEventListener = doc.removeEventListener = removeEventListener; if ( 'Element' in win ) { Element.prototype.addEventListener = addEventListener; Element.prototype.removeEventListener = removeEventListener; } else { head = doc.getElementsByTagName('head')[0]; style = doc.createElement('style'); head.insertBefore( style, head.firstChild ); style.styleSheet.cssText = '*{-ms-event-prototype:expression(!this.addEventListener&&(this.addEventListener=addEventListener)&&(this.removeEventListener=removeEventListener))}'; } }( win, doc )); } }( global )); (function () { var getInterpolator, updateModel, getBinding, inheritProperties, arrayContentsMatch, MultipleSelectBinding, SelectBinding, RadioNameBinding, CheckboxNameBinding, CheckedBinding, FileListBinding, GenericBinding; bindAttribute = function () { var node = this.parentNode, interpolator, binding; 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; //this.updateModel = getUpdater( this ); binding = getBinding( this ); if ( !binding ) { return false; } node._ractive.binding = binding; this.twoway = true; return true; }; 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 ) { inheritProperties( this, attribute, node ); node.addEventListener( 'change', updateModel, false ); }; MultipleSelectBinding.prototype = { update: function () { var attribute, value, selectedOptions, i, previousValue, changed, len; attribute = this.attr; previousValue = attribute.value || []; value = []; selectedOptions = this.node.querySelectorAll( 'option:checked' ); len = selectedOptions.length; for ( i=0; i<len; i+=1 ) { value[ value.length ] = selectedOptions[i]._ractive.value; } // has the selection changed? changed = ( len !== previousValue.length ); i = value.length; while ( i-- ) { if ( value[i] !== previousValue[i] ) { changed = true; } } if ( changed = true ) { 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 ) { inheritProperties( this, attribute, node ); node.addEventListener( 'change', updateModel, false ); }; SelectBinding.prototype = { update: function () { var selectedOption, value; selectedOption = this.node.querySelector( 'option:checked' ); if ( !selectedOption ) { return; } value = selectedOption._ractive.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 ) { inheritProperties( this, attribute, node ); node.name = '{{' + attribute.keypath + '}}'; node.addEventListener( 'change', updateModel, false ); if ( node.attachEvent ) { node.addEventListener( 'click', updateModel, false ); } }; RadioNameBinding.prototype = { update: function () { var node = this.node; if ( node.checked ) { this.attr.receiving = true; this.root.set( this.keypath, node._ractive ? node._ractive.value : node.value ); this.attr.receiving = false; } }, teardown: function () { this.node.removeEventListener( 'change', updateModel, false ); this.node.removeEventListener( 'click', updateModel, false ); } }; CheckboxNameBinding = function ( attribute, node ) { inheritProperties( this, attribute, node ); node.name = '{{' + this.keypath + '}}'; this.query = 'input[type="checkbox"][name="' + node.name + '"]'; node.addEventListener( 'change', updateModel, false ); if ( node.attachEvent ) { node.addEventListener( 'click', updateModel, false ); } }; CheckboxNameBinding.prototype = { update: function () { var previousValue, value, checkboxes, len, i, checkbox; previousValue = this.root.get( this.keypath ); // TODO is this overkill? checkboxes = this.root.el.querySelectorAll( this.query ); len = checkboxes.length; value = []; for ( i=0; i<len; i+=1 ) { checkbox = checkboxes[i]; if ( checkbox.checked ) { value[ value.length ] = checkbox._ractive.value; } } if ( !arrayContentsMatch( previousValue, value ) ) { this.attr.receiving = true; this.root.set( this.keypath, value ); 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 = { update: function () { this.attr.receiving = true; this.root.set( this.keypath, this.node.checked ); 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 = { update: function () { this.attr.root.set( this.attr.keypath, this.attr.parentNode.files ); }, 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 = { update: function () { var attribute = this.attr, value = attribute.parentNode.value; // if the value is numeric, treat it as a number. otherwise don't if ( ( +value + '' === value ) && value.indexOf( 'e' ) === -1 ) { value = +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; }; arrayContentsMatch = function ( a, b ) { var i; if ( !isArray( a ) || !isArray( b ) ) { return false; } if ( a.length !== b.length ) { return false; } i = a.length; while ( i-- ) { if ( a[i] !== b[i] ) { return false; } } return true; }; }()); (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.querySelectorAll( 'option' ); 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.querySelectorAll( 'option' ); 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() ) { element.node._ractive.binding.update(); 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 ]; }; insertHtml = function ( html, docFrag ) { var div, nodes = []; div = doc.createElement( 'div' ); div.innerHTML = html; while ( div.firstChild ) { nodes[ nodes.length ] = div.firstChild; docFrag.appendChild( div.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; 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(); } } } // reassign proxy argument fragments TODO and intro/outro param fragments if ( element.proxyFrags ) { i = element.proxyFrags.length; while ( i-- ) { reassignFragment( element.proxyFrags[i], indexRef, oldIndex, newIndex, by, oldKeypath, newKeypath ); } } if ( element.node._ractive ) { if ( element.node._ractive.keypath.substr( 0, oldKeypath.length ) === oldKeypath ) { element.node._ractive.keypath = element.node._ractive.keypath.replace( oldKeypath, newKeypath ); } if ( indexRef !== undefined ) { element.node._ractive.index[ indexRef ] = newIndex; } } // 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 ) { unregisterDependant( mustache ); mustache.keypath = mustache.keypath.replace( oldKeypath, newKeypath ); registerDependant( mustache ); } } // 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, getFunctionFromString, thisPattern, wrapFunction; Evaluator = function ( root, keypath, functionStr, args, priority ) { var i, arg; this.root = root; this.keypath = keypath; this.fn = getFunctionFromString( functionStr, args.length ); this.values = []; this.refs = []; i = args.length; while ( i-- ) { 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 ); } } this.selfUpdating = ( this.refs.length <= 1 ); }; Evaluator.prototype = { bubble: function () { // If we only have one reference, we can update immediately... if ( this.selfUpdating ) { this.update(); } // ...otherwise we want to register it as a deferred item, to be // updated once all the information is in, to prevent unnecessary // cascading. Only if we're already 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? 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; } } }; 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 ); } this.value = evaluator.values[ argNum ] = value; registerDependant( this ); }; Reference.prototype = { update: function () { var value = this.root.get( this.keypath ); if ( typeof value === 'function' ) { value = value._wrapped || wrapFunction( value, this.root ); } if ( !isEqual( value, this.value ) ) { this.evaluator.values[ this.argNum ] = 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 ) { var prop; // if the function doesn't refer to `this`, we don't need // to set the context if ( !thisPattern.test( fn.toString() ) ) { return fn._wrapped = fn; } // otherwise, we do defineProperty( fn, '_wrapped', { value: function () { return fn.apply( ractive, arguments ); }, writable: true }); for ( prop in fn ) { if ( hasOwn.call( fn, prop ) ) { fn._wrapped[ prop ] = fn[ prop ]; } } return fn._wrapped; }; }({})); (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 = ( expression.r ? expression.r.length : 0 ); if ( !len ) { this.init(); // some expressions don't have references. edge case, but, yeah. } 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 ); } } }; ExpressionResolver.prototype = { init: function () { 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 ]; // can we initialise yet? if ( --this.unresolved ) { // no; return; } this.init(); }, 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 ); // initialise this.root._evaluators[ this.keypath ].update(); } 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 ][1]; }); // then sanitize by removing any periods or square brackets. Otherwise // splitKeypath will go mental! 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 ) { 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 ); } registry.partials[ name ] = Ractive.parse( registry.partials[ name ] ); } 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 : 0 ); 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; value = this.root.get( this.keypath, true ); if ( !isEqual( value, this.value ) ) { this.render( value ); this.value = value; } }; resolveMustache = function ( keypath ) { this.keypath = keypath; registerDependant( this ); this.update(); if ( this.expressionResolver ) { this.expressionResolver = null; } }; (function () { var updateListSection, updateListObjectSection, updateContextSection, updateConditionalSection; updateSection = function ( section, value ) { var fragmentOptions; fragmentOptions = { descriptor: section.descriptor.f, root: section.root, parentNode: section.parentNode, owner: section }; // if section is inverted, only check for truthiness/falsiness if ( section.descriptor.n ) { updateConditionalSection( section, value, true, fragmentOptions ); return; } // otherwise we need to work out what sort of section we're dealing with // if value is an array, or an object with an index reference, iterate through if ( isArray( value ) ) { updateListSection( section, value, fragmentOptions ); } // if value is a hash... else if ( isObject( value ) ) { if ( section.descriptor.i ) { updateListObjectSection( section, value, fr