/* freezer-js v0.14.0 (2-8-2018) * https://github.com/arqex/freezer * By arqex * License: MIT */ (function(root, factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } else { root.Freezer = factory(); } }(this, function() { 'use strict'; var global = typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : {}; var Utils = { extend: function( ob, props ){ for( var p in props ){ ob[p] = props[p]; } return ob; }, createNonEnumerable: function( obj, proto ){ var ne = {}; for( var key in obj ) ne[key] = {value: obj[key] }; return Object.create( proto || {}, ne ); }, error: function( message ){ var err = new Error( message ); if( console ) return console.error( err ); else throw err; }, each: function( o, clbk ){ var i,l,keys; if( o && o.constructor === Array ){ for (i = 0, l = o.length; i < l; i++) clbk( o[i], i ); } else { keys = Object.keys( o ); for( i = 0, l = keys.length; i < l; i++ ) clbk( o[ keys[i] ], keys[i] ); } }, addNE: function( node, attrs ){ for( var key in attrs ){ Object.defineProperty( node, key, { enumerable: false, configurable: true, writable: true, value: attrs[ key ] }); } }, /** * Creates non-enumerable property descriptors, to be used by Object.create. * @param {Object} attrs Properties to create descriptors * @return {Object} A hash with the descriptors. */ createNE: function( attrs ){ var ne = {}; for( var key in attrs ){ ne[ key ] = { writable: true, configurable: true, enumerable: false, value: attrs[ key ] } } return ne; }, // nextTick - by stagas / public domain nextTick: (function () { var queue = [], dirty = false, fn, hasPostMessage = !!global.postMessage && (typeof Window !== 'undefined') && (global instanceof Window), messageName = 'fzr' + Date.now(), trigger = (function () { return hasPostMessage ? function trigger () { global.postMessage(messageName, '*'); } : function trigger () { setTimeout(function () { processQueue() }, 0); }; }()), processQueue = (function () { return hasPostMessage ? function processQueue (event) { if (event.data === messageName) { event.stopPropagation(); flushQueue(); } } : flushQueue; })() ; function flushQueue () { dirty = false; while (fn = queue.shift()) { fn(); } } function nextTick (fn) { queue.push(fn); if (dirty) return; dirty = true; trigger(); } if (hasPostMessage) global.addEventListener('message', processQueue, true); nextTick.removeListener = function () { global.removeEventListener('message', processQueue, true); } return nextTick; })(), findPivot: function( node ){ if( !node || !node.__ ) return; if( node.__.pivot ) return node; var found = 0, parents = node.__.parents, i = 0, parent ; // Look up for the pivot in the parents while( !found && i < parents.length ){ parent = parents[i]; if( parent.__.pivot ) found = parent; i++; } if( found ){ return found; } // If not found, try with the parent's parents i=0; while( !found && i < parents.length ){ found = this.findPivot( parents[i] ); i++; } return found; }, isLeaf: function( node, freezeInstances ){ var cons; return !node || !(cons = node.constructor) || (freezeInstances ? (cons === String || cons === Number || cons === Boolean) : (cons !== Object && cons !== Array) ); }, warn: function(){ var args; if( typeof process === 'undefined' || process.env.NODE_ENV !== 'production' ){ if( !arguments[0] && typeof console !== 'undefined' ){ args = Array.prototype.slice.call( arguments, 1 ); args[0] = 'Freezer.js WARNING: ' + args[0]; console.warn.apply( console, args ); } } } }; var nodeCreator = { init: function( Frozen ){ var commonMethods = { set: function( attr, value ){ var attrs = attr, update = this.__.trans, isArray = this.constructor === Array, msg = 'Freezer arrays only accept numeric attributes, given: ' ; if( typeof attr !== 'object' ){ if( isArray && parseInt(attr) != attr ){ Utils.warn( 0, msg + attr ); return Utils.findPivot( this ) || this; } attrs = {}; attrs[ attr ] = value; } if( !update ){ for( var key in attrs ){ if( isArray && parseInt(key) != key ){ Utils.warn( 0, msg + key ); return Utils.findPivot( this ) || this; } else { update = update || this[ key ] !== attrs[ key ]; } } // No changes, just return the node if( !update ) return Utils.findPivot( this ) || this; } var name = isArray ? 'array.set' : 'object.set'; return this.__.store.notify( 'merge', this, attrs, name ); }, reset: function( attrs ) { return this.__.store.notify( 'replace', this, attrs, 'object.replace' ); }, getListener: function(){ return Frozen.createListener( this ); }, toJS: function(){ var js; if( this.constructor === Array ){ js = new Array( this.length ); } else { js = {}; } Utils.each( this, function( child, i ){ if( child && child.__ ) js[ i ] = child.toJS(); else js[ i ] = child; }); return js; }, transact: function(){ return this.__.store.notify( 'transact', this ); }, run: function(){ return this.__.store.notify( 'run', this ); }, now: function(){ return this.__.store.notify( 'now', this ); }, pivot: function(){ return this.__.store.notify( 'pivot', this ); } }; var arrayMethods = Utils.extend({ push: function( el ){ return this.append( [el], 'array.push' ); }, append: function( els, name ){ if( els && els.length ) return this.__.store.notify( 'splice', this, [this.length, 0].concat( els ), name || 'array.append' ); return this; }, pop: function(){ if( !this.length ) return this; return this.__.store.notify( 'splice', this, [this.length -1, 1], 'array.pop' ); }, unshift: function( el ){ return this.prepend( [el], 'array.unshift' ); }, prepend: function( els ){ if( els && els.length ) return this.__.store.notify( 'splice', this, [0, 0].concat( els ), 'array.prepend' ); return this; }, shift: function(){ if( !this.length ) return this; return this.__.store.notify( 'splice', this, [0, 1], 'array.shift' ); }, splice: function( index, toRemove, toAdd ){ return this.__.store.notify( 'splice', this, arguments, 'array.splice' ); }, sort: function(){ var mutable = this.slice(); mutable.sort.apply(mutable, arguments); return this.__.store.notify( 'replace', this, mutable, 'array.sort' ); } }, commonMethods ); var FrozenArray = Object.create( Array.prototype, Utils.createNE( arrayMethods ) ); var objectMethods = Utils.createNE( Utils.extend({ remove: function( keys ){ var filtered = [], k = keys ; if( keys.constructor !== Array ) k = [ keys ]; for( var i = 0, l = k.length; i= 0; i--) { if( listeners[i].callback === listener ) listeners.splice( i, 1 ); } } return this; }, emit: function( eventName ){ var args = [].slice.call( arguments, 1 ), listeners = this._events[ eventName ] || [], onceListeners = [], special = specialEvents.indexOf( eventName ) !== -1, i, listener, returnValue, lastValue ; special || this.emit.apply( this, [BEFOREALL, eventName].concat( args ) ); // Call listeners for (i = 0; i < listeners.length; i++) { listener = listeners[i]; if( listener.callback ) lastValue = listener.callback.apply( this, args ); else { // If there is not a callback, remove! listener.once = true; } if( listener.once ) onceListeners.push( i ); if( lastValue !== undefined ){ returnValue = lastValue; } } // Remove listeners marked as once for( i = onceListeners.length - 1; i >= 0; i-- ){ listeners.splice( onceListeners[i], 1 ); } special || this.emit.apply( this, [AFTERALL, eventName].concat( args ) ); return returnValue; }, trigger: function(){ Utils.warn( false, 'Method `trigger` is deprecated and will be removed from freezer in upcoming releases. Please use `emit`.' ); return this.emit.apply( this, arguments ); } }; // Methods are not enumerable so, when the stores are // extended with the emitter, they can be iterated as // hashmaps var Emitter = Utils.createNonEnumerable( emitterProto ); var Frozen = { freeze: function( node, store ){ if( node && node.__ ){ return node; } var me = this, frozen = nodeCreator.clone(node) ; Utils.addNE( frozen, { __: { listener: false, parents: [], store: store }}); // Freeze children Utils.each( node, function( child, key ){ if( !Utils.isLeaf( child, store.freezeInstances ) ){ child = me.freeze( child, store ); } if( child && child.__ ){ me.addParent( child, frozen ); } frozen[ key ] = child; }); store.freezeFn( frozen ); return frozen; }, merge: function( node, attrs ){ var _ = node.__, trans = _.trans, // Clone the attrs to not modify the argument attrs = Utils.extend( {}, attrs) ; if( trans ){ for( var attr in attrs ) trans[ attr ] = attrs[ attr ]; return node; } var me = this, frozen = this.copyMeta( node ), store = _.store, val, key, isFrozen ; Utils.each( node, function( child, key ){ isFrozen = child && child.__; if( isFrozen ){ me.removeParent( child, node ); } val = attrs[ key ]; if( !val ){ if( isFrozen ) me.addParent( child, frozen ); return frozen[ key ] = child; } if( !Utils.isLeaf( val, store.freezeInstances ) ) val = me.freeze( val, store ); if( val && val.__ ) me.addParent( val, frozen ); delete attrs[ key ]; frozen[ key ] = val; }); for( key in attrs ) { val = attrs[ key ]; if( !Utils.isLeaf( val, store.freezeInstances ) ) val = me.freeze( val, store ); if( val && val.__ ) me.addParent( val, frozen ); frozen[ key ] = val; } _.store.freezeFn( frozen ); this.refreshParents( node, frozen ); return frozen; }, replace: function( node, replacement ) { var me = this, _ = node.__, frozen = replacement ; if( !Utils.isLeaf( replacement, _.store.freezeInstances ) ) { frozen = me.freeze( replacement, _.store ); frozen.__.parents = _.parents; frozen.__.updateRoot = _.updateRoot; // Add the current listener if exists, replacing a // previous listener in the frozen if existed if( _.listener ) frozen.__.listener = _.listener; } if( frozen ){ this.fixChildren( frozen, node ); } this.refreshParents( node, frozen ); return frozen; }, remove: function( node, attrs ){ var trans = node.__.trans; if( trans ){ for( var l = attrs.length - 1; l >= 0; l-- ) delete trans[ attrs[l] ]; return node; } var me = this, frozen = this.copyMeta( node ), isFrozen ; Utils.each( node, function( child, key ){ isFrozen = child && child.__; if( isFrozen ){ me.removeParent( child, node ); } if( attrs.indexOf( key ) !== -1 ){ return; } if( isFrozen ) me.addParent( child, frozen ); frozen[ key ] = child; }); node.__.store.freezeFn( frozen ); this.refreshParents( node, frozen ); return frozen; }, splice: function( node, args ){ var _ = node.__, trans = _.trans ; if( trans ){ trans.splice.apply( trans, args ); return node; } var me = this, frozen = this.copyMeta( node ), index = args[0], deleteIndex = index + args[1], child ; // Clone the array Utils.each( node, function( child, i ){ if( child && child.__ ){ me.removeParent( child, node ); // Skip the nodes to delete if( i < index || i>= deleteIndex ) me.addParent( child, frozen ); } frozen[i] = child; }); // Prepare the new nodes if( args.length > 1 ){ for (var i = args.length - 1; i >= 2; i--) { child = args[i]; if( !Utils.isLeaf( child, _.store.freezeInstances ) ) child = this.freeze( child, _.store ); if( child && child.__ ) this.addParent( child, frozen ); args[i] = child; } } // splice Array.prototype.splice.apply( frozen, args ); _.store.freezeFn( frozen ); this.refreshParents( node, frozen ); return frozen; }, transact: function( node ) { var me = this, transacting = node.__.trans, trans ; if( transacting ) return transacting; trans = node.constructor === Array ? [] : {}; Utils.each( node, function( child, key ){ trans[ key ] = child; }); node.__.trans = trans; // Call run automatically in case // the user forgot about it Utils.nextTick( function(){ if( node.__.trans ) me.run( node ); }); return trans; }, run: function( node ) { var me = this, trans = node.__.trans ; if( !trans ) return node; // Remove the node as a parent Utils.each( trans, function( child, key ){ if( child && child.__ ){ me.removeParent( child, node ); } }); delete node.__.trans; var result = this.replace( node, trans ); return result; }, pivot: function( node ){ node.__.pivot = 1; this.unpivot( node ); return node; }, unpivot: function( node ){ Utils.nextTick( function(){ node.__.pivot = 0; }); }, refresh: function( node, oldChild, newChild ){ var me = this, trans = node.__.trans, found = 0 ; if( trans ){ Utils.each( trans, function( child, key ){ if( found ) return; if( child === oldChild ){ trans[ key ] = newChild; found = 1; if( newChild && newChild.__ ) me.addParent( newChild, node ); } }); return node; } var frozen = this.copyMeta( node ), replacement, __ ; Utils.each( node, function( child, key ){ if( child === oldChild ){ child = newChild; } if( child && (__ = child.__) ){ me.removeParent( child, node ); me.addParent( child, frozen ); } frozen[ key ] = child; }); node.__.store.freezeFn( frozen ); this.refreshParents( node, frozen ); }, fixChildren: function( node, oldNode ){ var me = this; Utils.each( node, function( child ){ if( !child || !child.__ ) return; // Update parents in all children no matter the child // is linked to the node or not. me.fixChildren( child ); if( child.__.parents.length === 1 ) return child.__.parents = [ node ]; if( oldNode ) me.removeParent( child, oldNode ); me.addParent( child, node ); }); }, copyMeta: function( node ){ var me = this, frozen = nodeCreator.clone( node ), _ = node.__ ; Utils.addNE( frozen, {__: { store: _.store, updateRoot: _.updateRoot, listener: _.listener, parents: _.parents.slice( 0 ), trans: _.trans, pivot: _.pivot, }}); if( _.pivot ) this.unpivot( frozen ); return frozen; }, refreshParents: function( oldChild, newChild ){ var _ = oldChild.__, parents = _.parents.length, i ; if( oldChild.__.updateRoot ){ oldChild.__.updateRoot( oldChild, newChild ); } if( newChild ){ this.emit( oldChild, 'update', newChild, _.store.live ); } if( parents ){ for (i = parents - 1; i >= 0; i--) { this.refresh( _.parents[i], oldChild, newChild ); } } }, removeParent: function( node, parent ){ var parents = node.__.parents, index = parents.indexOf( parent ) ; if( index !== -1 ){ parents.splice( index, 1 ); } }, addParent: function( node, parent ){ var parents = node.__.parents, index = parents.indexOf( parent ) ; if( index === -1 ){ if(node.__.store.singleParent && parents.length >= 1){ throw new Error("Freezer: Can't add node to the tree. It's already added and freezer is configured to `singleParent: true`."); } parents[ parents.length ] = parent; } }, emit: function( node, eventName, param, now ){ var listener = node.__.listener; if( !listener ) return; var ticking = listener.ticking; if( now ){ if( ticking || param ){ listener.ticking = 0; listener.emit( eventName, ticking || param, node ); } return; } listener.ticking = param; if( !listener.prevState ){ listener.prevState = node; } if( !ticking ){ Utils.nextTick( function(){ if( listener.ticking ){ var updated = listener.ticking, prevState = listener.prevState ; listener.ticking = 0; listener.prevState = 0; listener.emit( eventName, updated, node ); } }); } }, createListener: function( frozen ){ var l = frozen.__.listener; if( !l ) { l = Object.create(Emitter, { _events: { value: {}, writable: true } }); frozen.__.listener = l; } return l; } }; nodeCreator.init( Frozen ); var Freezer = function( initialValue, options ) { var me = this, ops = options || {}, store = { live: ops.live || false, freezeInstances: ops.freezeInstances || false, singleParent: ops.singleParent || false } ; // Immutable data var frozen; var pivotTriggers = [], pivotTicking = 0; var triggerNow = function( node ){ var _ = node.__, i ; if( _.listener ){ var prevState = _.listener.prevState || node; _.listener.prevState = 0; Frozen.emit( prevState, 'update', node, true ); } for (i = 0; i < _.parents.length; i++) { _.store.notify( 'now', _.parents[i] ); } }; var addToPivotTriggers = function( node ){ pivotTriggers.push( node ); if( !pivotTicking ){ pivotTicking = 1; Utils.nextTick( function(){ pivotTriggers = []; pivotTicking = 0; }); } }; // Last call to display info about orphan calls var lastCall; store.notify = function notify( eventName, node, options, name ){ if( name ){ if( lastCall && !lastCall.onStore ){ detachedWarn( lastCall ); } lastCall = {name: name, node: node, options: options, onStore: false}; } if( eventName === 'now' ){ if( pivotTriggers.length ){ while( pivotTriggers.length ){ triggerNow( pivotTriggers.shift() ); } } else { triggerNow( node ); } return node; } var update = Frozen[eventName]( node, options ); if( eventName !== 'pivot' ){ var pivot = Utils.findPivot( update ); if( pivot ) { addToPivotTriggers( update ); return pivot; } } return update; }; store.freezeFn = ops.mutable === true ? function(){} : function( obj ){ Object.freeze( obj ); } ; // Create the frozen object frozen = Frozen.freeze( initialValue, store ); frozen.__.updateRoot = function( prevNode, updated ){ if( prevNode === frozen ){ frozen = updated; if( lastCall ){ lastCall.onStore = true; } } else if( lastCall ) { setTimeout( function(){ if( !lastCall.onStore ){ detachedWarn( lastCall ); } }); } } // Listen to its changes immediately var listener = frozen.getListener(), hub = {} ; Utils.each(['on', 'off', 'once', 'emit', 'trigger'], function( method ){ var attrs = {}; attrs[ method ] = listener[method].bind(listener); Utils.addNE( me, attrs ); Utils.addNE( hub, attrs ); }); Utils.addNE( this, { get: function(){ return frozen; }, set: function( node ){ frozen.reset( node ); }, getEventHub: function(){ return hub; } }); Utils.addNE( this, { getData: this.get, setData: this.set } ); }; function detachedWarn( lastCall ){ Utils.warn( false, 'Method ' + lastCall.name + ' called on a detached node.', lastCall.node, lastCall.options ); } return Freezer; }));