UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

723 lines (651 loc) 28.3 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/main/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/main/Infusion-LICENSE.txt */ "use strict"; if (!fluid.oldRenderer) { fluid.fail("fluidRenderer.js is a necessary dependency of RendererUtilities"); } // TODO: API status of these 3 functions is uncertain. So far, they have never // appeared in documentation. fluid.oldRenderer.visitDecorators = function (that, visitor) { fluid.visitComponentChildren(that, function (component, name) { if (name.indexOf(fluid.oldRenderer.decoratorComponentPrefix) === 0) { visitor(component, name); } }, {flat: true}, []); }; fluid.oldRenderer.clearDecorators = function (that) { var instantiator = fluid.getInstantiator(that); fluid.oldRenderer.visitDecorators(that, function (component, name) { instantiator.clearComponent(that, name); }); }; fluid.oldRenderer.getDecoratorComponents = function (that) { var togo = {}; fluid.oldRenderer.visitDecorators(that, function (component, name) { togo[name] = component; }); return togo; }; // Utilities for coordinating options in renderer components - this code is all pretty // dreadful and needs to be organised as a suitable set of defaults and policies fluid.oldRenderer.modeliseOptions = function (options, defaults, baseOptions) { return $.extend({}, defaults, fluid.filterKeys(baseOptions, ["model", "applier"]), options); }; fluid.oldRenderer.reverseMerge = function (target, source, names) { names = fluid.makeArray(names); fluid.each(names, function (name) { if (target[name] === undefined && source[name] !== undefined) { target[name] = source[name]; } }); }; /** "Renderer component" infrastructure **/ // TODO: fix this up with IoC and improved handling of templateSource as well as better // options layout (model appears in both rOpts and eOpts) // "options" here is the original "rendererFnOptions" fluid.oldRenderer.createOldRendererSubcomponent = function (container, selectors, options, parentThat, fossils) { options = options || {}; var source = options.templateSource ? options.templateSource : {node: $(container)}; var nativeModel = options.rendererOptions.model === undefined; var rendererOptions = fluid.oldRenderer.modeliseOptions(options.rendererOptions, null, parentThat); rendererOptions.fossils = fossils || {}; rendererOptions.parentComponent = parentThat; if (container.jquery) { var cascadeOptions = { document: container[0].ownerDocument, jQuery: container.constructor }; fluid.oldRenderer.reverseMerge(rendererOptions, cascadeOptions, fluid.keys(cascadeOptions)); } var that = {}; var templates = null; that.render = function (tree) { var cutpointFn = options.cutpointGenerator || "fluid.oldRenderer.selectorsToCutpoints"; rendererOptions.cutpoints = rendererOptions.cutpoints || fluid.invokeGlobalFunction(cutpointFn, [selectors, options]); if (nativeModel) { // check necessary since the component insanely supports the possibility the model is not the component's model! // and the pagedTable uses this. rendererOptions.model = parentThat.model; // fix FLUID-5664 } var renderTarget = $(options.renderTarget ? options.renderTarget : container); if (templates) { fluid.clear(rendererOptions.fossils); fluid.reRender(templates, renderTarget, tree, rendererOptions); } else { if (typeof(source) === "function") { // TODO: make a better attempt than this at asynchrony source = source(); } templates = fluid.render(source, renderTarget, tree, rendererOptions); } }; return that; }; fluid.defaults("fluid.oldRendererComponent", { gradeNames: ["fluid.viewComponent"], mergePolicy: { "rendererOptions.idMap": "nomerge", "rendererOptions.cutpoints": fluid.arrayConcatPolicy, protoTree: "noexpand, replace", parentBundle: "nomerge", "changeApplierOptions.resolverSetConfig": "resolverSetConfig" }, workflows: { local: { fetchOldRendererTemplate: { funcName: "fluid.fetchOldRendererTemplate", priority: "after:concludeComponentObservation" }, initOldRendererComponent: { waitIO: true, funcName: "fluid.initOldRendererWorkflow", priority: "after:fetchOldRendererTemplate" } } }, invokers: { refreshView: { funcName: "fluid.oldRendererComponent.refreshView", args: "{that}" }, produceTree: { funcName: "fluid.oldRendererComponent.produceTree", args: "{that}" } }, rendererOptions: { autoBind: true }, events: { prepareModelForRender: null, onRenderTree: null, afterRender: null }, listeners: { "onCreate.renderOnInit": { funcName: "fluid.oldRendererComponent.renderOnInit", args: ["{that}.options.renderOnInit", "{that}"], priority: "last" } }, components: { messageResolver: { type: "fluid.messageResolver", options: { messageBase: "{oldRendererComponent}.options.strings", resolveFunc: "{oldRendererComponent}.options.messageResolverFunction", parents: "@expand:fluid.makeArray({oldRendererComponent}.options.parentBundle)" } } } }); // Final definition for backwards compatibility - will remove with old renderer fluid.defaults("fluid.rendererComponent", { gradeNames: "fluid.oldRendererComponent" }); fluid.oldRendererComponent.renderOnInit = function (renderOnInit, that) { if (renderOnInit || that.renderOnInit) { that.refreshView(); } }; fluid.protoExpanderForComponent = function (parentThat, options) { var expanderOptions = fluid.renderer.modeliseOptions(options.expanderOptions, {ELstyle: "${}"}, parentThat); fluid.renderer.reverseMerge(expanderOptions, options, ["resolverGetConfig", "resolverSetConfig"]); var expander = fluid.renderer.makeProtoExpander(expanderOptions, parentThat); return expander; }; fluid.oldRendererComponent.refreshView = function (that) { if (!that.renderer) { // Terrible stopgap fix for FLUID-5279 - all of this implementation will be swept away // model relay may cause this to be called during init, and we have no proper definition for "that.renderer" since it is // constructed in a terrible way that.renderOnInit = true; return; } else { fluid.oldRenderer.clearDecorators(that); that.events.prepareModelForRender.fire(that.model, that.applier, that); var tree = that.produceTree(that); var rendererFnOptions = that.renderer.rendererFnOptions; // Terrible stopgap fix for FLUID-5821 - given that model reference may be rebound, generate the expander from scratch on every render if (!rendererFnOptions.noexpand) { var expander = fluid.protoExpanderForComponent(that, rendererFnOptions); tree = expander(tree); } that.events.onRenderTree.fire(that, tree); that.renderer.render(tree); that.events.afterRender.fire(that); } }; fluid.oldRendererComponent.produceTree = function (that) { var produceTreeOption = that.options.produceTree; return produceTreeOption ? (typeof(produceTreeOption) === "string" ? fluid.getGlobalValue(produceTreeOption) : produceTreeOption) (that) : fluid.copy(that.options.protoTree); }; fluid.fetchOldRendererTemplate = function (shadow) { // Cater for more modern components that mix in resourceLoader, as well as uses in the prefs framework which // fetch resources and inject them using a TemplateLoader higher in the tree in which case this is a no-op fluid.getForComponent(shadow.that, "resources.template"); }; fluid.initOldRendererWorkflow = function (shadow) { fluid.initOldRendererComponent(shadow.that); }; fluid.initOldRendererComponent = function (that) { var rendererOptions = fluid.oldRenderer.modeliseOptions(fluid.copy(that.options.rendererOptions), null, that); if (!rendererOptions.messageSource && that.options.strings) { rendererOptions.messageSource = {type: "resolver", resolver: that.messageResolver}; } fluid.oldRenderer.reverseMerge(rendererOptions, that.options, ["resolverGetConfig", "resolverSetConfig"]); that.rendererOptions = rendererOptions; var rendererFnOptions = $.extend({}, that.options.rendererFnOptions, { rendererOptions: rendererOptions, repeatingSelectors: that.options.repeatingSelectors, selectorsToIgnore: that.options.selectorsToIgnore, expanderOptions: { envAdd: {styles: that.options.styles} } }); if (that.resources && that.resources.template) { rendererFnOptions.templateSource = function () { // TODO: don't obliterate, multitemplates, etc. return that.resources.template.resourceText; }; } fluid.oldRenderer.reverseMerge(rendererFnOptions, that.options, ["resolverGetConfig", "resolverSetConfig"]); var renderer = { fossils: {}, rendererFnOptions: rendererFnOptions, boundPathForNode: function (node) { return fluid.boundPathForNode(node, renderer.fossils); } }; var rendererSub = fluid.oldRenderer.createOldRendererSubcomponent(that.container, that.options.selectors, rendererFnOptions, that, renderer.fossils); that.renderer = $.extend(renderer, rendererSub); }; var removeSelectors = function (selectors, selectorsToIgnore) { fluid.each(fluid.makeArray(selectorsToIgnore), function (selectorToIgnore) { delete selectors[selectorToIgnore]; }); return selectors; }; var markRepeated = function (selectorKey, repeatingSelectors) { if (repeatingSelectors) { fluid.each(repeatingSelectors, function (repeatingSelector) { if (selectorKey === repeatingSelector) { selectorKey = selectorKey + ":"; } }); } return selectorKey; }; fluid.oldRenderer.selectorsToCutpoints = function (selectors, options) { var togo = []; options = options || {}; selectors = fluid.copy(selectors); // Make a copy before potentially destructively changing someone's selectors. if (options.selectorsToIgnore) { selectors = removeSelectors(selectors, options.selectorsToIgnore); } for (var selectorKey in selectors) { togo.push({ id: markRepeated(selectorKey, options.repeatingSelectors), selector: selectors[selectorKey] }); } return togo; }; /** END of "Renderer Components" infrastructure **/ fluid.oldRenderer.NO_COMPONENT = {}; /* A special "shallow copy" operation suitable for nondestructively * merging trees of components. jQuery.extend in shallow mode will * neglect null valued properties. * This function is unsupported: It is not really intended for use by implementors. */ fluid.oldRenderer.mergeComponents = function (target, source) { for (var key in source) { target[key] = source[key]; } return target; }; fluid.registerNamespace("fluid.oldRenderer.selection"); /** Definition of expanders - firstly, "heavy" expanders **/ fluid.oldRenderer.selection.inputs = function (options, container, key, config) { fluid.expect("Selection to inputs expander", options, ["selectID", "inputID", "labelID", "rowID"]); var selection = config.expander(options.tree); // Remove the tree from option expansion as this is handled above, and // the tree may have strings with similar syntax to IoC references. var optsToExpand = fluid.censorKeys(options, ["tree"]); var expandedOpts = config.expandLight(optsToExpand); var rows = fluid.transform(selection.optionlist.value, function (option, index) { var togo = {}; var element = {parentRelativeID: "..::" + expandedOpts.selectID, choiceindex: index}; togo[expandedOpts.inputID] = element; togo[expandedOpts.labelID] = fluid.copy(element); return togo; }); var togo = {}; // TODO: JICO needs to support "quoted literal key initialisers" :P togo[expandedOpts.selectID] = selection; togo[expandedOpts.rowID] = {children: rows}; togo = config.expander(togo); return togo; }; fluid.oldRenderer.repeat = function (options, container, key, config) { fluid.expect("Repetition expander", options, ["controlledBy", "tree"]); var env = config.threadLocal(); var path = fluid.extractContextualPath(options.controlledBy, {ELstyle: "ALL"}, env); var list = fluid.get(config.model, path, config.resolverGetConfig); var togo = {}; if (!list || list.length === 0) { return options.ifEmpty ? config.expander(options.ifEmpty) : togo; } var expanded = []; fluid.each(list, function (element, i) { var EL = fluid.model.composePath(path, i); var envAdd = {}; if (options.pathAs) { envAdd[options.pathAs] = "${" + EL + "}"; } if (options.valueAs) { envAdd[options.valueAs] = fluid.get(config.model, EL, config.resolverGetConfig); } var expandrow = fluid.withEnvironment(envAdd, function () { return config.expander(options.tree); }, env); if (fluid.isArrayable(expandrow)) { if (expandrow.length > 0) { expanded.push({children: expandrow}); } } else if (expandrow !== fluid.oldRenderer.NO_COMPONENT) { expanded.push(expandrow); } }); var repeatID = options.repeatID; if (repeatID.indexOf(":") === -1) { repeatID = repeatID + ":"; } fluid.each(expanded, function (entry) {entry.ID = repeatID; }); return expanded; }; fluid.oldRenderer.condition = function (options, container, key, config) { fluid.expect("Selection to condition expander", options, ["condition"]); var condition; if (options.condition.funcName) { var args = config.expandLight(options.condition.args); condition = fluid.invokeGlobalFunction(options.condition.funcName, args); } else if (options.condition.expander) { condition = config.expander(options.condition); } else { condition = config.expandLight(options.condition); } var tree = (condition ? options.trueTree : options.falseTree); if (!tree) { tree = fluid.renderer.NO_COMPONENT; } return config.expander(tree); }; /* BEGIN unofficial IoC material */ // The following three functions are unsupported ane only used in the renderer expander. // The material they produce is no longer recognised for component resolution. fluid.withEnvironment = function (envAdd, func, root) { var key; root = root || fluid.globalThreadLocal(); try { for (key in envAdd) { root[key] = envAdd[key]; } $.extend(root, envAdd); return func(); } finally { for (key in envAdd) { delete root[key]; // TODO: users may want a recursive "scoping" model } } }; fluid.fetchContextReference = function (parsed, directModel, env, elResolver, externalFetcher) { // The "elResolver" is a hack to make certain common idioms in protoTrees work correctly, where a contextualised EL // path actually resolves onto a further EL reference rather than directly onto a value target if (elResolver) { parsed = elResolver(parsed, env); } var base = parsed.context ? env[parsed.context] : directModel; if (!base) { var resolveExternal = externalFetcher && externalFetcher(parsed); return resolveExternal || base; } return parsed.noDereference ? parsed.path : fluid.get(base, parsed.path); }; fluid.makeEnvironmentFetcher = function (directModel, elResolver, envGetter, externalFetcher) { envGetter = envGetter || fluid.globalThreadLocal; return function (parsed) { var env = envGetter(); return fluid.fetchContextReference(parsed, directModel, env, elResolver, externalFetcher); }; }; /* END of unofficial IoC material */ /* An EL extraction utility suitable for context expressions which occur in * expanding component trees. It dispatches context expressions to fluid.transformContextPath * in order to resolve them against EL references stored in the direct environment, and hence * to the "true (direct) model" - however, if there is no entry in the direct environment, it will resort to the "externalFetcher". * It satisfies a similar contract as fluid.extractEL, in that it will either return * an EL path, or undefined if the string value supplied cannot be interpreted * as an EL path with respect to the supplied options - it may also return {value: value} * in the case the context can be resolved by the supplied "externalFetcher" (required for FLUID-4986) */ // unsupported, non-API function fluid.extractContextualPath = function (string, options, env, externalFetcher) { var parsed = fluid.extractELWithContext(string, options); if (parsed) { if (parsed.context) { return env[parsed.context] ? fluid.transformContextPath(parsed, env).path : {value: externalFetcher(parsed)}; } else { return parsed.path; } } }; // unsupported, non-API function fluid.transformContextPath = function (parsed, env) { if (parsed.context) { var fetched = env[parsed.context]; var EL; if (typeof(fetched) === "string") { EL = fluid.extractEL(fetched, {ELstyle: "${}"}); } if (EL) { return { noDereference: parsed.path === "", path: fluid.model.composePath(EL, parsed.path) }; } } return parsed; }; // A forgiving variation of "makeStackFetcher" that returns nothing on failing to resolve an IoC reference, // in keeping with current protoComponent semantics. Note to self: abolish protoComponents fluid.oldRenderer.makeExternalFetcher = function (contextThat) { return function (parsed) { var foundComponent = fluid.resolveContext(parsed.context, contextThat); return foundComponent ? fluid.getForComponent(foundComponent, parsed.path) : undefined; }; }; /* * Create a "protoComponent expander" with the supplied set of options. * The returned value will be a function which accepts a "protoComponent tree" * as argument, and returns a "fully expanded" tree suitable for supplying * directly to the renderer. * A "protoComponent tree" is similar to the "dehydrated form" accepted by * the historical renderer - only * i) The input format is unambiguous - this expander will NOT accept hydrated * components in the {ID: "myId, myfield: "myvalue"} form - but ONLY in * the dehydrated {myID: {myfield: myvalue}} form. * ii) This expander has considerably greater power to expand condensed trees. * In particular, an "EL style" option can be supplied which will expand bare * strings found as values in the tree into UIBound components by a configurable * strategy. Supported values for "ELstyle" are a) "ALL" - every string will be * interpreted as an EL reference and assigned to the "valuebinding" member of * the UIBound, or b) any single character, which if it appears as the first * character of the string, will mark it out as an EL reference - otherwise it * will be considered a literal value, or c) the value "${}" which will be * recognised bracketing any other EL expression. */ fluid.oldRenderer.makeProtoExpander = function (expandOptions, parentThat) { // shallow copy of options - cheaply avoid destroying model, and all others are primitive var options = $.extend({ ELstyle: "${}" }, expandOptions); // shallow copy of options if (parentThat) { options.externalFetcher = fluid.oldRenderer.makeExternalFetcher(parentThat); } var threadLocal; // rebound on every expansion at entry point function fetchEL(string) { var env = threadLocal(); return fluid.extractContextualPath(string, options, env, options.externalFetcher); } var IDescape = options.IDescape || "\\"; var expandLight = function (source) { return fluid.expand(source, options); }; var expandBound = function (value, concrete) { if (value.messagekey !== undefined) { return { componentType: "UIMessage", messagekey: expandBound(value.messagekey), args: expandLight(value.args) }; } var proto; if (!fluid.isPrimitive(value) && !fluid.isArrayable(value)) { proto = $.extend({}, value); if (proto.decorators) { proto.decorators = expandLight(proto.decorators); } value = proto.value; delete proto.value; } else { proto = {}; } var EL; if (typeof(value) === "string") { var fetched = fetchEL(value); EL = typeof(fetched) === "string" ? fetched : null; value = fluid.get(fetched, "value") || value; } if (EL) { proto.valuebinding = EL; } else if (value !== undefined) { proto.value = value; } if (options.model && proto.valuebinding && proto.value === undefined) { proto.value = fluid.get(options.model, proto.valuebinding, options.resolverGetConfig); } if (concrete) { proto.componentType = "UIBound"; } return proto; }; options.filter = fluid.expander.lightFilter; var expandCond; var expandLeafOrCond; var expandEntry = function (entry) { var comp = []; expandCond(entry, comp); return {children: comp}; }; var expandExternal = function (entry) { if (entry === fluid.renderer.NO_COMPONENT) { return entry; } var singleTarget; var target = []; var pusher = function (comp) { singleTarget = comp; }; expandLeafOrCond(entry, target, pusher); return singleTarget || target; }; var expandConfig = { model: options.model, resolverGetConfig: options.resolverGetConfig, resolverSetConfig: options.resolverSetConfig, expander: expandExternal, expandLight: expandLight //threadLocal: threadLocal }; var expandLeaf = function (leaf, componentType) { var togo = {componentType: componentType}; var map = fluid.renderer.boundMap[componentType] || {}; for (var key in leaf) { if (/decorators|args/.test(key)) { togo[key] = expandLight(leaf[key]); continue; } else if (map[key]) { togo[key] = expandBound(leaf[key]); } else { togo[key] = leaf[key]; } } return togo; }; // A child entry may be a cond, a leaf, or another "thing with children". // Unlike the case with a cond's contents, these must be homogeneous - at least // they may either be ALL leaves, or else ALL cond/childed etc. // In all of these cases, the key will be THE PARENT'S KEY var expandChildren = function (entry, pusher) { var children = entry.children; for (var i = 0; i < children.length; ++i) { // each child in this list will lead to a WHOLE FORKED set of children. var target = []; var comp = { children: target}; var child = children[i]; // This use of function creation within a loop is acceptable since // the function does not attempt to close directly over the loop counter var childPusher = function (comp) { // eslint-disable-line no-loop-func target[target.length] = comp; }; expandLeafOrCond(child, target, childPusher); // Rescue the case of an expanded leaf into single component - TODO: check what sense this makes of the grammar if (comp.children.length === 1 && !comp.children[0].ID) { comp = comp.children[0]; } pusher(comp); } }; function detectBareBound(entry) { return fluid.find(entry, function (value, key) { return key === "decorators"; }) !== false; } // We have reached something which is either a leaf or Cond - either inside // a Cond or as an entry in children. expandLeafOrCond = function (entry, target, pusher) { var componentType = fluid.renderer.inferComponentType(entry); if (!componentType && (fluid.isPrimitive(entry) || detectBareBound(entry))) { componentType = "UIBound"; } if (componentType) { pusher(componentType === "UIBound" ? expandBound(entry, true) : expandLeaf(entry, componentType)); } else { // we couldn't recognise it as a leaf, so it must be a cond // this may be illegal if we are already in a cond. if (!target) { fluid.fail("Illegal cond->cond transition"); } expandCond(entry, target); } }; // cond entry may be a leaf, "thing with children" or a "direct bound". // a Cond can ONLY occur as a direct member of "children". Each "cond" entry may // give rise to one or many elements with the SAME key - if "expandSingle" discovers // "thing with children" they will all share the same key found in proto. expandCond = function (proto, target) { var key; var expandToTarget = function (expander) { var expanded = fluid.invokeGlobalFunction(expander.type, [expander, proto, key, expandConfig]); if (expanded !== fluid.renderer.NO_COMPONENT) { fluid.each(expanded, function (el) {target[target.length] = el; }); } }; var condPusher = function (comp) { comp.ID = key; target[target.length] = comp; }; for (key in proto) { var entry = proto[key]; if (key.charAt(0) === IDescape) { key = key.substring(1); } if (key === "expander") { var expanders = fluid.makeArray(entry); fluid.each(expanders, expandToTarget); } else if (entry) { if (entry.children) { if (key.indexOf(":") === -1) { key = key + ":"; } expandChildren(entry, condPusher); } else if (fluid.renderer.isBoundPrimitive(entry)) { condPusher(expandBound(entry, true)); } else { expandLeafOrCond(entry, null, condPusher); } } } }; return function (entry) { threadLocal = fluid.threadLocal(function () { return $.extend({}, options.envAdd); }); options.fetcher = fluid.makeEnvironmentFetcher(options.model, fluid.transformContextPath, threadLocal, options.externalFetcher); expandConfig.threadLocal = threadLocal; return expandEntry(entry); }; }; // Final definition for backwards compatibility - remove with old renderer jQuery.extend(fluid.renderer, fluid.oldRenderer);