UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

970 lines (883 loc) 129 kB
/* 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) {