can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
678 lines (677 loc) • 30.4 kB
JavaScript
/*!
* CanJS - 2.3.34
* http://canjs.com/
* Copyright (c) 2018 Bitovi
* Mon, 30 Apr 2018 20:56:51 GMT
* Licensed MIT
*/
/*can@2.3.34#view/bindings/bindings*/
define([
'can/util/library',
'can/view/expression',
'can/view/callbacks',
'can/view/live',
'can/view/scope',
'can/view/href'
], function (can, expression, viewCallbacks, live) {
var behaviors = {
viewModel: function (el, tagData, makeViewModel, initialViewModelData) {
initialViewModelData = initialViewModelData || {};
var bindingsSemaphore = {}, viewModel, onCompleteBindings = [], onTeardowns = {}, bindingInfos = {}, attributeViewModelBindings = can.extend({}, initialViewModelData);
can.each(can.makeArray(el.attributes), function (node) {
var dataBinding = makeDataBinding(node, el, {
templateType: tagData.templateType,
scope: tagData.scope,
semaphore: bindingsSemaphore,
getViewModel: function () {
return viewModel;
},
attributeViewModelBindings: attributeViewModelBindings,
alreadyUpdatedChild: true,
nodeList: tagData.parentNodeList
});
if (dataBinding) {
if (dataBinding.onCompleteBinding) {
if (dataBinding.bindingInfo.parentToChild && dataBinding.value !== undefined) {
initialViewModelData[cleanVMName(dataBinding.bindingInfo.childName)] = dataBinding.value;
}
onCompleteBindings.push(dataBinding.onCompleteBinding);
}
onTeardowns[node.name] = dataBinding.onTeardown;
}
});
viewModel = makeViewModel(initialViewModelData);
for (var i = 0, len = onCompleteBindings.length; i < len; i++) {
onCompleteBindings[i]();
}
can.bind.call(el, 'attributes', function (ev) {
var attrName = ev.attributeName, value = el.getAttribute(attrName);
if (onTeardowns[attrName]) {
onTeardowns[attrName]();
}
var parentBindingWasAttribute = bindingInfos[attrName] && bindingInfos[attrName].parent === 'attribute';
if (value !== null || parentBindingWasAttribute) {
var dataBinding = makeDataBinding({
name: attrName,
value: value
}, el, {
templateType: tagData.templateType,
scope: tagData.scope,
semaphore: {},
getViewModel: function () {
return viewModel;
},
attributeViewModelBindings: attributeViewModelBindings,
initializeValues: true,
nodeList: tagData.parentNodeList
});
if (dataBinding) {
if (dataBinding.onCompleteBinding) {
dataBinding.onCompleteBinding();
}
bindingInfos[attrName] = dataBinding.bindingInfo;
onTeardowns[attrName] = dataBinding.onTeardown;
}
}
});
return function () {
for (var attrName in onTeardowns) {
onTeardowns[attrName]();
}
};
},
data: function (el, attrData) {
if (can.data(can.$(el), 'preventDataBindings')) {
return;
}
var viewModel = can.viewModel(el), semaphore = {}, teardown;
var dataBinding = makeDataBinding({
name: attrData.attributeName,
value: el.getAttribute(attrData.attributeName),
nodeList: attrData.nodeList
}, el, {
templateType: attrData.templateType,
scope: attrData.scope,
semaphore: semaphore,
getViewModel: function () {
return viewModel;
}
});
if (dataBinding.onCompleteBinding) {
dataBinding.onCompleteBinding();
}
teardown = dataBinding.onTeardown;
can.one.call(el, 'removed', function () {
teardown();
});
can.bind.call(el, 'attributes', function (ev) {
var attrName = ev.attributeName, value = el.getAttribute(attrName);
if (attrName === attrData.attributeName) {
if (teardown) {
teardown();
}
if (value !== null) {
var dataBinding = makeDataBinding({
name: attrName,
value: value
}, el, {
templateType: attrData.templateType,
scope: attrData.scope,
semaphore: semaphore,
getViewModel: function () {
return viewModel;
},
initializeValues: true,
nodeList: attrData.nodeList
});
if (dataBinding) {
if (dataBinding.onCompleteBinding) {
dataBinding.onCompleteBinding();
}
teardown = dataBinding.onTeardown;
}
}
}
});
},
reference: function (el, attrData) {
if (el.getAttribute(attrData.attributeName)) {
console.warn('*reference attributes can only export the view model.');
}
var name = can.camelize(attrData.attributeName.substr(1).toLowerCase());
var viewModel = can.viewModel(el);
var refs = attrData.scope.getRefs();
refs._context.attr('*' + name, viewModel);
},
event: function (el, data) {
var attributeName = data.attributeName, legacyBinding = attributeName.indexOf('can-') === 0, event = attributeName.indexOf('can-') === 0 ? attributeName.substr('can-'.length) : can.camelize(removeBrackets(attributeName, '(', ')')), onBindElement = legacyBinding;
if (event.charAt(0) === '$') {
event = event.substr(1);
onBindElement = true;
}
var handler = function (ev) {
var attrVal = el.getAttribute(attributeName);
if (!attrVal) {
return;
}
var $el = can.$(el), viewModel = can.viewModel($el[0]);
var expr = expression.parse(removeBrackets(attrVal), {
lookupRule: 'method',
methodRule: 'call'
});
if (!(expr instanceof expression.Call) && !(expr instanceof expression.Helper)) {
var defaultArgs = can.map([
data.scope._context,
$el
].concat(can.makeArray(arguments)), function (data) {
return new expression.Literal(data);
});
expr = new expression.Call(expr, defaultArgs, {});
}
var localScope = data.scope.add({
'@element': $el,
'@event': ev,
'@viewModel': viewModel,
'@scope': data.scope,
'@context': data.scope._context,
'%element': this,
'$element': $el,
'%event': ev,
'%viewModel': viewModel,
'%scope': data.scope,
'%context': data.scope._context
}, { notContext: true });
var scopeData = localScope.read(expr.methodExpr.key, { isArgument: true });
if (!scopeData.value) {
scopeData = localScope.read(expr.methodExpr.key, { isArgument: true });
return null;
}
var args = expr.args(localScope, null)();
return scopeData.value.apply(scopeData.parent, args);
};
if (special[event]) {
var specialData = special[event](data, el, handler);
handler = specialData.handler;
event = specialData.event;
}
can.bind.call(onBindElement ? el : can.viewModel(el), event, handler);
var attributesHandler = function (ev) {
if (ev.attributeName === attributeName && !this.getAttribute(attributeName)) {
can.unbind.call(onBindElement ? el : can.viewModel(el), event, handler);
can.unbind.call(el, 'attributes', attributesHandler);
}
};
can.bind.call(el, 'attributes', attributesHandler);
},
value: function (el, data) {
var propName = '$value', attrValue = can.trim(removeBrackets(el.getAttribute('can-value'))), getterSetter;
if (el.nodeName.toLowerCase() === 'input' && (el.type === 'checkbox' || el.type === 'radio')) {
var property = getComputeFrom.scope(el, data.scope, attrValue, {}, true);
if (el.type === 'checkbox') {
var trueValue = can.attr.has(el, 'can-true-value') ? el.getAttribute('can-true-value') : true, falseValue = can.attr.has(el, 'can-false-value') ? el.getAttribute('can-false-value') : false;
getterSetter = can.compute(function (newValue) {
if (arguments.length) {
property(newValue ? trueValue : falseValue);
} else {
return property() == trueValue;
}
});
} else if (el.type === 'radio') {
getterSetter = can.compute(function (newValue) {
if (arguments.length) {
if (newValue) {
property(el.value);
}
} else {
return property() == el.value;
}
});
}
propName = '$checked';
attrValue = 'getterSetter';
data.scope = new can.view.Scope({ getterSetter: getterSetter });
} else if (isContentEditable(el)) {
propName = '$innerHTML';
}
var dataBinding = makeDataBinding({
name: '{(' + propName + '})',
value: attrValue
}, el, {
templateType: data.templateType,
scope: data.scope,
semaphore: {},
initializeValues: true,
legacyBindings: true,
syncChildWithParent: true
});
can.one.call(el, 'removed', function () {
dataBinding.onTeardown();
});
}
};
can.view.attr(/^\{[^\}]+\}$/, behaviors.data);
can.view.attr(/\*[\w\.\-_]+/, behaviors.reference);
can.view.attr(/^\([\$?\w\.\-]+\)$/, behaviors.event);
can.view.attr(/can-[\w\.]+/, behaviors.event);
can.view.attr('can-value', behaviors.value);
var getComputeFrom = {
scope: function (el, scope, scopeProp, bindingData, mustBeACompute, stickyCompute) {
if (!scopeProp) {
return can.compute();
} else {
if (mustBeACompute) {
var parentExpression = expression.parse(scopeProp, { baseMethodType: 'Call' });
return parentExpression.value(scope, new can.view.Options({}));
} else {
return function (newVal) {
scope.attr(cleanVMName(scopeProp), newVal);
};
}
}
},
viewModel: function (el, scope, vmName, bindingData, mustBeACompute, stickyCompute) {
var setName = cleanVMName(vmName);
if (mustBeACompute) {
return can.compute(function (newVal) {
var viewModel = bindingData.getViewModel();
if (arguments.length) {
viewModel.attr(setName, newVal);
} else {
return vmName === '.' ? viewModel : can.compute.read(viewModel, can.compute.read.reads(vmName), {}).value;
}
});
} else {
return function (newVal) {
var childCompute;
var viewModel = bindingData.getViewModel();
if (stickyCompute) {
childCompute = viewModel._get(setName, { readCompute: false });
if (!childCompute || !childCompute.isComputed) {
childCompute = can.compute();
viewModel._set(setName, childCompute, { readCompute: false });
}
childCompute(newVal);
} else {
viewModel.attr(setName, newVal);
}
};
}
},
attribute: function (el, scope, prop, bindingData, mustBeACompute, stickyCompute, event) {
var hasChildren = el.nodeName.toLowerCase() === 'select', isMultiselectValue = prop === 'value' && hasChildren && el.multiple, isStringValue, lastSet, scheduledAsyncSet = false, timer, parentEvents, originalValue;
if (!event) {
if (prop === 'innerHTML') {
event = [
'blur',
'change'
];
} else {
event = 'change';
}
}
if (!can.isArray(event)) {
event = [event];
}
var set = function (newVal) {
if (hasChildren && !scheduledAsyncSet) {
clearTimeout(timer);
timer = setTimeout(function () {
set(newVal);
}, 1);
}
lastSet = newVal;
if (isMultiselectValue) {
if (newVal && typeof newVal === 'string') {
newVal = newVal.split(';');
isStringValue = true;
} else if (newVal) {
newVal = can.makeArray(newVal);
} else {
newVal = [];
}
var isSelected = {};
can.each(newVal, function (val) {
isSelected[val] = true;
});
can.each(el.childNodes, function (option) {
if (option.value) {
option.selected = !!isSelected[option.value];
}
});
} else {
if (!bindingData.legacyBindings && hasChildren && 'selectedIndex' in el && prop === 'value') {
can.attr.setSelectValue(el, newVal);
} else {
can.attr.setAttrOrProp(el, prop, newVal == null ? '' : newVal);
}
}
return newVal;
}, get = function () {
if (isMultiselectValue) {
var values = [], children = el.childNodes;
can.each(children, function (child) {
if (child.selected && child.value) {
values.push(child.value);
}
});
return isStringValue ? values.join(';') : values;
} else if (hasChildren && 'selectedIndex' in el && el.selectedIndex === -1) {
return undefined;
}
return can.attr.get(el, prop);
};
if (hasChildren) {
setTimeout(function () {
scheduledAsyncSet = true;
}, 1);
}
if (el.tagName && el.tagName.toLowerCase() === 'input' && el.form) {
parentEvents = [{
el: el.form,
eventName: 'reset',
handler: function () {
set(originalValue);
}
}];
}
var observer;
originalValue = get();
return can.compute(originalValue, {
on: function (updater) {
can.each(event, function (eventName) {
can.bind.call(el, eventName, updater);
});
can.each(parentEvents, function (parentEvent) {
can.bind.call(parentEvent.el, parentEvent.eventName, parentEvent.handler);
});
if (hasChildren) {
var onMutation = function (mutations) {
if (stickyCompute) {
set(stickyCompute());
}
updater();
};
if (can.attr.MutationObserver) {
observer = new can.attr.MutationObserver(onMutation);
observer.observe(el, {
childList: true,
subtree: true
});
} else {
can.data(can.$(el), 'canBindingCallback', { onMutation: onMutation });
}
}
},
off: function (updater) {
can.each(event, function (eventName) {
can.unbind.call(el, eventName, updater);
});
can.each(parentEvents, function (parentEvent) {
can.unbind.call(parentEvent.el, parentEvent.eventName, parentEvent.handler);
});
if (hasChildren) {
if (can.attr.MutationObserver) {
observer.disconnect();
} else {
can.data(can.$(el), 'canBindingCallback', null);
}
}
},
get: get,
set: set
});
}
};
var bind = {
childToParent: function (el, parentCompute, childCompute, bindingsSemaphore, attrName, syncChild) {
var parentUpdateIsFunction = typeof parentCompute === 'function';
var updateParent = function (ev, newVal) {
if (!bindingsSemaphore[attrName]) {
if (parentUpdateIsFunction) {
parentCompute(newVal);
if (syncChild) {
if (parentCompute() !== childCompute()) {
bindingsSemaphore[attrName] = (bindingsSemaphore[attrName] || 0) + 1;
can.batch.start();
childCompute(parentCompute());
can.batch.after(function () {
--bindingsSemaphore[attrName];
});
can.batch.stop();
}
}
} else if (parentCompute instanceof can.Map) {
parentCompute.attr(newVal, true);
}
}
};
if (childCompute && childCompute.isComputed) {
childCompute.bind('change', updateParent);
}
return updateParent;
},
parentToChild: function (el, parentCompute, childUpdate, bindingsSemaphore, attrName) {
var updateChild = function (ev, newValue) {
bindingsSemaphore[attrName] = (bindingsSemaphore[attrName] || 0) + 1;
can.batch.start();
childUpdate(newValue);
can.batch.after(function () {
--bindingsSemaphore[attrName];
});
can.batch.stop();
};
if (parentCompute && parentCompute.isComputed) {
parentCompute.bind('change', updateChild);
}
return updateChild;
}
};
var getBindingInfo = function (node, attributeViewModelBindings, templateType, tagName) {
var bindingInfo, attributeName = node.name, attributeValue = node.value || '';
var matches = attributeName.match(bindingsRegExp);
if (!matches) {
var ignoreAttribute = ignoreAttributesRegExp.test(attributeName);
var vmName = can.camelize(attributeName);
if (ignoreAttribute || viewCallbacks.attr(attributeName)) {
return;
}
var syntaxRight = attributeValue[0] === '{' && can.last(attributeValue) === '}';
var isAttributeToChild = templateType === 'legacy' ? attributeViewModelBindings[vmName] : !syntaxRight;
var scopeName = syntaxRight ? attributeValue.substr(1, attributeValue.length - 2) : attributeValue;
if (isAttributeToChild) {
return {
bindingAttributeName: attributeName,
parent: 'attribute',
parentName: attributeName,
child: 'viewModel',
childName: vmName,
parentToChild: true,
childToParent: true
};
} else {
return {
bindingAttributeName: attributeName,
parent: 'scope',
parentName: scopeName,
child: 'viewModel',
childName: vmName,
parentToChild: true,
childToParent: true
};
}
}
var twoWay = !!matches[1], childToParent = twoWay || !!matches[2], parentToChild = twoWay || !childToParent;
var childName = matches[3];
var isDOM = childName.charAt(0) === '$';
if (isDOM) {
bindingInfo = {
parent: 'scope',
child: 'attribute',
childToParent: childToParent,
parentToChild: parentToChild,
bindingAttributeName: attributeName,
childName: childName.substr(1),
parentName: attributeValue,
initializeValues: true
};
if (tagName === 'select') {
bindingInfo.stickyParentToChild = true;
}
return bindingInfo;
} else {
bindingInfo = {
parent: 'scope',
child: 'viewModel',
childToParent: childToParent,
parentToChild: parentToChild,
bindingAttributeName: attributeName,
childName: can.camelize(childName),
parentName: attributeValue,
initializeValues: true
};
if (attributeValue.trim().charAt(0) === '~') {
bindingInfo.stickyParentToChild = true;
}
return bindingInfo;
}
};
var bindingsRegExp = /\{(\()?(\^)?([^\}\)]+)\)?\}/, ignoreAttributesRegExp = /^(data-view-id|class|id|\[[\w\.-]+\]|#[\w\.-])$/i;
var makeDataBinding = function (node, el, bindingData) {
var bindingInfo = getBindingInfo(node, bindingData.attributeViewModelBindings, bindingData.templateType, el.nodeName.toLowerCase());
if (!bindingInfo) {
return;
}
bindingInfo.alreadyUpdatedChild = bindingData.alreadyUpdatedChild;
if (bindingData.initializeValues) {
bindingInfo.initializeValues = true;
}
var parentCompute = getComputeFrom[bindingInfo.parent](el, bindingData.scope, bindingInfo.parentName, bindingData, bindingInfo.parentToChild), childCompute = getComputeFrom[bindingInfo.child](el, bindingData.scope, bindingInfo.childName, bindingData, bindingInfo.childToParent, bindingInfo.stickyParentToChild && parentCompute), updateParent, updateChild, childLifecycle;
if (bindingData.nodeList) {
if (parentCompute && parentCompute.isComputed) {
parentCompute.computeInstance.setPrimaryDepth(bindingData.nodeList.nesting + 1);
}
if (childCompute && childCompute.isComputed) {
childCompute.computeInstance.setPrimaryDepth(bindingData.nodeList.nesting + 1);
}
}
if (bindingInfo.parentToChild) {
updateChild = bind.parentToChild(el, parentCompute, childCompute, bindingData.semaphore, bindingInfo.bindingAttributeName);
}
var completeBinding = function () {
if (bindingInfo.childToParent) {
updateParent = bind.childToParent(el, parentCompute, childCompute, bindingData.semaphore, bindingInfo.bindingAttributeName, bindingData.syncChildWithParent);
} else if (bindingInfo.stickyParentToChild) {
childCompute.bind('change', childLifecycle = can.k);
}
if (bindingInfo.initializeValues) {
initializeValues(bindingInfo, childCompute, parentCompute, updateChild, updateParent);
}
};
var onTeardown = function () {
unbindUpdate(parentCompute, updateChild);
unbindUpdate(childCompute, updateParent);
unbindUpdate(childCompute, childLifecycle);
};
if (bindingInfo.child === 'viewModel') {
return {
value: getValue(parentCompute),
onCompleteBinding: completeBinding,
bindingInfo: bindingInfo,
onTeardown: onTeardown
};
} else {
completeBinding();
return {
bindingInfo: bindingInfo,
onTeardown: onTeardown
};
}
};
var initializeValues = function (bindingInfo, childCompute, parentCompute, updateChild, updateParent) {
var doUpdateParent = false;
if (bindingInfo.parentToChild && !bindingInfo.childToParent) {
if (bindingInfo.stickyParentToChild) {
updateChild({}, getValue(parentCompute));
}
} else if (!bindingInfo.parentToChild && bindingInfo.childToParent) {
doUpdateParent = true;
} else if (getValue(childCompute) === undefined) {
} else if (getValue(parentCompute) === undefined) {
doUpdateParent = true;
}
if (doUpdateParent) {
updateParent({}, getValue(childCompute));
} else {
if (!bindingInfo.alreadyUpdatedChild) {
updateChild({}, getValue(parentCompute));
}
}
};
if (!can.attr.MutationObserver) {
var updateSelectValue = function (el) {
var bindingCallback = can.data(can.$(el), 'canBindingCallback');
if (bindingCallback) {
bindingCallback.onMutation(el);
}
};
live.registerChildMutationCallback('select', updateSelectValue);
live.registerChildMutationCallback('optgroup', function (el) {
updateSelectValue(el.parentNode);
});
}
var isContentEditable = function () {
var values = {
'': true,
'true': true,
'false': false
};
var editable = function (el) {
if (!el || !el.getAttribute) {
return;
}
var attr = el.getAttribute('contenteditable');
return values[attr];
};
return function (el) {
var val = editable(el);
if (typeof val === 'boolean') {
return val;
} else {
return !!editable(el.parentNode);
}
};
}(), removeBrackets = function (value, open, close) {
open = open || '{';
close = close || '}';
if (value[0] === open && value[value.length - 1] === close) {
return value.substr(1, value.length - 2);
}
return value;
}, getValue = function (value) {
return value && value.isComputed ? value() : value;
}, unbindUpdate = function (compute, updateOther) {
if (compute && compute.isComputed && typeof updateOther === 'function') {
compute.unbind('change', updateOther);
}
}, cleanVMName = function (name) {
return name.replace(/@/g, '');
};
var special = {
enter: function (data, el, original) {
return {
event: 'keyup',
handler: function (ev) {
if (ev.keyCode === 13) {
return original.call(this, ev);
}
}
};
}
};
can.bindings = {
behaviors: behaviors,
getBindingInfo: getBindingInfo,
special: special
};
return can.bindings;
});