infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
970 lines (883 loc) • 129 kB
JavaScript
/*
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.
You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
*/
var fluid_3_0_0 = fluid_3_0_0 || {};
(function ($, fluid) {
"use strict";
/** NOTE: The contents of this file are by default NOT PART OF THE PUBLIC FLUID API unless explicitly annotated before the function **/
/* The Fluid "IoC System proper" - resolution of references and
* completely automated instantiation of declaratively defined
* component trees */
// Currently still uses manual traversal - once we ban manually instantiated components,
// it will use the instantiator's records instead.
fluid.visitComponentChildren = function (that, visitor, options, segs) {
segs = segs || [];
for (var name in that) {
var component = that[name];
// This entire algorithm is primitive and expensive and will be removed once we can abolish manual init components
if (!fluid.isComponent(component) || (options.visited && options.visited[component.id])) {
continue;
}
segs.push(name);
if (options.visited) { // recall that this is here because we may run into a component that has been cross-injected which might otherwise cause cyclicity
options.visited[component.id] = true;
}
if (visitor(component, name, segs, segs.length - 1)) {
return true;
}
if (!options.flat) {
fluid.visitComponentChildren(component, visitor, options, segs);
}
segs.pop();
}
};
fluid.getContextHash = function (instantiator, that) {
var shadow = instantiator.idToShadow[that.id];
return shadow && shadow.contextHash;
};
fluid.componentHasGrade = function (that, gradeName) {
var contextHash = fluid.getContextHash(fluid.globalInstantiator, that);
return !!(contextHash && contextHash[gradeName]);
};
// A variant of fluid.visitComponentChildren that supplies the signature expected for fluid.matchIoCSelector
// this is: thatStack, contextHashes, memberNames, i - note, the supplied arrays are NOT writeable and shared through the iteration
fluid.visitComponentsForMatching = function (that, options, visitor) {
var instantiator = fluid.getInstantiator(that);
options = $.extend({
visited: {},
instantiator: instantiator
}, options);
var thatStack = [that];
var contextHashes = [fluid.getContextHash(instantiator, that)];
var visitorWrapper = function (component, name, segs) {
thatStack.length = 1;
contextHashes.length = 1;
for (var i = 0; i < segs.length; ++i) {
var child = thatStack[i][segs[i]];
thatStack[i + 1] = child;
contextHashes[i + 1] = fluid.getContextHash(instantiator, child) || {};
}
return visitor(component, thatStack, contextHashes, segs, segs.length);
};
fluid.visitComponentChildren(that, visitorWrapper, options, []);
};
fluid.getMemberNames = function (instantiator, thatStack) {
if (thatStack.length === 0) { // Odd edge case for FLUID-6126 from fluid.computeDistributionPriority
return [];
} else {
var path = instantiator.idToPath(thatStack[thatStack.length - 1].id);
var segs = instantiator.parseEL(path);
// TODO: we should now have no longer shortness in the stack
segs.unshift.apply(segs, fluid.generate(thatStack.length - segs.length, ""));
return segs;
}
};
// thatStack contains an increasing list of MORE SPECIFIC thats.
// this visits all components starting from the current location (end of stack)
// in visibility order UP the tree.
fluid.visitComponentsForVisibility = function (instantiator, thatStack, visitor, options) {
options = options || {
visited: {},
flat: true,
instantiator: instantiator
};
var memberNames = fluid.getMemberNames(instantiator, thatStack);
for (var i = thatStack.length - 1; i >= 0; --i) {
var that = thatStack[i];
// explicitly visit the direct parent first
options.visited[that.id] = true;
if (visitor(that, memberNames[i], memberNames, i)) {
return;
}
if (fluid.visitComponentChildren(that, visitor, options, memberNames)) {
return;
}
memberNames.pop();
}
};
fluid.mountStrategy = function (prefix, root, toMount) {
var offset = prefix.length;
return function (target, name, i, segs) {
if (i <= prefix.length) { // Avoid OOB to not trigger deoptimisation!
return;
}
for (var j = 0; j < prefix.length; ++j) {
if (segs[j] !== prefix[j]) {
return;
}
}
return toMount(target, name, i - prefix.length, segs.slice(offset));
};
};
fluid.invokerFromRecord = function (invokerec, name, that) {
fluid.pushActivity("makeInvoker", "beginning instantiation of invoker with name %name and record %record as child of %that",
{name: name, record: invokerec, that: that});
var invoker = invokerec ? fluid.makeInvoker(that, invokerec, name) : undefined;
fluid.popActivity();
return invoker;
};
fluid.memberFromRecord = function (memberrecs, name, that) {
var togo;
for (var i = 0; i < memberrecs.length; ++i) { // memberrecs is the special "fluid.mergingArray" type which is not Arrayable
var expanded = fluid.expandImmediate(memberrecs[i], that);
if (!fluid.isPlainObject(togo)) { // poor man's "merge" algorithm to hack FLUID-5668 for now
togo = expanded;
} else {
togo = $.extend(true, togo, expanded);
}
}
return togo;
};
fluid.recordStrategy = function (that, options, optionsStrategy, recordPath, recordMaker, prefix, exceptions) {
prefix = prefix || [];
return {
strategy: function (target, name, i) {
if (i !== 1) {
return;
}
var record = fluid.driveStrategy(options, [recordPath, name], optionsStrategy);
if (record === undefined) {
return;
}
fluid.set(target, [name], fluid.inEvaluationMarker);
var member = recordMaker(record, name, that);
fluid.set(target, [name], member);
return member;
},
initter: function () {
var records = fluid.driveStrategy(options, recordPath, optionsStrategy) || {};
for (var name in records) {
if (!exceptions || !exceptions[name]) {
fluid.getForComponent(that, prefix.concat([name]));
}
}
}
};
};
// patch Fluid.js version for timing
fluid.instantiateFirers = function (that) {
var shadow = fluid.shadowForComponent(that);
var initter = fluid.get(shadow, ["eventStrategyBlock", "initter"]) || fluid.identity;
initter();
};
fluid.makeDistributionRecord = function (contextThat, sourceRecord, sourcePath, targetSegs, exclusions, sourceType) {
sourceType = sourceType || "distribution";
fluid.pushActivity("makeDistributionRecord", "Making distribution record from source record %sourceRecord path %sourcePath to target path %targetSegs", {sourceRecord: sourceRecord, sourcePath: sourcePath, targetSegs: targetSegs});
var source = fluid.copy(fluid.get(sourceRecord, sourcePath));
fluid.each(exclusions, function (exclusion) {
fluid.model.applyChangeRequest(source, {segs: exclusion, type: "DELETE"});
});
var record = {options: {}};
fluid.model.applyChangeRequest(record, {segs: targetSegs, type: "ADD", value: source});
fluid.checkComponentRecord(record);
fluid.popActivity();
return $.extend(record, {contextThat: contextThat, recordType: sourceType});
};
// Part of the early "distributeOptions" workflow. Given the description of the blocks to be distributed, assembles "canned" records
// suitable to be either registered into the shadow record for later or directly pushed to an existing component, as well as honouring
// any "removeSource" annotations by removing these options from the source block.
fluid.filterBlocks = function (contextThat, sourceBlocks, sourceSegs, targetSegs, exclusions, removeSource) {
var togo = [];
fluid.each(sourceBlocks, function (block) {
var source = fluid.get(block.source, sourceSegs);
if (source !== undefined) {
togo.push(fluid.makeDistributionRecord(contextThat, block.source, sourceSegs, targetSegs, exclusions, block.recordType));
var rescued = $.extend({}, source);
if (removeSource) {
fluid.model.applyChangeRequest(block.source, {segs: sourceSegs, type: "DELETE"});
}
fluid.each(exclusions, function (exclusion) {
var orig = fluid.get(rescued, exclusion);
fluid.set(block.source, sourceSegs.concat(exclusion), orig);
});
}
});
return togo;
};
// Use this peculiar signature since the actual component and shadow itself may not exist yet. Perhaps clean up with FLUID-4925
fluid.noteCollectedDistribution = function (parentShadow, memberName, distribution) {
fluid.model.setSimple(parentShadow, ["collectedDistributions", memberName, distribution.id], true);
};
fluid.isCollectedDistribution = function (parentShadow, memberName, distribution) {
return fluid.model.getSimple(parentShadow, ["collectedDistributions", memberName, distribution.id]);
};
fluid.clearCollectedDistributions = function (parentShadow, memberName) {
fluid.model.applyChangeRequest(parentShadow, {segs: ["collectedDistributions", memberName], type: "DELETE"});
};
fluid.collectDistributions = function (distributedBlocks, parentShadow, distribution, thatStack, contextHashes, memberNames, i) {
var lastMember = memberNames[memberNames.length - 1];
if (!fluid.isCollectedDistribution(parentShadow, lastMember, distribution) &&
fluid.matchIoCSelector(distribution.selector, thatStack, contextHashes, memberNames, i)) {
distributedBlocks.push.apply(distributedBlocks, distribution.blocks);
fluid.noteCollectedDistribution(parentShadow, lastMember, distribution);
}
};
// Slightly silly function to clean up the "appliedDistributions" records. In general we need to be much more aggressive both
// about clearing instantiation garbage (e.g. onCreate and most of the shadow)
// as well as caching frequently-used records such as the "thatStack" which
// would mean this function could be written in a sensible way
fluid.registerCollectedClearer = function (shadow, parentShadow, memberName) {
if (!shadow.collectedClearer && parentShadow) {
shadow.collectedClearer = function () {
fluid.clearCollectedDistributions(parentShadow, memberName);
};
}
};
fluid.receiveDistributions = function (parentThat, gradeNames, memberName, that) {
var instantiator = fluid.getInstantiator(parentThat || that);
var thatStack = instantiator.getThatStack(parentThat || that); // most specific is at end
thatStack.unshift(fluid.rootComponent);
var memberNames = fluid.getMemberNames(instantiator, thatStack);
var shadows = fluid.transform(thatStack, function (thisThat) {
return instantiator.idToShadow[thisThat.id];
});
var parentShadow = shadows[shadows.length - (parentThat ? 1 : 2)];
var contextHashes = fluid.getMembers(shadows, "contextHash");
if (parentThat) { // if called before construction of component from assembleCreatorArguments - NB this path will be abolished/amalgamated
memberNames.push(memberName);
contextHashes.push(fluid.gradeNamesToHash(gradeNames));
thatStack.push(that);
} else {
fluid.registerCollectedClearer(shadows[shadows.length - 1], parentShadow, memberNames[memberNames.length - 1]);
}
var distributedBlocks = [];
for (var i = 0; i < thatStack.length - 1; ++i) {
fluid.each(shadows[i].distributions, function (distribution) { // eslint-disable-line no-loop-func
fluid.collectDistributions(distributedBlocks, parentShadow, distribution, thatStack, contextHashes, memberNames, i);
});
}
return distributedBlocks;
};
fluid.computeTreeDistance = function (path1, path2) {
var i = 0;
while (i < path1.length && i < path2.length && path1[i] === path2[i]) {
++i;
}
return path1.length + path2.length - 2*i; // eslint-disable-line space-infix-ops
};
// Called from applyDistributions (immediate application route) as well as mergeRecordsToList (pre-instantiation route) AS WELL AS assembleCreatorArguments (pre-pre-instantiation route)
fluid.computeDistributionPriority = function (targetThat, distributedBlock) {
if (!distributedBlock.priority) {
var instantiator = fluid.getInstantiator(targetThat);
var targetStack = instantiator.getThatStack(targetThat);
var targetPath = fluid.getMemberNames(instantiator, targetStack);
var sourceStack = instantiator.getThatStack(distributedBlock.contextThat);
var sourcePath = fluid.getMemberNames(instantiator, sourceStack);
var distance = fluid.computeTreeDistance(targetPath, sourcePath);
distributedBlock.priority = fluid.mergeRecordTypes.distribution - distance;
}
return distributedBlock;
};
// convert "preBlocks" as produced from fluid.filterBlocks into "real blocks" suitable to be used by the expansion machinery.
fluid.applyDistributions = function (that, preBlocks, targetShadow) {
var distributedBlocks = fluid.transform(preBlocks, function (preBlock) {
return fluid.generateExpandBlock(preBlock, that, targetShadow.mergePolicy);
}, function (distributedBlock) {
return fluid.computeDistributionPriority(that, distributedBlock);
});
var mergeOptions = targetShadow.mergeOptions;
mergeOptions.mergeBlocks.push.apply(mergeOptions.mergeBlocks, distributedBlocks);
mergeOptions.updateBlocks();
return distributedBlocks;
};
// TODO: This implementation is obviously poor and has numerous flaws - in particular it does no backtracking as well as matching backwards through the selector
/** Match a parsed IoC selector against a selection of data structures representing a component's tree context.
* @param {ParsedSelector} selector - A parsed selector structure as returned from `fluid.parseSelector`.
* @param {Component[]} thatStack - An array of components ascending up the tree from the component being matched,
* which will be held in the last position.
* @param {Object[]} contextHashes - An array of context hashes as cached in the component's shadows - a hash to
* `true`/"memberName" depending on the reason the context matches
* @param {String[]} [memberNames] - An array of member names of components in their parents. This is only used in the distributeOptions route.
* @param {Number} i - One plus the index of the IoCSS head component within `thatStack` - all components before this
* index will be ignored for matching. Will have value `1` in the queryIoCSelector route.
* @return {Boolean} `true` if the selector matches the leaf component at the end of `thatStack`
*/
fluid.matchIoCSelector = function (selector, thatStack, contextHashes, memberNames, i) {
var thatpos = thatStack.length - 1;
var selpos = selector.length - 1;
while (true) {
var isChild = selector[selpos].child;
var mustMatchHere = thatpos === thatStack.length - 1 || isChild;
var that = thatStack[thatpos];
var selel = selector[selpos];
var match = true;
for (var j = 0; j < selel.predList.length; ++j) {
var pred = selel.predList[j];
var context = pred.context;
if (context && context !== "*" && !(contextHashes[thatpos][context] || memberNames[thatpos] === context)) {
match = false;
break;
}
if (pred.id && that.id !== pred.id) {
match = false;
break;
}
}
if (selpos === 0 && thatpos > i && mustMatchHere && isChild) {
match = false; // child selector must exhaust stack completely - FLUID-5029
}
if (match) {
if (selpos === 0) {
return true;
}
--thatpos;
--selpos;
}
else {
if (mustMatchHere) {
return false;
}
else {
--thatpos;
}
}
if (thatpos < i) {
return false;
}
}
};
/** Query for all components matching a selector in a particular tree
* @param {Component} root - The root component at which to start the search
* @param {String} selector - An IoCSS selector, in form of a string. Note that since selectors supplied to this function implicitly
* match downwards, they need not contain the "head context" followed by whitespace required in the distributeOptions form. E.g.
* simply <code>"fluid.viewComponent"</code> will match all viewComponents below the root.
* @param {Boolean} flat - [Optional] <code>true</code> if the search should just be performed at top level of the component tree
* Note that with <code>flat=true</code> this search will scan every component in the tree and may well be very slow.
* @return {Component[]} The list of all components matching the selector
*/
// supported, PUBLIC API function
fluid.queryIoCSelector = function (root, selector, flat) {
var parsed = fluid.parseSelector(selector, fluid.IoCSSMatcher);
var togo = [];
fluid.visitComponentsForMatching(root, {flat: flat}, function (that, thatStack, contextHashes) {
if (fluid.matchIoCSelector(parsed, thatStack, contextHashes, [], 1)) {
togo.push(that);
}
});
return togo;
};
fluid.isIoCSSSelector = function (context) {
return context.indexOf(" ") !== -1; // simple-minded check for an IoCSS reference
};
fluid.pushDistributions = function (targetHead, selector, target, blocks) {
var targetShadow = fluid.shadowForComponent(targetHead);
var id = fluid.allocateGuid();
var distribution = {
id: id, // This id is used in clearDistributions
target: target, // Here for improved debuggability - info is duplicated in "selector"
selector: selector,
blocks: blocks
};
Object.freeze(distribution);
Object.freeze(distribution.blocks);
fluid.pushArray(targetShadow, "distributions", distribution);
return id;
};
fluid.clearDistribution = function (targetHeadId, id) {
var targetHeadShadow = fluid.globalInstantiator.idToShadow[targetHeadId];
// By FLUID-6193, the head component may already have been destroyed, in which case the distributions are gone,
// and we have leaked only its id. In theory we may want to re-establish the distribution if the head is
// re-created, but that is a far wider issue.
if (targetHeadShadow) {
fluid.remove_if(targetHeadShadow.distributions, function (distribution) {
return distribution.id === id;
});
}
};
fluid.clearDistributions = function (shadow) {
fluid.each(shadow.outDistributions, function (outDist) {
fluid.clearDistribution(outDist.targetHeadId, outDist.distributionId);
});
};
// Modifies a parsed selector to extract and remove its head context which will be matched upwards
fluid.extractSelectorHead = function (parsedSelector) {
var predList = parsedSelector[0].predList;
var context = predList[0].context;
predList.length = 0;
return context;
};
fluid.parseExpectedOptionsPath = function (path, role) {
var segs = fluid.model.parseEL(path);
if (segs[0] !== "options") {
fluid.fail("Error in options distribution path ", path, " - only " + role + " paths beginning with \"options\" are supported");
}
return segs.slice(1);
};
fluid.replicateProperty = function (source, property, targets) {
if (source[property] !== undefined) {
fluid.each(targets, function (target) {
target[property] = source[property];
});
}
};
fluid.undistributableOptions = ["gradeNames", "distributeOptions", "argumentMap", "initFunction", "mergePolicy", "progressiveCheckerOptions"]; // automatically added to "exclusions" of every distribution
fluid.distributeOptions = function (that, optionsStrategy) {
var thatShadow = fluid.shadowForComponent(that);
var records = fluid.driveStrategy(that.options, "distributeOptions", optionsStrategy);
fluid.each(records, function distributeOptionsOne(record) {
fluid.pushActivity("distributeOptions", "parsing distributeOptions block %record %that ", {that: that, record: record});
if (typeof(record.target) !== "string") {
fluid.fail("Error in options distribution record ", record, " a member named \"target\" must be supplied holding an IoC reference");
}
if (typeof(record.source) === "string" ^ record.record === undefined) {
fluid.fail("Error in options distribution record ", record, ": must supply either a member \"source\" holding an IoC reference or a member \"record\" holding a literal record");
}
var targetRef = fluid.parseContextReference(record.target);
var targetHead, selector, context;
if (fluid.isIoCSSSelector(targetRef.context)) {
selector = fluid.parseSelector(targetRef.context, fluid.IoCSSMatcher);
var headContext = fluid.extractSelectorHead(selector);
if (headContext === "/") {
targetHead = fluid.rootComponent;
} else {
context = headContext;
}
}
else {
context = targetRef.context;
}
targetHead = targetHead || fluid.resolveContext(context, that);
if (!targetHead) {
fluid.fail("Error in options distribution record ", record, " - could not resolve context {" + context + "} to a head component");
}
var targetSegs = fluid.model.parseEL(targetRef.path);
var preBlocks;
if (record.record !== undefined) {
preBlocks = [(fluid.makeDistributionRecord(that, record.record, [], targetSegs, []))];
}
else {
var source = fluid.parseContextReference(record.source);
if (source.context !== "that") {
fluid.fail("Error in options distribution record ", record, " only a context of {that} is supported");
}
var sourceSegs = fluid.parseExpectedOptionsPath(source.path, "source");
var fullExclusions = fluid.makeArray(record.exclusions).concat(sourceSegs.length === 0 ? fluid.undistributableOptions : []);
var exclusions = fluid.transform(fullExclusions, function (exclusion) {
return fluid.model.parseEL(exclusion);
});
preBlocks = fluid.filterBlocks(that, thatShadow.mergeOptions.mergeBlocks, sourceSegs, targetSegs, exclusions, record.removeSource);
thatShadow.mergeOptions.updateBlocks(); // perhaps unnecessary
}
fluid.replicateProperty(record, "priority", preBlocks);
fluid.replicateProperty(record, "namespace", preBlocks);
// TODO: inline material has to be expanded in its original context!
if (selector) {
var distributionId = fluid.pushDistributions(targetHead, selector, record.target, preBlocks);
thatShadow.outDistributions = thatShadow.outDistributions || [];
thatShadow.outDistributions.push({
targetHeadId: targetHead.id,
distributionId: distributionId
});
}
else { // The component exists now, we must rebalance it
var targetShadow = fluid.shadowForComponent(targetHead);
fluid.applyDistributions(that, preBlocks, targetShadow);
}
fluid.popActivity();
});
};
fluid.gradeNamesToHash = function (gradeNames) {
var contextHash = {};
fluid.each(gradeNames, function (gradeName) {
contextHash[gradeName] = true;
contextHash[fluid.computeNickName(gradeName)] = true;
});
return contextHash;
};
fluid.cacheShadowGrades = function (that, shadow) {
var contextHash = fluid.gradeNamesToHash(that.options.gradeNames);
if (!contextHash[shadow.memberName]) {
contextHash[shadow.memberName] = "memberName"; // This is filtered out again in recordComponent - TODO: Ensure that ALL resolution uses the scope chain eventually
}
shadow.contextHash = contextHash;
fluid.each(contextHash, function (troo, context) {
shadow.ownScope[context] = that;
if (shadow.parentShadow && shadow.parentShadow.that.type !== "fluid.rootComponent") {
shadow.parentShadow.childrenScope[context] = that;
}
});
};
// First sequence point where the mergeOptions strategy is delivered from Fluid.js - here we take care
// of both receiving and transmitting options distributions
fluid.deliverOptionsStrategy = function (that, target, mergeOptions) {
var shadow = fluid.shadowForComponent(that, shadow);
fluid.cacheShadowGrades(that, shadow);
shadow.mergeOptions = mergeOptions;
};
/** Dynamic grade closure algorithm - the following 4 functions share access to a small record structure "rec" which is
* constructed at the start of fluid.computeDynamicGrades
*/
fluid.collectDistributedGrades = function (rec) {
// Receive distributions first since these may cause arrival of more contextAwareness blocks.
var distributedBlocks = fluid.receiveDistributions(null, null, null, rec.that);
if (distributedBlocks.length > 0) {
var readyBlocks = fluid.applyDistributions(rec.that, distributedBlocks, rec.shadow);
var gradeNamesList = fluid.transform(fluid.getMembers(readyBlocks, ["source", "gradeNames"]), fluid.makeArray);
fluid.accumulateDynamicGrades(rec, fluid.flatten(gradeNamesList));
}
};
// Apply a batch of freshly acquired plain dynamic grades to the target component and recompute its options
fluid.applyDynamicGrades = function (rec) {
rec.oldGradeNames = fluid.makeArray(rec.gradeNames);
// Note that this crude algorithm doesn't allow us to determine which grades are "new" and which not // TODO: can no longer interpret comment
var newDefaults = fluid.copy(fluid.getMergedDefaults(rec.that.typeName, rec.gradeNames));
rec.gradeNames.length = 0; // acquire derivatives of dynamic grades (FLUID-5054)
rec.gradeNames.push.apply(rec.gradeNames, newDefaults.gradeNames);
fluid.each(rec.gradeNames, function (gradeName) {
if (!fluid.isIoCReference(gradeName)) {
rec.seenGrades[gradeName] = true;
}
});
var shadow = rec.shadow;
fluid.cacheShadowGrades(rec.that, shadow);
// This cheap strategy patches FLUID-5091 for now - some more sophisticated activity will take place
// at this site when we have a full fix for FLUID-5028
shadow.mergeOptions.destroyValue(["mergePolicy"]);
shadow.mergeOptions.destroyValue(["components"]);
shadow.mergeOptions.destroyValue(["invokers"]);
rec.defaultsBlock.source = newDefaults;
shadow.mergeOptions.updateBlocks();
shadow.mergeOptions.computeMergePolicy(); // TODO: we should really only do this if its content changed - this implies moving all options evaluation over to some (cheap) variety of the ChangeApplier
fluid.accumulateDynamicGrades(rec, newDefaults.gradeNames);
};
// Filter some newly discovered grades into their plain and dynamic queues
fluid.accumulateDynamicGrades = function (rec, newGradeNames) {
fluid.each(newGradeNames, function (gradeName) {
if (!rec.seenGrades[gradeName]) {
if (fluid.isIoCReference(gradeName)) {
rec.rawDynamic.push(gradeName);
rec.seenGrades[gradeName] = true;
} else if (!fluid.contains(rec.oldGradeNames, gradeName)) {
rec.plainDynamic.push(gradeName);
}
}
});
};
fluid.computeDynamicGrades = function (that, shadow, strategy) {
delete that.options.gradeNames; // Recompute gradeNames for FLUID-5012 and others
var gradeNames = fluid.driveStrategy(that.options, "gradeNames", strategy); // Just acquire the reference and force eval of mergeBlocks "target", contents are wrong
gradeNames.length = 0;
// TODO: In complex distribution cases, a component might end up with multiple default blocks
var defaultsBlock = fluid.findMergeBlocks(shadow.mergeOptions.mergeBlocks, "defaults")[0];
var rec = {
that: that,
shadow: shadow,
defaultsBlock: defaultsBlock,
gradeNames: gradeNames, // remember that this array is globally shared
seenGrades: {},
plainDynamic: [],
rawDynamic: []
};
fluid.each(shadow.mergeOptions.mergeBlocks, function (block) { // acquire parents of earlier blocks before applying later ones
gradeNames.push.apply(gradeNames, fluid.makeArray(block.target && block.target.gradeNames));
fluid.applyDynamicGrades(rec);
});
fluid.collectDistributedGrades(rec);
while (true) {
while (rec.plainDynamic.length > 0) {
gradeNames.push.apply(gradeNames, rec.plainDynamic);
rec.plainDynamic.length = 0;
fluid.applyDynamicGrades(rec);
fluid.collectDistributedGrades(rec);
}
if (rec.rawDynamic.length > 0) {
var expanded = fluid.expandImmediate(rec.rawDynamic.shift(), that, shadow.localDynamic);
if (typeof(expanded) === "function") {
expanded = expanded();
}
if (expanded) {
rec.plainDynamic = rec.plainDynamic.concat(expanded);
}
} else {
break;
}
}
if (shadow.collectedClearer) {
shadow.collectedClearer();
delete shadow.collectedClearer;
}
};
fluid.computeDynamicComponentKey = function (recordKey, sourceKey) {
return recordKey + (sourceKey === 0 ? "" : "-" + sourceKey); // TODO: configurable name strategies
};
// Hacked resolution of FLUID-6371 - we can't add a listener because this version of the framework doesn't
// support multiple records as subcomponents, and there may have been a total options injection
fluid.hasDynamicComponentCount = function (shadow, key) {
var hypos = key.indexOf("-");
if (hypos !== -1) {
var recordKey = key.substring(0, hypos);
return shadow.dynamicComponentCount !== undefined && shadow.dynamicComponentCount[recordKey] !== undefined;
}
};
fluid.clearDynamicParentRecord = function (shadow, key) {
if (fluid.hasDynamicComponentCount(shadow, key)) {
var holder = fluid.get(shadow.that, ["options", "components"]);
if (holder) {
delete holder[key];
}
}
};
fluid.registerDynamicRecord = function (that, recordKey, sourceKey, record, toCensor) {
var key = fluid.computeDynamicComponentKey(recordKey, sourceKey);
var recordCopy = fluid.copy(record);
delete recordCopy[toCensor];
fluid.set(that.options, ["components", key], recordCopy);
return key;
};
fluid.computeDynamicComponents = function (that, mergeOptions) {
var shadow = fluid.shadowForComponent(that);
var localSub = shadow.subcomponentLocal = {};
var records = fluid.driveStrategy(that.options, "dynamicComponents", mergeOptions.strategy);
fluid.each(records, function (record, recordKey) {
if (!record.sources && !record.createOnEvent) {
fluid.fail("Cannot process dynamicComponents record ", record, " without a \"sources\" or \"createOnEvent\" entry");
}
if (record.sources) {
var sources = fluid.expandOptions(record.sources, that);
fluid.each(sources, function (source, sourceKey) {
var key = fluid.registerDynamicRecord(that, recordKey, sourceKey, record, "sources");
localSub[key] = {"source": source, "sourcePath": sourceKey};
});
}
else if (record.createOnEvent) {
var event = fluid.event.expandOneEvent(that, record.createOnEvent);
fluid.set(shadow, ["dynamicComponentCount", recordKey], 0);
var listener = function () {
var key = fluid.registerDynamicRecord(that, recordKey, shadow.dynamicComponentCount[recordKey]++, record, "createOnEvent");
var localRecord = {"arguments": fluid.makeArray(arguments)};
fluid.initDependent(that, key, localRecord);
};
event.addListener(listener);
fluid.recordListener(event, listener, shadow);
}
});
};
// Second sequence point for mergeOptions from Fluid.js - here we construct all further
// strategies required on the IoC side and mount them into the shadow's getConfig for universal use
fluid.computeComponentAccessor = function (that, localRecord) {
var instantiator = fluid.globalInstantiator;
var shadow = fluid.shadowForComponent(that);
shadow.localDynamic = localRecord; // for signalling to dynamic grades from dynamic components
var options = that.options;
var strategy = shadow.mergeOptions.strategy;
var optionsStrategy = fluid.mountStrategy(["options"], options, strategy);
shadow.invokerStrategy = fluid.recordStrategy(that, options, strategy, "invokers", fluid.invokerFromRecord);
shadow.eventStrategyBlock = fluid.recordStrategy(that, options, strategy, "events", fluid.eventFromRecord, ["events"]);
var eventStrategy = fluid.mountStrategy(["events"], that, shadow.eventStrategyBlock.strategy, ["events"]);
shadow.memberStrategy = fluid.recordStrategy(that, options, strategy, "members", fluid.memberFromRecord, null, {model: true, modelRelay: true});
// NB - ginger strategy handles concrete, rationalise
shadow.getConfig = {strategies: [fluid.model.funcResolverStrategy, fluid.makeGingerStrategy(that),
optionsStrategy, shadow.invokerStrategy.strategy, shadow.memberStrategy.strategy, eventStrategy]};
fluid.computeDynamicGrades(that, shadow, strategy, shadow.mergeOptions.mergeBlocks);
fluid.distributeOptions(that, strategy);
if (shadow.contextHash["fluid.resolveRoot"]) {
var memberName;
if (shadow.contextHash["fluid.resolveRootSingle"]) {
var singleRootType = fluid.getForComponent(that, ["options", "singleRootType"]);
if (!singleRootType) {
fluid.fail("Cannot register object with grades " + Object.keys(shadow.contextHash).join(", ") + " as fluid.resolveRootSingle since it has not defined option singleRootType");
}
memberName = fluid.typeNameToMemberName(singleRootType);
} else {
memberName = fluid.computeGlobalMemberName(that);
}
var parent = fluid.resolveRootComponent;
if (parent[memberName]) {
instantiator.clearComponent(parent, memberName);
}
instantiator.recordKnownComponent(parent, that, memberName, false);
}
return shadow.getConfig;
};
// About the SHADOW:
// Allocated at: instantiator's "recordComponent"
// Contents:
// path {String} Principal allocated path (point of construction) in tree
// that {Component} The component itself
// contextHash {String to Boolean} Map of context names which this component matches
// mergePolicy, mergeOptions: Machinery for last phase of options merging
// invokerStrategy, eventStrategyBlock, memberStrategy, getConfig: Junk required to operate the accessor
// listeners: Listeners registered during this component's construction, to be cleared during clearListeners
// distributions, collectedClearer: Managing options distributions
// outDistributions: A list of distributions registered from this component, signalling from distributeOptions to clearDistributions
// subcomponentLocal: Signalling local record from computeDynamicComponents to assembleCreatorArguments
// dynamicLocal: Local signalling for dynamic grades
// ownScope: A hash of names to components which are in scope from this component - populated in cacheShadowGrades
// childrenScope: A hash of names to components which are in scope because they are children of this component (BELOW own ownScope in resolution order)
fluid.shadowForComponent = function (component) {
var instantiator = fluid.getInstantiator(component);
return instantiator && component ? instantiator.idToShadow[component.id] : null;
};
// Access the member at a particular path in a component, forcing it to be constructed gingerly if necessary
// supported, PUBLIC API function
fluid.getForComponent = function (component, path) {
var shadow = fluid.shadowForComponent(component);
var getConfig = shadow ? shadow.getConfig : undefined;
return fluid.get(component, path, getConfig);
};
// An EL segment resolver strategy that will attempt to trigger creation of
// components that it discovers along the EL path, if they have been defined but not yet
// constructed.
fluid.makeGingerStrategy = function (that) {
var instantiator = fluid.getInstantiator(that);
return function (component, thisSeg, index, segs) {
var atval = component[thisSeg];
if (atval === fluid.inEvaluationMarker && index === segs.length) {
fluid.fail("Error in component configuration - a circular reference was found during evaluation of path segment \"" + thisSeg +
"\": for more details, see the activity records following this message in the console, or issue fluid.setLogging(fluid.logLevel.TRACE) when running your application");
}
if (index > 1) {
return atval;
}
if (atval === undefined && component.hasOwnProperty(thisSeg)) { // avoid recomputing properties that have been explicitly evaluated to undefined
return fluid.NO_VALUE;
}
if (atval === undefined) { // pick up components in instantiation here - we can cut this branch by attaching early
var parentPath = instantiator.idToShadow[component.id].path;
var childPath = instantiator.composePath(parentPath, thisSeg);
atval = instantiator.pathToComponent[childPath];
}
if (atval === undefined) {
// TODO: This check is very expensive - once gingerness is stable, we ought to be able to
// eagerly compute and cache the value of options.components - check is also incorrect and will miss injections
var subRecord = fluid.getForComponent(component, ["options", "components", thisSeg]);
if (subRecord) {
if (subRecord.createOnEvent) {
fluid.fail("Error resolving path segment \"" + thisSeg + "\" of path " + segs.join(".") + " since component with record ", subRecord,
" has annotation \"createOnEvent\" - this very likely represents an implementation error. Either alter the reference so it does not " +
" match this component, or alter your workflow to ensure that the component is instantiated by the time this reference resolves");
}
fluid.initDependent(component, thisSeg);
atval = component[thisSeg];
}
}
return atval;
};
};
// Listed in dependence order
fluid.frameworkGrades = ["fluid.component", "fluid.modelComponent", "fluid.viewComponent", "fluid.rendererComponent"];
fluid.filterBuiltinGrades = function (gradeNames) {
return fluid.remove_if(fluid.makeArray(gradeNames), function (gradeName) {
return fluid.frameworkGrades.indexOf(gradeName) !== -1;
});
};
fluid.dumpGradeNames = function (that) {
return that.options && that.options.gradeNames ?
" gradeNames: " + JSON.stringify(fluid.filterBuiltinGrades(that.options.gradeNames)) : "";
};
fluid.dumpThat = function (that) {
return "{ typeName: \"" + that.typeName + "\"" + fluid.dumpGradeNames(that) + " id: " + that.id + "}";
};
fluid.dumpThatStack = function (thatStack, instantiator) {
var togo = fluid.transform(thatStack, function (that) {
var path = instantiator.idToPath(that.id);
return fluid.dumpThat(that) + (path ? (" - path: " + path) : "");
});
return togo.join("\n");
};
fluid.dumpComponentPath = function (that) {
var path = fluid.pathForComponent(that);
return path ? fluid.pathUtil.composeSegments(path) : "** no path registered for component **";
};
fluid.resolveContext = function (context, that, fast) {
if (context === "that") {
return that;
}
// TODO: Check performance impact of this type check introduced for FLUID-5903 in a very sensitive corner
if (typeof(context) === "object") {
var innerContext = fluid.resolveContext(context.context, that, fast);
if (!fluid.isComponent(innerContext)) {
fluid.triggerMismatchedPathError(context.context, that);
}
var rawValue = fluid.getForComponent(innerContext, context.path);
// TODO: Terrible, slow dispatch for this route
var expanded = fluid.expandOptions(rawValue, that);
if (!fluid.isComponent(expanded)) {
fluid.fail("Unable to resolve recursive context expression " + fluid.renderContextReference(context) + ": the directly resolved value of " + rawValue +
" did not resolve to a component in the scope of component ", that, ": got ", expanded);
}
return expanded;
} else {
var foundComponent;
var instantiator = fluid.globalInstantiator; // fluid.getInstantiator(that); // this hash lookup takes over 1us!
if (fast) {
var shadow = instantiator.idToShadow[that.id];
return shadow.ownScope[context];
} else {
var thatStack = instantiator.getFullStack(that);
fluid.visitComponentsForVisibility(instantiator, thatStack, function (component, name) {
var shadow = fluid.shadowForComponent(component);
// TODO: Some components, e.g. the static environment and typeTags do not have a shadow, which slows us down here
if (context === name || shadow && shadow.contextHash && shadow.contextHash[context] || context === component.typeName) {
foundComponent = component;
return true; // YOUR VISIT IS AT AN END!!
}
if (fluid.getForComponent(component, ["options", "components", context]) && !component[context]) {
// This is an expensive guess since we make it for every component up the stack - must apply the WAVE OF EXPLOSIONS (FLUID-4925) to discover all components first
// This line attempts a hopeful construction of components that could be guessed by nickname through finding them unconstructed
// in options. In the near future we should eagerly BEGIN the process of constructing components, discovering their
// types and then attaching them to the tree VERY EARLY so that we get consistent results from different strategies.
foundComponent = fluid.getForComponent(component, context);
return true;
}
});
return foundComponent;
}
}
};
fluid.triggerMismatchedPathError = function (parsed, parentThat) {
var ref = fluid.renderContextReference(parsed);
fluid.fail("Failed to resolve reference " + ref + " - could not match context with name " +
parsed.context + " from component " + fluid.dumpThat(parentThat) + " at path " + fluid.dumpComponentPath(parentThat) + " component: " , parentThat);
};
fluid.makeStackFetcher = function (parentThat, localRecord, fast) {
var fetcher = function (parsed) {
if (parentThat && parentThat.lifecycleStatus === "destroyed") {
fluid.fail("Cannot resolve reference " + fluid.renderContextReference(parsed) + " from component " + fluid.dumpThat(parentThat) + " which has been destroyed");
}
var context = parsed.context;
if (localRecord && context in localRecord) {
return fluid.get(localRecord[context], parsed.path);
}
var foundComponent = fluid.resolveContext(context, parentThat, fast);
if (!foundComponent && parsed.path !== "") {
fluid.triggerMismatchedPathError(parsed, parentThat);
}
return fluid.getForComponent(foundComponent, parsed.path);
};
return fetcher;
};
fluid.makeStackResolverOptions = function (parentThat, localRecord, fast) {
return $.extend(fluid.copy(fluid.rawDefaults("fluid.makeExpandOptions")), {
localRecord: localRecord || {},
fetcher: fluid.makeStackFetcher(parentThat, localRecord, fast),
contextThat: parentThat,
exceptions: {members: {model: true, modelRelay: true}}
});
};
fluid.clearListeners = function (shadow) {
// TODO: bug here - "afterDestroy" listeners will be unregistered already unless they come from this component
fluid.each(shadow.listeners, function (rec) {
rec.event.removeListener(rec.listenerId || rec.listener);
});
delete shadow.listeners;
};
fluid.recordListener = function (event, listener, shadow, listenerId) {
if (event.ownerId !== shadow.that.id) { // don't bother recording listeners registered from this component itself
fluid.pushArray(shadow, "listeners", {event: event, listener: listener, listenerId: listenerId});
}
};
fluid.constructScopeObjects = function (instantiator, parent, child, childShadow) {
var parentShadow = parent ? instantiator.idToShadow[parent.id] : null;
childShadow.childrenScope = parentShadow ? Object.create(parentShadow.ownScope) : {};
childShadow.ownScope = Object.create(childShadow.childrenScope);
childShadow.parentShadow = parentShadow;
};
fluid.clearChildrenScope = function (instantiator, parentShadow, child, childShadow) {