mirror of
https://github.com/Jermolene/TiddlyWiki5.git
synced 2026-03-11 17:21:56 -07:00
838 lines
25 KiB
JavaScript
Executable file
838 lines
25 KiB
JavaScript
Executable file
/*\
|
|
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<formalParams.length; p++) {
|
|
// Check if we've got a macro call parameter with the same name
|
|
paramInfo = formalParams[p];
|
|
paramValue = undefined;
|
|
paramMultiValue = undefined;
|
|
for(var m=0; m<actualParams.length; m++) {
|
|
if(typeof actualParams[m] !== "string" && actualParams[m].name === paramInfo.name) {
|
|
paramValue = actualParams[m].value;
|
|
paramMultiValue = actualParams[m].multiValue || [paramValue]
|
|
}
|
|
}
|
|
// If not, use the next available anonymous macro call parameter
|
|
while(nextAnonParameter < actualParams.length && actualParams[nextAnonParameter].name) {
|
|
nextAnonParameter++;
|
|
}
|
|
if(paramValue === undefined && nextAnonParameter < actualParams.length) {
|
|
var param = actualParams[nextAnonParameter++];
|
|
paramValue = typeof param === "string" ? param : param.value;
|
|
paramMultiValue = typeof param === "string" ? [param] : (param.multiValue || [paramValue]);
|
|
}
|
|
// If we've still not got a value, use the default, if any
|
|
if(!paramValue) {
|
|
paramValue = paramInfo["default"] || "";
|
|
paramMultiValue = [paramValue];
|
|
}
|
|
|
|
results.push({name: paramInfo.name, value: paramValue, multiValue: paramMultiValue});
|
|
}
|
|
return results;
|
|
};
|
|
|
|
Widget.prototype.substituteVariableReferences = function(text,options) {
|
|
var self = this;
|
|
return (text || "").replace(/\$\(([^\)\$]+)\)\$/g,function(match,p1,offset,string) {
|
|
return options.variables && options.variables[p1] || (self.getVariable(p1,{defaultValue: ""}));
|
|
});
|
|
};
|
|
|
|
Widget.prototype.evaluateMacroModule = function(name,actualParams,defaultValue) {
|
|
if($tw.utils.hop($tw.macros,name)) {
|
|
var macro = $tw.macros[name],
|
|
args = [];
|
|
if(macro.params.length > 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<macro.params.length; p++) {
|
|
// Check if we've got a macro call parameter with the same name
|
|
paramInfo = macro.params[p];
|
|
paramValue = undefined;
|
|
for(var m=0; m<actualParams.length; m++) {
|
|
if(actualParams[m].name === paramInfo.name) {
|
|
paramValue = actualParams[m].value;
|
|
}
|
|
}
|
|
// If not, use the next available anonymous macro call parameter
|
|
while(nextAnonParameter < actualParams.length && actualParams[nextAnonParameter].name) {
|
|
nextAnonParameter++;
|
|
}
|
|
if(paramValue === undefined && nextAnonParameter < actualParams.length) {
|
|
paramValue = actualParams[nextAnonParameter++].value;
|
|
}
|
|
// If we've still not got a value, use the default, if any
|
|
paramValue = paramValue || paramInfo["default"] || "";
|
|
// Save the parameter
|
|
args.push(paramValue);
|
|
}
|
|
}
|
|
else for(var i=0; i<actualParams.length; ++i) {
|
|
args.push(actualParams[i].value);
|
|
}
|
|
return (macro.run.apply(this,args) || "").toString();
|
|
} else {
|
|
return defaultValue;
|
|
}
|
|
};
|
|
|
|
Widget.prototype.hasVariable = function(name,value) {
|
|
var node = this;
|
|
while(node) {
|
|
if($tw.utils.hop(node.variables,name) && node.variables[name].value === value) {
|
|
return true;
|
|
}
|
|
node = node.parentWidget;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
Widget.prototype.getStateQualifier = function(name) {
|
|
this.qualifiers = this.qualifiers || Object.create(null);
|
|
name = name || "transclusion";
|
|
if(this.qualifiers[name]) {
|
|
return this.qualifiers[name];
|
|
} else {
|
|
var output = [],
|
|
node = this;
|
|
while(node && node.parentWidget) {
|
|
if($tw.utils.hop(node.parentWidget.variables,name)) {
|
|
output.push(node.getVariable(name));
|
|
}
|
|
node = node.parentWidget;
|
|
}
|
|
var value = $tw.utils.hashString(output.join(""));
|
|
this.qualifiers[name] = value;
|
|
return value;
|
|
}
|
|
};
|
|
|
|
Widget.prototype.makeFakeWidgetWithVariables = function(vars = {}) {
|
|
const self = this;
|
|
|
|
const fakeWidget = {
|
|
getVariableInfo(name,opts = {}) {
|
|
if(name in vars) {
|
|
const value = vars[name];
|
|
return Array.isArray(value)
|
|
? { text: value[0], resultList: value }
|
|
: { text: value, resultList: [value] };
|
|
}
|
|
opts = opts || {};
|
|
opts.variables = Object.assign({}, vars, opts.variables || {});
|
|
return self.getVariableInfo(name, opts);
|
|
},
|
|
|
|
getVariable(name,opts) {
|
|
return this.getVariableInfo(name, opts).text;
|
|
},
|
|
|
|
resolveVariableParameters: self.resolveVariableParameters,
|
|
wiki: self.wiki,
|
|
makeFakeWidgetWithVariables: self.makeFakeWidgetWithVariables,
|
|
|
|
get variables() {
|
|
// Merge parent vars via prototype-like delegation
|
|
return Object.create(self.variables || {},
|
|
Object.keys(vars).reduce((acc, key) => {
|
|
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<this.children.length; t++) {
|
|
var domNode = this.children[t].findFirstDomNode();
|
|
if(domNode) {
|
|
return domNode;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/*
|
|
Entry into destroy procedure
|
|
options include:
|
|
removeDOMNodes: boolean (default true)
|
|
*/
|
|
Widget.prototype.destroyChildren = function(options) {
|
|
$tw.utils.each(this.children,function(childWidget) {
|
|
childWidget.destroy(options);
|
|
});
|
|
};
|
|
|
|
/*
|
|
Legacy entry into destroy procedure
|
|
*/
|
|
Widget.prototype.removeChildDomNodes = function() {
|
|
this.destroy({removeDOMNodes: true});
|
|
};
|
|
|
|
/*
|
|
Default destroy
|
|
options include:
|
|
- removeDOMNodes: boolean (default true)
|
|
*/
|
|
Widget.prototype.destroy = function(options) {
|
|
const { removeDOMNodes = true } = options || {};
|
|
let removeChildDOMNodes = removeDOMNodes;
|
|
if(removeDOMNodes && this.domNodes.length > 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<this.children.length; t++) {
|
|
var child = this.children[t],
|
|
childIsActionWidget = !!child.invokeAction,
|
|
actionRefreshPolicy = child.getVariable("tv-action-refresh-policy"); // Default is "once"
|
|
// Refresh the child if required
|
|
if(childIsActionWidget || actionRefreshPolicy === "always") {
|
|
child.refreshSelf();
|
|
}
|
|
// Invoke the child if it is an action widget
|
|
if(childIsActionWidget) {
|
|
if(child.invokeAction(triggeringWidget,event)) {
|
|
handled = true;
|
|
}
|
|
}
|
|
// Propagate through through the child if it permits it
|
|
if(child.allowActionPropagation() && child.invokeActions(triggeringWidget,event)) {
|
|
handled = true;
|
|
}
|
|
}
|
|
return handled;
|
|
};
|
|
|
|
/*
|
|
Invoke the action widgets defined in a string
|
|
*/
|
|
Widget.prototype.invokeActionString = function(actions,triggeringWidget,event,variables) {
|
|
actions = actions || "";
|
|
var parser = this.wiki.parseText("text/vnd.tiddlywiki",actions,{
|
|
parentWidget: this,
|
|
document: this.document
|
|
}),
|
|
widgetNode = this.wiki.makeWidget(parser,{
|
|
parentWidget: this,
|
|
document: this.document,
|
|
variables: variables
|
|
});
|
|
var container = this.document.createElement("div");
|
|
widgetNode.render(container,null);
|
|
return widgetNode.invokeActions(this,event);
|
|
};
|
|
|
|
/*
|
|
Execute action tiddlers by tag
|
|
*/
|
|
Widget.prototype.invokeActionsByTag = function(tag,event,variables) {
|
|
var self = this;
|
|
$tw.utils.each(self.wiki.filterTiddlers("[all[shadows+tiddlers]tag[" + tag + "]!has[draft.of]]"),function(title) {
|
|
self.invokeActionString(self.wiki.getTiddlerText(title),self,event,variables);
|
|
});
|
|
};
|
|
|
|
Widget.prototype.allowActionPropagation = function() {
|
|
return true;
|
|
};
|
|
|
|
/*
|
|
Find child <$data> 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;
|