gedi
Version:
An evented data API
418 lines (336 loc) • 13 kB
JavaScript
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
};
};