/*\ title: $:/core/modules/widgets/widget.js type: application/javascript module-type: widget \*/ "use strict"; var Widget = function(parseTreeNode,options) { this.initialise(parseTreeNode,options); }; Widget.prototype.initialise = function(parseTreeNode,options) { // Bail if parseTreeNode is undefined, meaning that the widget constructor was called without any arguments so that it can be subclassed if(parseTreeNode === undefined) { return; } options = options || {}; // Save widget info this.parseTreeNode = parseTreeNode; this.wiki = options.wiki; this.parentWidget = options.parentWidget; this.variables = Object.create(this.parentWidget ? this.parentWidget.variables : null); this.document = options.document; this.attributes = {}; this.children = []; this.domNodes = []; this.eventListeners = {}; // Hashmap of the widget classes if(!this.widgetClasses) { // Get widget classes Widget.prototype.widgetClasses = $tw.modules.applyMethods("widget"); // Process any subclasses $tw.modules.forEachModuleOfType("widget-subclass",function(title,module) { if(module.baseClass) { var baseClass = Widget.prototype.widgetClasses[module.baseClass]; if(!baseClass) { throw "Module '" + title + "' is attemping to extend a non-existent base class '" + module.baseClass + "'"; } var subClass = module.constructor; subClass.prototype = new baseClass(); $tw.utils.extend(subClass.prototype,module.prototype); Widget.prototype.widgetClasses[module.name || module.baseClass] = subClass; } }); } }; Widget.prototype.render = function(parent,nextSibling) { this.parentDomNode = parent; this.execute(); this.renderChildren(parent,nextSibling); }; Widget.prototype.execute = function() { this.makeChildWidgets(); }; Widget.prototype.setVariable = function(name,value,params,isMacroDefinition,options) { options = options || {}; var valueIsArray = $tw.utils.isArray(value); this.variables[name] = { value: valueIsArray ? (value[0] || "") : value, resultList: valueIsArray ? value : [value], params: params, isMacroDefinition: !!isMacroDefinition, isFunctionDefinition: !!options.isFunctionDefinition, isProcedureDefinition: !!options.isProcedureDefinition, isWidgetDefinition: !!options.isWidgetDefinition, configTrimWhiteSpace: !!options.configTrimWhiteSpace }; }; Widget.prototype.getVariableInfo = function(name,options) { options = options || {}; var self = this, actualParams = options.params || [], variable; if(options.allowSelfAssigned) { variable = this.variables[name]; } else { variable = this.parentWidget && this.parentWidget.variables[name]; } if(variable) { var originalValue = variable.value, value = originalValue, params = [], resultList = [value]; // Only substitute parameter and variable references if this variable was defined with the \define pragma if(variable.isMacroDefinition) { params = self.resolveVariableParameters(variable.params,actualParams); // Substitute any parameters specified in the definition $tw.utils.each(params,function(param) { if("name" in param) { value = $tw.utils.replaceString(value,new RegExp("\\$" + $tw.utils.escapeRegExp(param.name) + "\\$","mg"),param.value); } }); value = self.substituteVariableReferences(value,options); resultList = [value]; } else if(variable.isFunctionDefinition) { // Function evaluations params = self.resolveVariableParameters(variable.params,actualParams); var variables = $tw.utils.extend({},options.variables); // Apply default parameter values $tw.utils.each(variable.params,function(param,index) { if(param["default"]) { variables[param.name] = param["default"]; } }); // Parameters are an array of {name:, value:, multivalue:} pairs (name and multivalue are optional) $tw.utils.each(params,function(param) { if(param.multiValue && param.multiValue.length) { variables[param.name] = param.multiValue; } else { variables[param.name] = param.value || ""; } }); resultList = this.wiki.filterTiddlers(value,this.makeFakeWidgetWithVariables(variables),options.source); value = resultList[0] || ""; } else { if(variable.resultList) { resultList = variable.resultList; } params = variable.params; } return { text: value, params: params, resultList: resultList, srcVariable: variable, isCacheable: originalValue === value }; } var text = this.evaluateMacroModule(name,actualParams); if(text === undefined) { text = options.defaultValue; } return { text: text, resultList: [text] }; }; Widget.prototype.getVariable = function(name,options) { return this.getVariableInfo(name,options).text; }; Widget.prototype.resolveVariableParameters = function(formalParams,actualParams) { formalParams = formalParams || []; actualParams = actualParams || []; var nextAnonParameter = 0, // Next candidate anonymous parameter in macro call paramInfo, paramValue, paramMultiValue, results = []; // Step through each of the parameters in the macro definition for(var p=0; p 0) { var nextAnonParameter = 0, // Next candidate anonymous parameter in macro call paramInfo, paramValue; // Step through each of the parameters in the macro definition for(var p=0; p { acc[key] = { value: vars[key], enumerable: true, configurable: true }; return acc; }, {}) ); } }; return fakeWidget; }; Widget.prototype.computeAttributes = function(options) { options = options || {}; var changedAttributes = {}, self = this, newMultiValuedAttributes = Object.create(null); $tw.utils.each(this.parseTreeNode.attributes,function(attribute,name) { if(options.filterFn) { if(!options.filterFn(name)) { return; } } var value = self.computeAttribute(attribute,options), multiValue = null; if($tw.utils.isArray(value)) { multiValue = value; newMultiValuedAttributes[name] = multiValue; value = value[0] || ""; } var changed = (self.attributes[name] !== value); if(!changed && multiValue && self.multiValuedAttributes) { changed = !$tw.utils.isArrayEqual(self.multiValuedAttributes[name] || [], multiValue); } if(changed) { self.attributes[name] = value; changedAttributes[name] = true; } }); this.multiValuedAttributes = newMultiValuedAttributes; return changedAttributes; }; Widget.prototype.computeAttribute = function(attribute,options) { options = options || {}; var self = this, value; if(attribute.type === "filtered") { value = this.wiki.filterTiddlers(attribute.filter,this); if(!options.asList) { value = value[0] || ""; } } else if(attribute.type === "indirect") { value = this.wiki.getTextReference(attribute.textReference,"",this.getVariable("currentTiddler")); if(value && options.asList) { value = [value]; } } else if(attribute.type === "macro") { // Get the macro name var macroName = attribute.value.attributes["$variable"].value; // Collect macro parameters var params = []; $tw.utils.each(attribute.value.orderedAttributes,function(attr) { var param = { value: self.computeAttribute(attr) }; if(attr.name && !attr.isPositional) { param.name = attr.name; } params.push(param); }); // Invoke the macro var variableInfo = this.getVariableInfo(macroName,{params: params}); if(options.asList || attribute.isMVV) { value = variableInfo.resultList; } else { value = variableInfo.text; } } else if(attribute.type === "substituted") { value = this.wiki.getSubstitutedText(attribute.rawValue,this) || ""; if(options.asList) { value = [value]; } } else { // String attribute value = attribute.value; if(options.asList) { if(value === undefined) { value = []; } else { value = [value]; } } } return value; }; Widget.prototype.hasAttribute = function(name) { return $tw.utils.hop(this.attributes,name); }; Widget.prototype.hasParseTreeNodeAttribute = function(name) { return $tw.utils.hop(this.parseTreeNode.attributes,name); }; Widget.prototype.getAttribute = function(name,defaultText) { if($tw.utils.hop(this.attributes,name)) { return this.attributes[name]; } else { return defaultText; } }; Widget.prototype.assignAttributes = function(domNode,options) { options = options || {}; var self = this, changedAttributes = options.changedAttributes || this.attributes, sourcePrefix = options.sourcePrefix || "", destPrefix = options.destPrefix || "", EVENT_ATTRIBUTE_PREFIX = "on"; var assignAttribute = function(name,value) { // Process any CSS custom properties if(name.substr(0,2) === "--" && name.length > 2) { domNode.style.setProperty(name,value); return; } if(name.substr(0,6) === "style." && name.length > 6) { domNode.style[$tw.utils.unHyphenateCss(name.substr(6))] = value; return; } if(name.substr(0,sourcePrefix.length) === sourcePrefix) { name = destPrefix + name.substr(sourcePrefix.length); } else { value = undefined; } if(options.excludeEventAttributes && name.substr(0,2).toLowerCase() === EVENT_ATTRIBUTE_PREFIX) { value = undefined; } if(value !== undefined) { // Handle the xlink: namespace var namespace = null; if(name.substr(0,6) === "xlink:" && name.length > 6) { namespace = "http://www.w3.org/1999/xlink"; name = name.substr(6); } try { domNode.setAttributeNS(namespace,name,value); } catch(e) { } } }; // If the parse tree node has the orderedAttributes property then use that order if(this.parseTreeNode.orderedAttributes) { $tw.utils.each(this.parseTreeNode.orderedAttributes,function(attribute,index) { if(attribute.name in changedAttributes) { assignAttribute(attribute.name,self.getAttribute(attribute.name)); } }); // Otherwise update each changed attribute irrespective of order } else { $tw.utils.each(changedAttributes,function(value,name) { assignAttribute(name,self.getAttribute(name)); }); } }; Widget.prototype.getAncestorCount = function() { if(this.ancestorCount === undefined) { if(this.parentWidget) { this.ancestorCount = this.parentWidget.getAncestorCount() + 1; } else { this.ancestorCount = 0; } } return this.ancestorCount; }; Widget.prototype.makeChildWidgets = function(parseTreeNodes,options) { options = options || {}; this.children = []; var self = this; // Check for too much recursion if(this.getAncestorCount() > $tw.utils.TranscludeRecursionError.MAX_WIDGET_TREE_DEPTH) { throw new $tw.utils.TranscludeRecursionError(); } else { // Create set variable widgets for each variable $tw.utils.each(options.variables,function(value,name) { var setVariableWidget = { type: "set", attributes: { name: {type: "string", value: name}, value: {type: "string", value: value} }, children: parseTreeNodes }; parseTreeNodes = [setVariableWidget]; }); // Create the child widgets $tw.utils.each(parseTreeNodes || (this.parseTreeNode && this.parseTreeNode.children),function(childNode) { self.children.push(self.makeChildWidget(childNode)); }); } }; Widget.prototype.makeChildWidget = function(parseTreeNode,options) { var self = this; options = options || {}; // Check whether this node type is defined by a custom widget definition var variableDefinitionName = "$" + parseTreeNode.type; if(this.variables[variableDefinitionName]) { var isOverrideable = function() { // Widget is overrideable if its name contains a period, or if it is an existing JS widget and we're not in safe mode return parseTreeNode.type.indexOf(".") !== -1 || (!!self.widgetClasses[parseTreeNode.type] && !$tw.safeMode); }; if(!parseTreeNode.isNotRemappable && isOverrideable()) { var variableInfo = this.getVariableInfo(variableDefinitionName,{allowSelfAssigned: true}); if(variableInfo && variableInfo.srcVariable && variableInfo.srcVariable.value && variableInfo.srcVariable.isWidgetDefinition) { var newParseTreeNode = { type: "transclude", children: parseTreeNode.children, isBlock: parseTreeNode.isBlock }; $tw.utils.addAttributeToParseTreeNode(newParseTreeNode,"$variable",variableDefinitionName); $tw.utils.each(parseTreeNode.attributes,function(attr,name) { // If the attribute starts with a dollar then add an extra dollar so that it doesn't clash with the $xxx attributes of transclude name = name.charAt(0) === "$" ? "$" + name : name; $tw.utils.addAttributeToParseTreeNode(newParseTreeNode,$tw.utils.extend({},attr,{name: name})); }); parseTreeNode = newParseTreeNode; } } } var WidgetClass = this.widgetClasses[parseTreeNode.type]; if(!WidgetClass) { WidgetClass = this.widgetClasses.text; parseTreeNode = {type: "text", text: "Undefined widget '" + parseTreeNode.type + "'"}; } $tw.utils.each(options.variables,function(value,name) { var setVariableWidget = { type: "set", attributes: { name: {type: "string", value: name}, value: {type: "string", value: value} }, children: [ parseTreeNode ] }; parseTreeNode = setVariableWidget; }); return new WidgetClass(parseTreeNode,{ wiki: this.wiki, parentWidget: this, document: this.document }); }; Widget.prototype.nextSibling = function() { if(this.parentWidget) { var index = this.parentWidget.children.indexOf(this); if(index !== -1 && index < this.parentWidget.children.length-1) { return this.parentWidget.children[index+1]; } } return null; }; Widget.prototype.previousSibling = function() { if(this.parentWidget) { var index = this.parentWidget.children.indexOf(this); if(index !== -1 && index > 0) { return this.parentWidget.children[index-1]; } } return null; }; Widget.prototype.renderChildren = function(parent,nextSibling) { var children = this.children; for(var i = 0; i < children.length; i++) { children[i].render(parent,nextSibling); }; }; Widget.prototype.addEventListeners = function(listeners) { var self = this; $tw.utils.each(listeners,function(listenerInfo) { self.addEventListener(listenerInfo.type,listenerInfo.handler); }); }; Widget.prototype.addEventListener = function(type,handler) { this.eventListeners[type] = this.eventListeners[type] || []; if(this.eventListeners[type].indexOf(handler) === -1) { this.eventListeners[type].push(handler); } }; Widget.prototype.removeEventListener = function(type,handler) { if(!this.eventListeners[type]) return; var index = this.eventListeners[type].indexOf(handler); if(index !== -1) { this.eventListeners[type].splice(index,1); } }; Widget.prototype.dispatchEvent = function(event) { event.widget = event.widget || this; var listeners = this.eventListeners[event.type]; if(listeners) { var self = this; var shouldPropagate = true; $tw.utils.each(listeners,function(handler) { var propagate; if(typeof handler === "string") { // If handler is a string, call it as a method on the widget propagate = self[handler].call(self,event); } else { // Otherwise call the function handler directly propagate = handler.call(self,event); } if(propagate === false) { shouldPropagate = false; } }); if(!shouldPropagate) { return false; } } if(this.parentWidget) { return this.parentWidget.dispatchEvent(event); } return true; }; Widget.prototype.refresh = function(changedTiddlers) { return this.refreshChildren(changedTiddlers); }; Widget.prototype.refreshSelf = function() { var nextSibling = this.findNextSiblingDomNode(); this.removeChildDomNodes(); this.render(this.parentDomNode,nextSibling); }; Widget.prototype.refreshChildren = function(changedTiddlers) { var children = this.children, refreshed = false; for (var i = 0; i < children.length; i++) { refreshed = children[i].refresh(changedTiddlers) || refreshed; } return refreshed; }; Widget.prototype.findNextSiblingDomNode = function(startIndex) { // Refer to this widget by its index within its parents children var parent = this.parentWidget, index = startIndex !== undefined ? startIndex : parent.children.indexOf(this); if(index === -1) { throw "node not found in parents children"; } while(++index < parent.children.length) { var domNode = parent.children[index].findFirstDomNode(); if(domNode) { return domNode; } } var grandParent = parent.parentWidget; if(grandParent && parent.parentDomNode === this.parentDomNode) { index = grandParent.children.indexOf(parent); if(index !== -1) { return parent.findNextSiblingDomNode(index); } } return null; }; Widget.prototype.findFirstDomNode = function() { // Return the first dom node of this widget, if we've got one if(this.domNodes.length > 0) { return this.domNodes[0]; } // Otherwise, recursively call our children for(var t=0; t 0) { // If this widget will remove its own DOM nodes, children should not remove theirs removeChildDOMNodes = false; } // Destroy children first this.destroyChildren({removeDOMNodes: removeChildDOMNodes}); this.children = []; // Call custom cleanup method if implemented if(typeof this.onDestroy === "function") { this.onDestroy(); } // Remove our DOM nodes if needed if(removeDOMNodes) { this.removeLocalDomNodes(); } }; /* Remove any DOM nodes created by this widget */ Widget.prototype.removeLocalDomNodes = function() { for(const domNode of this.domNodes) { if(domNode.parentNode) { domNode.parentNode.removeChild(domNode); } } this.domNodes = []; }; /* Invoke the action widgets that are descendents of the current widget. */ Widget.prototype.invokeActions = function(triggeringWidget,event) { var handled = false; // For each child widget for(var t=0; t widgets recursively. The tag name allows aliased versions of the widget to be found too */ Widget.prototype.findChildrenDataWidgets = function(children,tag,callback) { var self = this; $tw.utils.each(children,function(child) { if(child.dataWidgetTag === tag) { callback(child); } if(child.children) { self.findChildrenDataWidgets(child.children,tag,callback); } }); }; /* Evaluate a variable with parameters. This is a static convenience method that attempts to evaluate a variable as a function, returning an array of strings */ Widget.evaluateVariable = function(widget,name,options) { var result; if(widget.getVariableInfo) { var variableInfo = widget.getVariableInfo(name,options); result = variableInfo.resultList || [variableInfo.text]; } else { result = [widget.getVariable(name)]; } return result; }; exports.widget = Widget;