UNPKG

gedi

Version:
418 lines (336 loc) 13 kB
var WeakMap = require('./weakmap'), paths = require('gedi-paths'), pathConstants = paths.constants modelOperations = require('./modelOperations'), get = modelOperations.get, set = modelOperations.set; var isBrowser = typeof Node != 'undefined'; module.exports = function(modelGet, gel, PathToken){ var modelBindings, modelBindingDetails, callbackReferenceDetails, modelReferences; function resetEvents(){ modelBindings = {}; modelBindingDetails = new WeakMap(); callbackReferenceDetails = new WeakMap(); modelReferences = new WeakMap(); } resetEvents(); function createGetValue(expression, parentPath){ return function(scope, returnAsTokens){ return modelGet(expression, parentPath, scope, returnAsTokens); } } function ModelEventEmitter(target){ this.model = modelGet(); this.events = {}; this.alreadyEmitted = {}; this.target = target; } ModelEventEmitter.prototype.pushPath = function(path, type, skipReferences){ var currentEvent = this.events[path]; if(!currentEvent || type === 'target' || type === 'keys'){ this.events[path] = type; } if(skipReferences){ return; } var modelValue = get(path, this.model), references = modelValue && typeof modelValue === 'object' && modelReferences.get(modelValue), referencePathParts, referenceBubblePath, pathParts = paths.toParts(path), targetParts = paths.toParts(this.target), referenceTarget; // If no references, or only in the model once // There are no reference events to fire. if(!references || Object.keys(references).length === 1){ return; } for(var key in references){ referencePathParts = paths.toParts(key); referenceTarget = paths.create(referencePathParts.concat(targetParts.slice(pathParts.length))); bubbleTrigger(referenceTarget, this, true); this.pushPath(referenceTarget, 'target', true); sinkTrigger(referenceTarget, this, true); } }; ModelEventEmitter.prototype.emit = function(){ var emitter = this, targetReference, referenceDetails; for(var path in this.events){ var type = this.events[path]; targetReference = get(path, modelBindings); referenceDetails = targetReference && modelBindingDetails.get(targetReference); if(!referenceDetails){ continue; } for(var i = 0; i < referenceDetails.length; i++) { var details = referenceDetails[i], binding = details.binding, wildcardPath = matchWildcardPath(binding, emitter.target, details.parentPath); // binding had wildcards but // did not match the current target if(wildcardPath === false){ continue; } details.callback({ target: emitter.target, binding: wildcardPath || details.binding, captureType: type, getValue: createGetValue(wildcardPath || details.binding, details.parentPath) }); }; } }; function sinkTrigger(path, emitter, skipReferences){ var reference = get(path, modelBindings); for(var key in reference){ var sinkPath = paths.append(path, key); emitter.pushPath(sinkPath, 'sink', skipReferences); sinkTrigger(sinkPath, emitter, skipReferences); } } function bubbleTrigger(path, emitter, skipReferences){ var pathParts = paths.toParts(path); for(var i = 0; i < pathParts.length - 1; i++){ emitter.pushPath( paths.create(pathParts.slice(0, i+1)), 'bubble', skipReferences ); } } function trigger(path, keysChange){ // resolve path to root path = paths.resolve(paths.createRoot(), path); var emitter = new ModelEventEmitter(path); bubbleTrigger(path, emitter); if(keysChange){ emitter.pushPath(paths.resolve(path ,pathConstants.upALevel), 'keys'); } emitter.pushPath(path, 'target'); sinkTrigger(path, emitter); emitter.emit(); } var memoisedExpressionPaths = {}; function getPathsInExpression(expression) { var paths = []; if(memoisedExpressionPaths[expression]){ return memoisedExpressionPaths[expression]; } if (gel) { var tokens = gel.tokenise(expression); for(var index = 0; index < tokens.length; index++){ var token = tokens[index]; if(token.path != null){ paths.push(token.path); } } } else { return memoisedExpressionPaths[expression] = [paths.create(expression)]; } return memoisedExpressionPaths[expression] = paths; } function addReferencesForBinding(path){ var model = modelGet(), pathParts = paths.toParts(path), itemPath = path, item = get(path, model); while(typeof item !== 'object' && pathParts.length){ pathParts.pop(); itemPath = paths.create(pathParts); item = get(itemPath, model); } addModelReference(itemPath, item); } function setBinding(path, details){ // Handle wildcards if(path.indexOf(pathConstants.wildcard)>=0){ var parts = paths.toParts(path); path = paths.create(parts.slice(0, parts.indexOf(pathConstants.wildcard))); } var resolvedPath = paths.resolve(paths.createRoot(), details.parentPath, path), reference = get(resolvedPath, modelBindings) || {}, referenceDetails = modelBindingDetails.get(reference), callbackReferences = callbackReferenceDetails.get(details.callback); if(!referenceDetails){ referenceDetails = []; modelBindingDetails.set(reference, referenceDetails); } if(!callbackReferences){ callbackReferences = []; callbackReferenceDetails.set(details.callback, callbackReferences); } callbackReferences.push(resolvedPath); referenceDetails.push(details); set(resolvedPath, reference, modelBindings); addReferencesForBinding(path); } function bindExpression(binding, details){ var expressionPaths = getPathsInExpression(binding), boundExpressions = {}; for(var i = 0; i < expressionPaths.length; i++) { var path = expressionPaths[i]; if(!boundExpressions[path]){ boundExpressions[path] = true; setBinding(path, details); } } } function bind(path, callback, parentPath, binding){ parentPath = parentPath || paths.create(); var details = { binding: binding || path, callback: callback, parentPath: parentPath }; // If the binding is a simple path, skip the more complex // expression path binding. if (paths.is(path)) { return setBinding(path, details); } bindExpression(path, details); } function matchWildcardPath(binding, target, parentPath){ if( binding.indexOf(pathConstants.wildcard) >= 0 && getPathsInExpression(binding)[0] === binding ){ //fully resolve the callback path var wildcardParts = paths.toParts(paths.resolve(paths.createRoot(), parentPath, binding)), targetParts = paths.toParts(target); for(var i = 0; i < wildcardParts.length; i++) { var pathPart = wildcardParts[i]; if(pathPart === pathConstants.wildcard){ wildcardParts[i] = targetParts[i]; }else if (pathPart !== targetParts[i]){ return false; } } return paths.create(wildcardParts); } } function debindPath(path, callback){ var targetReference = get(path, modelBindings), referenceDetails = modelBindingDetails.get(targetReference); if(referenceDetails){ for(var i = 0; i < referenceDetails.length; i++) { var details = referenceDetails[i]; if(!callback || callback === details.callback){ referenceDetails.splice(i, 1); i--; } } } } function debindCallbackPaths(path, callback, parentPath){ if(callback){ var references = callback && callbackReferenceDetails.get(callback), resolvedPath = paths.resolve(paths.createRoot(), parentPath, path); if(references){ for(var i = 0; i < references.length; i++) { if(path != null && references[i] !== resolvedPath){ continue; } debindPath(references.splice(i, 1)[0], callback); i--; } } return; } debindPath(path); } function debindExpression(binding, callback, parentPath){ var expressionPaths = getPathsInExpression(binding); for(var i = 0; i < expressionPaths.length; i++) { var path = expressionPaths[i]; debindCallbackPaths(path, callback, parentPath); } } function debind(expression, callback, parentPath){ // If you pass no path and no callback // You are trying to debind the entire gedi instance. if(!expression && !callback){ resetEvents(); return; } if(typeof expression === 'function'){ callback = expression; expression = null; } //If the expression is a simple path, skip the expression debind step. if (expression == null || paths.is(expression)) { return debindCallbackPaths(expression, callback, parentPath); } debindExpression(expression, callback, parentPath); } // Add a new object who's references should be tracked. function addModelReference(path, object){ if(!object || typeof object !== 'object'){ return; } if(!paths.isAbsolute(path)){ path = paths.resolve(paths.createRoot(),path); } var objectReferences = modelReferences.get(object); if(!objectReferences){ objectReferences = {}; modelReferences.set(object, objectReferences); } if(!(path in objectReferences)){ objectReferences[path] = null; } if(isBrowser && object instanceof Node){ return; } addModelReference(object.constructor.prototype); var keys = Object.keys(object); for(var i = 0; i < keys.length; i++){ var key = keys[i], prop = object[key]; // Faster to check again here than to create pointless paths. if(prop && typeof prop === 'object'){ var refPath = paths.append(path, key); if(modelReferences.has(prop)){ if(prop !== object){ modelReferences.get(prop)[refPath] = null; } }else{ addModelReference(refPath, prop); } } } } function removeModelReference(path, object){ if(!object || typeof object !== 'object'){ return; } var path = paths.resolve(paths.createRoot(),path), objectReferences = modelReferences.get(object), refIndex; if(!objectReferences){ return; } delete objectReferences[path]; if(!Object.keys(objectReferences).length){ modelReferences['delete'](object); } for(var key in object){ var prop = object[key]; // Faster to check again here than to create pointless paths. if(prop && typeof prop === 'object' && prop !== object){ removeModelReference(paths.append(path, paths.create(key)), prop); } } } return { bind: bind, trigger: trigger, debind: debind, addModelReference: addModelReference, removeModelReference: removeModelReference }; };