mirror of
https://github.com/Febbweiss/febbweiss.github.io.git
synced 2026-03-05 06:35:50 +00:00
302 lines
17 KiB
JavaScript
302 lines
17 KiB
JavaScript
(function () {
|
|
var _templateEngine;
|
|
ko.setTemplateEngine = function (templateEngine) {
|
|
if ((templateEngine != undefined) && !(templateEngine instanceof ko.templateEngine))
|
|
throw new Error("templateEngine must inherit from ko.templateEngine");
|
|
_templateEngine = templateEngine;
|
|
}
|
|
|
|
function invokeForEachNodeInContinuousRange(firstNode, lastNode, action) {
|
|
var node, nextInQueue = firstNode, firstOutOfRangeNode = ko.virtualElements.nextSibling(lastNode);
|
|
while (nextInQueue && ((node = nextInQueue) !== firstOutOfRangeNode)) {
|
|
nextInQueue = ko.virtualElements.nextSibling(node);
|
|
action(node, nextInQueue);
|
|
}
|
|
}
|
|
|
|
function activateBindingsOnContinuousNodeArray(continuousNodeArray, bindingContext) {
|
|
// To be used on any nodes that have been rendered by a template and have been inserted into some parent element
|
|
// Walks through continuousNodeArray (which *must* be continuous, i.e., an uninterrupted sequence of sibling nodes, because
|
|
// the algorithm for walking them relies on this), and for each top-level item in the virtual-element sense,
|
|
// (1) Does a regular "applyBindings" to associate bindingContext with this node and to activate any non-memoized bindings
|
|
// (2) Unmemoizes any memos in the DOM subtree (e.g., to activate bindings that had been memoized during template rewriting)
|
|
|
|
if (continuousNodeArray.length) {
|
|
var firstNode = continuousNodeArray[0],
|
|
lastNode = continuousNodeArray[continuousNodeArray.length - 1],
|
|
parentNode = firstNode.parentNode,
|
|
provider = ko.bindingProvider['instance'],
|
|
preprocessNode = provider['preprocessNode'];
|
|
|
|
if (preprocessNode) {
|
|
invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node, nextNodeInRange) {
|
|
var nodePreviousSibling = node.previousSibling;
|
|
var newNodes = preprocessNode.call(provider, node);
|
|
if (newNodes) {
|
|
if (node === firstNode)
|
|
firstNode = newNodes[0] || nextNodeInRange;
|
|
if (node === lastNode)
|
|
lastNode = newNodes[newNodes.length - 1] || nodePreviousSibling;
|
|
}
|
|
});
|
|
|
|
// Because preprocessNode can change the nodes, including the first and last nodes, update continuousNodeArray to match.
|
|
// We need the full set, including inner nodes, because the unmemoize step might remove the first node (and so the real
|
|
// first node needs to be in the array).
|
|
continuousNodeArray.length = 0;
|
|
if (!firstNode) { // preprocessNode might have removed all the nodes, in which case there's nothing left to do
|
|
return;
|
|
}
|
|
if (firstNode === lastNode) {
|
|
continuousNodeArray.push(firstNode);
|
|
} else {
|
|
continuousNodeArray.push(firstNode, lastNode);
|
|
ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode);
|
|
}
|
|
}
|
|
|
|
// Need to applyBindings *before* unmemoziation, because unmemoization might introduce extra nodes (that we don't want to re-bind)
|
|
// whereas a regular applyBindings won't introduce new memoized nodes
|
|
invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
|
|
if (node.nodeType === 1 || node.nodeType === 8)
|
|
ko.applyBindings(bindingContext, node);
|
|
});
|
|
invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
|
|
if (node.nodeType === 1 || node.nodeType === 8)
|
|
ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]);
|
|
});
|
|
|
|
// Make sure any changes done by applyBindings or unmemoize are reflected in the array
|
|
ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode);
|
|
}
|
|
}
|
|
|
|
function getFirstNodeFromPossibleArray(nodeOrNodeArray) {
|
|
return nodeOrNodeArray.nodeType ? nodeOrNodeArray
|
|
: nodeOrNodeArray.length > 0 ? nodeOrNodeArray[0]
|
|
: null;
|
|
}
|
|
|
|
function executeTemplate(targetNodeOrNodeArray, renderMode, template, bindingContext, options) {
|
|
options = options || {};
|
|
var firstTargetNode = targetNodeOrNodeArray && getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
|
|
var templateDocument = (firstTargetNode || template || {}).ownerDocument;
|
|
var templateEngineToUse = (options['templateEngine'] || _templateEngine);
|
|
ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse, templateDocument);
|
|
var renderedNodesArray = templateEngineToUse['renderTemplate'](template, bindingContext, options, templateDocument);
|
|
|
|
// Loosely check result is an array of DOM nodes
|
|
if ((typeof renderedNodesArray.length != "number") || (renderedNodesArray.length > 0 && typeof renderedNodesArray[0].nodeType != "number"))
|
|
throw new Error("Template engine must return an array of DOM nodes");
|
|
|
|
var haveAddedNodesToParent = false;
|
|
switch (renderMode) {
|
|
case "replaceChildren":
|
|
ko.virtualElements.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray);
|
|
haveAddedNodesToParent = true;
|
|
break;
|
|
case "replaceNode":
|
|
ko.utils.replaceDomNodes(targetNodeOrNodeArray, renderedNodesArray);
|
|
haveAddedNodesToParent = true;
|
|
break;
|
|
case "ignoreTargetNode": break;
|
|
default:
|
|
throw new Error("Unknown renderMode: " + renderMode);
|
|
}
|
|
|
|
if (haveAddedNodesToParent) {
|
|
activateBindingsOnContinuousNodeArray(renderedNodesArray, bindingContext);
|
|
if (options['afterRender'])
|
|
ko.dependencyDetection.ignore(options['afterRender'], null, [renderedNodesArray, bindingContext['$data']]);
|
|
}
|
|
|
|
return renderedNodesArray;
|
|
}
|
|
|
|
function resolveTemplateName(template, data, context) {
|
|
// The template can be specified as:
|
|
if (ko.isObservable(template)) {
|
|
// 1. An observable, with string value
|
|
return template();
|
|
} else if (typeof template === 'function') {
|
|
// 2. A function of (data, context) returning a string
|
|
return template(data, context);
|
|
} else {
|
|
// 3. A string
|
|
return template;
|
|
}
|
|
}
|
|
|
|
ko.renderTemplate = function (template, dataOrBindingContext, options, targetNodeOrNodeArray, renderMode) {
|
|
options = options || {};
|
|
if ((options['templateEngine'] || _templateEngine) == undefined)
|
|
throw new Error("Set a template engine before calling renderTemplate");
|
|
renderMode = renderMode || "replaceChildren";
|
|
|
|
if (targetNodeOrNodeArray) {
|
|
var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
|
|
|
|
var whenToDispose = function () { return (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); }; // Passive disposal (on next evaluation)
|
|
var activelyDisposeWhenNodeIsRemoved = (firstTargetNode && renderMode == "replaceNode") ? firstTargetNode.parentNode : firstTargetNode;
|
|
|
|
return ko.dependentObservable( // So the DOM is automatically updated when any dependency changes
|
|
function () {
|
|
// Ensure we've got a proper binding context to work with
|
|
var bindingContext = (dataOrBindingContext && (dataOrBindingContext instanceof ko.bindingContext))
|
|
? dataOrBindingContext
|
|
: new ko.bindingContext(dataOrBindingContext, null, null, null, { "exportDependencies": true });
|
|
|
|
var templateName = resolveTemplateName(template, bindingContext['$data'], bindingContext),
|
|
renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
|
|
|
|
if (renderMode == "replaceNode") {
|
|
targetNodeOrNodeArray = renderedNodesArray;
|
|
firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
|
|
}
|
|
},
|
|
null,
|
|
{ disposeWhen: whenToDispose, disposeWhenNodeIsRemoved: activelyDisposeWhenNodeIsRemoved }
|
|
);
|
|
} else {
|
|
// We don't yet have a DOM node to evaluate, so use a memo and render the template later when there is a DOM node
|
|
return ko.memoization.memoize(function (domNode) {
|
|
ko.renderTemplate(template, dataOrBindingContext, options, domNode, "replaceNode");
|
|
});
|
|
}
|
|
};
|
|
|
|
ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode, parentBindingContext) {
|
|
// Since setDomNodeChildrenFromArrayMapping always calls executeTemplateForArrayItem and then
|
|
// activateBindingsCallback for added items, we can store the binding context in the former to use in the latter.
|
|
var arrayItemContext;
|
|
|
|
// This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
|
|
var executeTemplateForArrayItem = function (arrayValue, index) {
|
|
// Support selecting template as a function of the data being rendered
|
|
arrayItemContext = parentBindingContext['createChildContext'](arrayValue, options['as'], function(context) {
|
|
context['$index'] = index;
|
|
});
|
|
|
|
var templateName = resolveTemplateName(template, arrayValue, arrayItemContext);
|
|
return executeTemplate(null, "ignoreTargetNode", templateName, arrayItemContext, options);
|
|
}
|
|
|
|
// This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode
|
|
var activateBindingsCallback = function(arrayValue, addedNodesArray, index) {
|
|
activateBindingsOnContinuousNodeArray(addedNodesArray, arrayItemContext);
|
|
if (options['afterRender'])
|
|
options['afterRender'](addedNodesArray, arrayValue);
|
|
|
|
// release the "cache" variable, so that it can be collected by
|
|
// the GC when its value isn't used from within the bindings anymore.
|
|
arrayItemContext = null;
|
|
};
|
|
|
|
return ko.dependentObservable(function () {
|
|
var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || [];
|
|
if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
|
|
unwrappedArray = [unwrappedArray];
|
|
|
|
// Filter out any entries marked as destroyed
|
|
var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
|
|
return options['includeDestroyed'] || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
|
|
});
|
|
|
|
// Call setDomNodeChildrenFromArrayMapping, ignoring any observables unwrapped within (most likely from a callback function).
|
|
// If the array items are observables, though, they will be unwrapped in executeTemplateForArrayItem and managed within setDomNodeChildrenFromArrayMapping.
|
|
ko.dependencyDetection.ignore(ko.utils.setDomNodeChildrenFromArrayMapping, null, [targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback]);
|
|
|
|
}, null, { disposeWhenNodeIsRemoved: targetNode });
|
|
};
|
|
|
|
var templateComputedDomDataKey = ko.utils.domData.nextKey();
|
|
function disposeOldComputedAndStoreNewOne(element, newComputed) {
|
|
var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
|
|
if (oldComputed && (typeof(oldComputed.dispose) == 'function'))
|
|
oldComputed.dispose();
|
|
ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && newComputed.isActive()) ? newComputed : undefined);
|
|
}
|
|
|
|
ko.bindingHandlers['template'] = {
|
|
'init': function(element, valueAccessor) {
|
|
// Support anonymous templates
|
|
var bindingValue = ko.utils.unwrapObservable(valueAccessor());
|
|
if (typeof bindingValue == "string" || bindingValue['name']) {
|
|
// It's a named template - clear the element
|
|
ko.virtualElements.emptyNode(element);
|
|
} else if ('nodes' in bindingValue) {
|
|
// We've been given an array of DOM nodes. Save them as the template source.
|
|
// There is no known use case for the node array being an observable array (if the output
|
|
// varies, put that behavior *into* your template - that's what templates are for), and
|
|
// the implementation would be a mess, so assert that it's not observable.
|
|
var nodes = bindingValue['nodes'] || [];
|
|
if (ko.isObservable(nodes)) {
|
|
throw new Error('The "nodes" option must be a plain, non-observable array.');
|
|
}
|
|
var container = ko.utils.moveCleanedNodesToContainerElement(nodes); // This also removes the nodes from their current parent
|
|
new ko.templateSources.anonymousTemplate(element)['nodes'](container);
|
|
} else {
|
|
// It's an anonymous template - store the element contents, then clear the element
|
|
var templateNodes = ko.virtualElements.childNodes(element),
|
|
container = ko.utils.moveCleanedNodesToContainerElement(templateNodes); // This also removes the nodes from their current parent
|
|
new ko.templateSources.anonymousTemplate(element)['nodes'](container);
|
|
}
|
|
return { 'controlsDescendantBindings': true };
|
|
},
|
|
'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
|
|
var value = valueAccessor(),
|
|
options = ko.utils.unwrapObservable(value),
|
|
shouldDisplay = true,
|
|
templateComputed = null,
|
|
templateName;
|
|
|
|
if (typeof options == "string") {
|
|
templateName = value;
|
|
options = {};
|
|
} else {
|
|
templateName = options['name'];
|
|
|
|
// Support "if"/"ifnot" conditions
|
|
if ('if' in options)
|
|
shouldDisplay = ko.utils.unwrapObservable(options['if']);
|
|
if (shouldDisplay && 'ifnot' in options)
|
|
shouldDisplay = !ko.utils.unwrapObservable(options['ifnot']);
|
|
}
|
|
|
|
if ('foreach' in options) {
|
|
// Render once for each data point (treating data set as empty if shouldDisplay==false)
|
|
var dataArray = (shouldDisplay && options['foreach']) || [];
|
|
templateComputed = ko.renderTemplateForEach(templateName || element, dataArray, options, element, bindingContext);
|
|
} else if (!shouldDisplay) {
|
|
ko.virtualElements.emptyNode(element);
|
|
} else {
|
|
// Render once for this single data point (or use the viewModel if no data was provided)
|
|
var innerBindingContext = ('data' in options) ?
|
|
bindingContext.createStaticChildContext(options['data'], options['as']) : // Given an explitit 'data' value, we create a child binding context for it
|
|
bindingContext; // Given no explicit 'data' value, we retain the same binding context
|
|
templateComputed = ko.renderTemplate(templateName || element, innerBindingContext, options, element);
|
|
}
|
|
|
|
// It only makes sense to have a single template computed per element (otherwise which one should have its output displayed?)
|
|
disposeOldComputedAndStoreNewOne(element, templateComputed);
|
|
}
|
|
};
|
|
|
|
// Anonymous templates can't be rewritten. Give a nice error message if you try to do it.
|
|
ko.expressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
|
|
var parsedBindingValue = ko.expressionRewriting.parseObjectLiteral(bindingValue);
|
|
|
|
if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown'])
|
|
return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting)
|
|
|
|
if (ko.expressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
|
|
return null; // Named templates can be rewritten, so return "no error"
|
|
return "This template engine does not support anonymous templates nested within its templates";
|
|
};
|
|
|
|
ko.virtualElements.allowedBindings['template'] = true;
|
|
})();
|
|
|
|
ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine);
|
|
ko.exportSymbol('renderTemplate', ko.renderTemplate);
|