2013-10-12 16:05:13 +00:00
/ * \
2013-11-08 08:47:00 +00:00
title : $ : / c o r e / m o d u l e s / w i d g e t s / w i d g e t . j s
2013-10-12 16:05:13 +00:00
type : application / javascript
2013-11-08 08:47:00 +00:00
module - type : widget
2013-10-12 16:05:13 +00:00
Widget base class
\ * /
( function ( ) {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict" ;
2022-05-12 15:26:33 +00:00
/* Maximum permitted depth of the widget tree for recursion detection */
var MAX _WIDGET _TREE _DEPTH = 1000 ;
2013-10-12 16:05:13 +00:00
/ *
Create a widget object for a parse tree node
parseTreeNode : reference to the parse tree node to be rendered
options : see below
Options include :
wiki : mandatory reference to wiki associated with this render tree
parentWidget : optional reference to a parent renderer node for the context chain
document : optional document object to use instead of global document
* /
var Widget = function ( parseTreeNode , options ) {
2019-03-17 12:25:15 +00:00
this . initialise ( parseTreeNode , options ) ;
2013-10-12 16:05:13 +00:00
} ;
/ *
Initialise widget properties . These steps are pulled out of the constructor so that we can reuse them in subclasses
* /
Widget . prototype . initialise = function ( parseTreeNode , options ) {
2019-03-17 12:25:15 +00:00
// 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 ;
}
2013-10-12 16:05:13 +00:00
options = options || { } ;
// Save widget info
this . parseTreeNode = parseTreeNode ;
this . wiki = options . wiki ;
this . parentWidget = options . parentWidget ;
2022-09-27 07:58:10 +00:00
this . variables = Object . create ( this . parentWidget ? this . parentWidget . variables : null ) ;
2013-10-12 16:05:13 +00:00
this . document = options . document ;
this . attributes = { } ;
this . children = [ ] ;
this . domNodes = [ ] ;
this . eventListeners = { } ;
// Hashmap of the widget classes
if ( ! this . widgetClasses ) {
2019-03-17 12:25:15 +00:00
// Get widget classes
2013-11-08 08:47:00 +00:00
Widget . prototype . widgetClasses = $tw . modules . applyMethods ( "widget" ) ;
2019-03-17 12:25:15 +00:00
// 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 ;
}
} ) ;
2013-10-12 16:05:13 +00:00
}
} ;
/ *
Render this widget into the DOM
* /
Widget . prototype . render = function ( parent , nextSibling ) {
this . parentDomNode = parent ;
this . execute ( ) ;
this . renderChildren ( parent , nextSibling ) ;
} ;
/ *
Compute the internal state of the widget
* /
Widget . prototype . execute = function ( ) {
this . makeChildWidgets ( ) ;
} ;
2013-11-13 21:25:45 +00:00
/ *
Set the value of a context variable
name : name of the variable
value : value of the variable
params : array of { name : , default : } for each parameter
2018-06-15 07:31:02 +00:00
isMacroDefinition : true if the variable is set via a \ define macro pragma ( and hence should have variable substitution performed )
2022-04-30 11:44:26 +00:00
options includes :
2022-05-09 17:00:09 +00:00
isProcedureDefinition : true if the variable is set via a \ procedure pragma ( and hence should not have variable substitution performed )
2022-04-30 11:44:26 +00:00
isFunctionDefinition : true if the variable is set via a \ function pragma ( and hence should not have variable substitution performed )
2022-05-13 07:49:53 +00:00
isWidgetDefinition : true if the variable is set via a \ widget pragma ( and hence should not have variable substitution performed )
2013-11-13 21:25:45 +00:00
* /
2022-04-30 11:44:26 +00:00
Widget . prototype . setVariable = function ( name , value , params , isMacroDefinition , options ) {
options = options || { } ;
this . variables [ name ] = {
value : value ,
params : params ,
isMacroDefinition : ! ! isMacroDefinition ,
2022-05-09 17:00:09 +00:00
isFunctionDefinition : ! ! options . isFunctionDefinition ,
2022-05-13 07:49:53 +00:00
isProcedureDefinition : ! ! options . isProcedureDefinition ,
2022-05-23 14:30:33 +00:00
isWidgetDefinition : ! ! options . isWidgetDefinition ,
configTrimWhiteSpace : ! ! options . configTrimWhiteSpace
2022-04-30 11:44:26 +00:00
} ;
2013-11-13 21:25:45 +00:00
} ;
2013-10-12 16:05:13 +00:00
/ *
Get the prevailing value of a context variable
name : name of variable
2013-10-25 21:16:15 +00:00
options : see below
Options include
2013-10-12 16:05:13 +00:00
params : array of { name : , value : } for each parameter
2013-10-25 21:16:15 +00:00
defaultValue : default value if the variable is not defined
2022-05-28 11:23:50 +00:00
allowSelfAssigned : if true , includes the current widget in the context chain instead of just the parent
2017-12-16 09:10:10 +00:00
Returns an object with the following fields :
params : array of { name : , value : } of parameters passed to wikitext variables
text : text of variable , with parameters properly substituted
2013-10-12 16:05:13 +00:00
* /
2017-12-16 09:10:10 +00:00
Widget . prototype . getVariableInfo = function ( name , options ) {
2013-10-25 21:16:15 +00:00
options = options || { } ;
2022-05-28 11:23:50 +00:00
var self = this ,
actualParams = options . params || [ ] ,
2022-09-27 07:58:10 +00:00
variable ;
if ( options . allowSelfAssigned ) {
variable = this . variables [ name ] ;
} else {
variable = this . parentWidget && this . parentWidget . variables [ name ] ;
}
2014-05-08 10:51:02 +00:00
// Check for the variable defined in the parent widget (or an ancestor in the prototype chain)
2022-09-27 07:58:10 +00:00
if ( variable ) {
var originalValue = variable . value ,
value = originalValue ,
params = [ ] ;
// 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 ) {
value = $tw . utils . replaceString ( value , new RegExp ( "\\$" + $tw . utils . escapeRegExp ( param . name ) + "\\$" , "mg" ) , param . value ) ;
} ) ;
value = self . substituteVariableReferences ( value , options ) ;
}
return {
text : value ,
params : params ,
srcVariable : variable ,
isCacheable : originalValue === value
} ;
2022-05-28 11:23:50 +00:00
}
// If the variable doesn't exist in the parent widget then look for a macro module
var text = this . evaluateMacroModule ( name , actualParams ) ;
if ( text === undefined ) {
text = options . defaultValue ;
}
2017-12-16 09:10:10 +00:00
return {
2022-05-28 11:23:50 +00:00
text : text ,
2022-05-21 15:31:34 +00:00
srcVariable : { }
2017-12-16 09:10:10 +00:00
} ;
2013-10-13 21:38:46 +00:00
} ;
2017-12-16 09:10:10 +00:00
/ *
Simplified version of getVariableInfo ( ) that just returns the text
* /
Widget . prototype . getVariable = function ( name , options ) {
return this . getVariableInfo ( name , options ) . text ;
} ;
2023-01-21 22:07:34 +00:00
/ *
Maps actual parameters onto formal parameters , returning an array of { name : , value : } objects
2023-01-23 16:34:08 +00:00
formalParams - Array of { name : , default : } ( default value is optional )
actualParams - Array of string values or { name : , value : } ( name is optional )
2023-01-21 22:07:34 +00:00
* /
2017-12-16 09:10:10 +00:00
Widget . prototype . resolveVariableParameters = function ( formalParams , actualParams ) {
formalParams = formalParams || [ ] ;
actualParams = actualParams || [ ] ;
var nextAnonParameter = 0 , // Next candidate anonymous parameter in macro call
paramInfo , paramValue ,
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 ;
for ( var m = 0 ; m < actualParams . length ; m ++ ) {
2023-01-23 16:34:08 +00:00
if ( typeof actualParams [ m ] !== "string" && actualParams [ m ] . name === paramInfo . name ) {
2017-12-16 09:10:10 +00:00
paramValue = actualParams [ m ] . value ;
2013-10-12 16:05:13 +00:00
}
}
2017-12-16 09:10:10 +00:00
// If not, use the next available anonymous macro call parameter
while ( nextAnonParameter < actualParams . length && actualParams [ nextAnonParameter ] . name ) {
nextAnonParameter ++ ;
}
if ( paramValue === undefined && nextAnonParameter < actualParams . length ) {
2023-01-23 16:34:08 +00:00
var param = actualParams [ nextAnonParameter ++ ] ;
paramValue = typeof param === "string" ? param : param . value ;
2017-12-16 09:10:10 +00:00
}
// If we've still not got a value, use the default, if any
paramValue = paramValue || paramInfo [ "default" ] || "" ;
// Store the parameter name and value
results . push ( { name : paramInfo . name , value : paramValue } ) ;
2013-10-12 16:05:13 +00:00
}
2017-12-16 09:10:10 +00:00
return results ;
2013-10-13 21:38:46 +00:00
} ;
2021-11-23 13:51:42 +00:00
Widget . prototype . substituteVariableReferences = function ( text , options ) {
2013-10-13 21:38:46 +00:00
var self = this ;
2014-07-14 13:52:00 +00:00
return ( text || "" ) . replace ( /\$\(([^\)\$]+)\)\$/g , function ( match , p1 , offset , string ) {
2021-11-23 13:51:42 +00:00
return options . variables && options . variables [ p1 ] || ( self . getVariable ( p1 , { defaultValue : "" } ) ) ;
2013-10-13 21:38:46 +00:00
} ) ;
2013-10-12 16:05:13 +00:00
} ;
2013-10-17 15:57:07 +00:00
Widget . prototype . evaluateMacroModule = function ( name , actualParams , defaultValue ) {
if ( $tw . utils . hop ( $tw . macros , name ) ) {
var macro = $tw . macros [ name ] ,
args = [ ] ;
2013-12-11 21:59:52 +00:00
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 ;
}
2013-10-17 15:57:07 +00:00
}
2013-12-11 21:59:52 +00:00
// 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 ) ;
2013-10-17 15:57:07 +00:00
}
2013-12-11 21:59:52 +00:00
}
else for ( var i = 0 ; i < actualParams . length ; ++ i ) {
args . push ( actualParams [ i ] . value ) ;
2013-10-17 15:57:07 +00:00
}
2015-01-10 13:36:43 +00:00
return ( macro . run . apply ( this , args ) || "" ) . toString ( ) ;
2013-10-17 15:57:07 +00:00
} else {
return defaultValue ;
}
} ;
2013-10-12 16:05:13 +00:00
/ *
Check whether a given context variable value exists in the parent chain
* /
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 ;
} ;
/ *
2013-11-28 10:53:37 +00:00
Construct a qualifying string based on a hash of concatenating the values of a given variable in the parent chain
2013-10-12 16:05:13 +00:00
* /
Widget . prototype . getStateQualifier = function ( name ) {
2015-07-06 10:23:12 +00:00
this . qualifiers = this . qualifiers || Object . create ( null ) ;
2013-10-12 16:05:13 +00:00
name = name || "transclusion" ;
2015-07-06 10:23:12 +00:00
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 ;
2013-10-12 16:05:13 +00:00
}
2015-07-06 10:23:12 +00:00
var value = $tw . utils . hashString ( output . join ( "" ) ) ;
this . qualifiers [ name ] = value ;
return value ;
2013-10-12 16:05:13 +00:00
}
} ;
2022-05-23 19:18:54 +00:00
/ *
Make a fake widget with specified variables , suitable for variable lookup in filters
* /
Widget . prototype . makeFakeWidgetWithVariables = function ( variables ) {
var self = this ;
return {
getVariable : function ( name , opts ) {
2022-10-25 13:13:38 +00:00
if ( $tw . utils . hop ( variables , name ) ) {
2022-05-23 19:18:54 +00:00
return variables [ name ] ;
} else {
2022-10-25 13:13:38 +00:00
opts = opts || { } ;
opts . variables = variables ;
2022-05-23 19:18:54 +00:00
return self . getVariable ( name , opts ) ;
} ;
} ,
getVariableInfo : function ( name , opts ) {
2022-10-25 13:13:38 +00:00
if ( $tw . utils . hop ( variables , name ) ) {
2022-05-23 19:18:54 +00:00
return {
text : variables [ name ]
} ;
} else {
2022-10-25 13:13:38 +00:00
opts = opts || { } ;
opts . variables = variables ;
2022-05-23 19:18:54 +00:00
return self . getVariableInfo ( name , opts ) ;
} ;
} ,
2023-01-21 22:07:34 +00:00
makeFakeWidgetWithVariables : self . makeFakeWidgetWithVariables ,
evaluateVariable : self . evaluateVariable ,
2023-01-23 16:34:08 +00:00
resolveVariableParameters : self . resolveVariableParameters ,
2023-01-21 22:07:34 +00:00
wiki : self . wiki
2022-05-23 19:18:54 +00:00
} ;
} ;
2023-01-21 22:07:34 +00:00
/ *
Evaluate a variable and associated actual parameters and result the resulting array .
The way that the variable is evaluated depends upon its type :
* Functions are evaluated as parameterised filter strings
* Macros are returned as plain text with substitution of parameters
* Procedures and widgets are returned as plain text
Options are :
params - the actual parameters – may be one of :
* an array of values that may be an anonymous string value , or a { name : , value : } pair
* a hashmap of { name : value } pairs
* a function invoked with parameters ( name , index ) that returns a parameter value by name or position
source - iterator for source tiddlers
* /
Widget . prototype . evaluateVariable = function ( name , options ) {
options = options || { } ;
var params = options . params || [ ] ;
// Get the details of the variable (includes processing text substitution for macros
var variableInfo = this . getVariableInfo ( name , { params : params , defaultValue : "" } ) ;
// Process function parameters
var variables = Object . create ( null ) ;
if ( variableInfo . srcVariable && variableInfo . srcVariable . isFunctionDefinition ) {
// Apply default parameter values
$tw . utils . each ( variableInfo . srcVariable . params , function ( param , index ) {
if ( param [ "default" ] ) {
variables [ param . name ] = param [ "default" ] ;
}
} ) ;
if ( $tw . utils . isArray ( params ) ) {
// Parameters are an array of values or {name:, value:} pairs
2023-01-23 16:34:08 +00:00
$tw . utils . each ( this . resolveVariableParameters ( variableInfo . srcVariable . params , params ) , function ( param ) {
variables [ param . name ] = param . value ;
2023-01-21 22:07:34 +00:00
} ) ;
} else if ( typeof params === "function" ) {
// Parameters are passed via a function
$tw . utils . each ( variableInfo . srcVariable . params , function ( param , index ) {
variables [ param . name ] = params ( param . name , index ) || param [ "default" ] || "" ;
} ) ;
} else {
// Parameters are a hashmap
$tw . utils . each ( params , function ( value , name ) {
variables [ name ] = value ;
} ) ;
}
return this . wiki . filterTiddlers ( variableInfo . text , this . makeFakeWidgetWithVariables ( variables ) , options . source ) ;
} else {
return [ variableInfo . text ] ;
}
} ;
2013-10-12 16:05:13 +00:00
/ *
2022-05-07 10:41:28 +00:00
Compute the current values of the attributes of the widget . Returns a hashmap of the names of the attributes that have changed .
Options include :
filterFn : only include attributes where filterFn ( name ) returns true
2013-10-12 16:05:13 +00:00
* /
2022-05-07 10:41:28 +00:00
Widget . prototype . computeAttributes = function ( options ) {
options = options || { } ;
2013-10-12 16:05:13 +00:00
var changedAttributes = { } ,
2021-10-30 10:42:22 +00:00
self = this ;
2013-10-12 16:05:13 +00:00
$tw . utils . each ( this . parseTreeNode . attributes , function ( attribute , name ) {
2022-05-07 10:41:28 +00:00
if ( options . filterFn ) {
if ( ! options . filterFn ( name ) ) {
return ;
}
}
2021-10-30 10:42:22 +00:00
var value = self . computeAttribute ( attribute ) ;
2013-10-12 16:05:13 +00:00
if ( self . attributes [ name ] !== value ) {
self . attributes [ name ] = value ;
changedAttributes [ name ] = true ;
}
} ) ;
return changedAttributes ;
} ;
2021-10-30 10:42:22 +00:00
Widget . prototype . computeAttribute = function ( attribute ) {
2022-05-23 19:18:54 +00:00
var self = this ,
value ;
2021-10-30 10:42:22 +00:00
if ( attribute . type === "filtered" ) {
value = this . wiki . filterTiddlers ( attribute . filter , this ) [ 0 ] || "" ;
} else if ( attribute . type === "indirect" ) {
value = this . wiki . getTextReference ( attribute . textReference , "" , this . getVariable ( "currentTiddler" ) ) ;
} else if ( attribute . type === "macro" ) {
2022-05-23 19:18:54 +00:00
var variableInfo = this . getVariableInfo ( attribute . value . name , { params : attribute . value . params } ) ;
if ( variableInfo . srcVariable && variableInfo . srcVariable . isFunctionDefinition ) {
2023-01-21 22:07:34 +00:00
// It is a function definition. Go through each of the defined parameters, and make a variable with the value of the corresponding provided parameter
var paramArray = this . resolveVariableParameters ( variableInfo . srcVariable . params , attribute . value . params ) ;
value = this . evaluateVariable ( attribute . value . name , { params : paramArray } ) [ 0 ] || "" ;
2022-05-23 19:18:54 +00:00
} else {
value = variableInfo . text ;
}
2021-10-30 10:42:22 +00:00
} else { // String attribute
value = attribute . value ;
}
return value ;
} ;
2013-10-12 16:05:13 +00:00
/ *
2022-04-16 17:02:27 +00:00
Check for the presence of an evaluated attribute on the widget . Note that attributes set to a missing variable ( ie attr = << missing >> ) will be treated as missing
2013-10-12 16:05:13 +00:00
* /
Widget . prototype . hasAttribute = function ( name ) {
return $tw . utils . hop ( this . attributes , name ) ;
} ;
2022-04-16 17:02:27 +00:00
/ *
Check for the presence of a raw attribute on the widget parse tree node . Note that attributes set to a missing variable ( ie attr = << missing >> ) will NOT be treated as missing
* /
Widget . prototype . hasParseTreeNodeAttribute = function ( name ) {
return $tw . utils . hop ( this . parseTreeNode . attributes , name ) ;
} ;
2013-10-12 16:05:13 +00:00
/ *
Get the value of an attribute
* /
Widget . prototype . getAttribute = function ( name , defaultText ) {
if ( $tw . utils . hop ( this . attributes , name ) ) {
return this . attributes [ name ] ;
} else {
return defaultText ;
}
} ;
/ *
Assign the computed attributes of the widget to a domNode
2014-03-12 16:39:18 +00:00
options include :
excludeEventAttributes : ignores attributes whose name begins with "on"
2013-10-12 16:05:13 +00:00
* /
2014-03-12 16:39:18 +00:00
Widget . prototype . assignAttributes = function ( domNode , options ) {
options = options || { } ;
2013-10-12 16:05:13 +00:00
var self = this ;
2022-02-21 15:24:06 +00:00
var assignAttribute = function ( name , value ) {
// Check for excluded attribute names
if ( options . excludeEventAttributes && name . substr ( 0 , 2 ) === "on" ) {
value = undefined ;
2014-03-12 16:39:18 +00:00
}
2022-02-21 15:24:06 +00:00
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 ) ;
}
// Handle styles
if ( name . substr ( 0 , 6 ) === "style." && name . length > 6 ) {
domNode . style [ $tw . utils . unHyphenateCss ( name . substr ( 6 ) ) ] = value ;
} else {
// Setting certain attributes can cause a DOM error (eg xmlns on the svg element)
try {
domNode . setAttributeNS ( namespace , name , value ) ;
} catch ( e ) {
2014-08-30 15:25:04 +00:00
}
2013-10-12 16:05:13 +00:00
}
}
2022-02-21 15:24:06 +00:00
}
// Not all parse tree nodes have the orderedAttributes property
if ( this . parseTreeNode . orderedAttributes ) {
$tw . utils . each ( this . parseTreeNode . orderedAttributes , function ( attribute , index ) {
assignAttribute ( attribute . name , self . attributes [ attribute . name ] ) ;
} ) ;
} else {
$tw . utils . each ( Object . keys ( self . attributes ) . sort ( ) , function ( name ) {
assignAttribute ( name , self . attributes [ name ] ) ;
} ) ;
}
2013-10-12 16:05:13 +00:00
} ;
2022-05-12 15:26:33 +00:00
/ *
Get the number of ancestor widgets for this widget
* /
Widget . prototype . getAncestorCount = function ( ) {
if ( this . ancestorCount === undefined ) {
if ( this . parentWidget ) {
this . ancestorCount = this . parentWidget . getAncestorCount ( ) + 1 ;
} else {
this . ancestorCount = 0 ;
}
}
return this . ancestorCount ;
} ;
2013-10-12 16:05:13 +00:00
/ *
Make child widgets correspondng to specified parseTreeNodes
* /
2021-03-19 15:37:59 +00:00
Widget . prototype . makeChildWidgets = function ( parseTreeNodes , options ) {
options = options || { } ;
2013-10-12 16:05:13 +00:00
this . children = [ ] ;
var self = this ;
2022-05-12 15:26:33 +00:00
// Check for too much recursion
if ( this . getAncestorCount ( ) > MAX _WIDGET _TREE _DEPTH ) {
this . children . push ( this . makeChildWidget ( { type : "error" , attributes : {
"$message" : { type : "string" , value : $tw . language . getString ( "Error/RecursiveTransclusion" ) }
} } ) ) ;
} 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 ) ) ;
} ) ;
}
2013-10-12 16:05:13 +00:00
} ;
/ *
Construct the widget object for a parse tree node
2021-03-19 15:37:59 +00:00
options include :
variables : optional hashmap of variables to wrap around the widget
2013-10-12 16:05:13 +00:00
* /
2021-03-19 15:37:59 +00:00
Widget . prototype . makeChildWidget = function ( parseTreeNode , options ) {
2022-04-24 20:24:38 +00:00
var self = this ;
2021-03-19 15:37:59 +00:00
options = options || { } ;
2022-05-21 14:47:19 +00:00
// Check whether this node type is defined by a custom widget definition
2022-09-24 11:41:28 +00:00
var variableDefinitionName = "$" + parseTreeNode . type ;
if ( this . variables [ variableDefinitionName ] ) {
var isOverrideable = function ( ) {
// Widget is overrideable if it has a double dollar user defined name, or if it is an existing JS widget and we're not in safe mode
return parseTreeNode . type . charAt ( 0 ) === "$" || ( ! ! 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 ;
}
}
2022-04-24 20:24:38 +00:00
}
// Get the widget class for this node type
2013-10-12 16:05:13 +00:00
var WidgetClass = this . widgetClasses [ parseTreeNode . type ] ;
if ( ! WidgetClass ) {
2014-08-30 19:44:26 +00:00
WidgetClass = this . widgetClasses . text ;
2013-10-12 16:05:13 +00:00
parseTreeNode = { type : "text" , text : "Undefined widget '" + parseTreeNode . type + "'" } ;
}
2021-03-19 15:37:59 +00:00
// 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 : [
parseTreeNode
]
} ;
parseTreeNode = setVariableWidget ;
} ) ;
2013-10-12 16:05:13 +00:00
return new WidgetClass ( parseTreeNode , {
wiki : this . wiki ,
parentWidget : this ,
document : this . document
} ) ;
} ;
2013-11-07 22:55:39 +00:00
/ *
Get the next sibling of this widget
* /
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 ;
} ;
/ *
Get the previous sibling of this widget
* /
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 ;
} ;
2013-10-12 16:05:13 +00:00
/ *
Render the children of this widget into the DOM
* /
Widget . prototype . renderChildren = function ( parent , nextSibling ) {
2019-07-14 15:20:27 +00:00
var children = this . children ;
for ( var i = 0 ; i < children . length ; i ++ ) {
children [ i ] . render ( parent , nextSibling ) ;
} ;
2013-10-12 16:05:13 +00:00
} ;
/ *
Add a list of event listeners from an array [ { type : , handler : } , ... ]
* /
Widget . prototype . addEventListeners = function ( listeners ) {
var self = this ;
$tw . utils . each ( listeners , function ( listenerInfo ) {
2014-05-13 09:15:55 +00:00
self . addEventListener ( listenerInfo . type , listenerInfo . handler ) ;
2013-10-12 16:05:13 +00:00
} ) ;
} ;
/ *
Add an event listener
* /
Widget . prototype . addEventListener = function ( type , handler ) {
var self = this ;
if ( typeof handler === "string" ) { // The handler is a method name on this widget
this . eventListeners [ type ] = function ( event ) {
return self [ handler ] . call ( self , event ) ;
} ;
2013-10-21 19:13:08 +00:00
} else { // The handler is a function
this . eventListeners [ type ] = function ( event ) {
return handler . call ( self , event ) ;
2014-08-30 19:44:26 +00:00
} ;
2013-10-12 16:05:13 +00:00
}
} ;
/ *
Dispatch an event to a widget . If the widget doesn ' t handle the event then it is also dispatched to the parent widget
* /
Widget . prototype . dispatchEvent = function ( event ) {
2020-05-09 14:53:38 +00:00
event . widget = event . widget || this ;
2013-10-12 16:05:13 +00:00
// Dispatch the event if this widget handles it
var listener = this . eventListeners [ event . type ] ;
if ( listener ) {
// Don't propagate the event if the listener returned false
if ( ! listener ( event ) ) {
return false ;
}
}
// Dispatch the event to the parent widget
if ( this . parentWidget ) {
return this . parentWidget . dispatchEvent ( event ) ;
}
return true ;
} ;
/ *
Selectively refreshes the widget if needed . Returns true if the widget or any of its children needed re - rendering
* /
Widget . prototype . refresh = function ( changedTiddlers ) {
return this . refreshChildren ( changedTiddlers ) ;
} ;
/ *
Rebuild a previously rendered widget
* /
Widget . prototype . refreshSelf = function ( ) {
2013-10-22 17:14:16 +00:00
var nextSibling = this . findNextSiblingDomNode ( ) ;
2013-10-12 16:05:13 +00:00
this . removeChildDomNodes ( ) ;
this . render ( this . parentDomNode , nextSibling ) ;
} ;
/ *
Refresh all the children of a widget
* /
Widget . prototype . refreshChildren = function ( changedTiddlers ) {
2019-07-14 15:20:27 +00:00
var children = this . children ,
2013-10-12 16:05:13 +00:00
refreshed = false ;
2019-07-14 15:20:27 +00:00
for ( var i = 0 ; i < children . length ; i ++ ) {
refreshed = children [ i ] . refresh ( changedTiddlers ) || refreshed ;
}
2013-10-12 16:05:13 +00:00
return refreshed ;
} ;
/ *
Find the next sibling in the DOM to this widget . This is done by scanning the widget tree through all next siblings and their descendents that share the same parent DOM node
* /
2013-10-22 17:14:16 +00:00
Widget . prototype . findNextSiblingDomNode = function ( startIndex ) {
2013-10-12 16:05:13 +00:00
// 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" ;
}
// Look for a DOM node in the later siblings
while ( ++ index < parent . children . length ) {
var domNode = parent . children [ index ] . findFirstDomNode ( ) ;
if ( domNode ) {
return domNode ;
}
}
// Go back and look for later siblings of our parent if it has the same parent dom node
2013-10-21 21:27:12 +00:00
var grandParent = parent . parentWidget ;
if ( grandParent && parent . parentDomNode === this . parentDomNode ) {
index = grandParent . children . indexOf ( parent ) ;
2015-05-03 15:56:37 +00:00
if ( index !== - 1 ) {
return parent . findNextSiblingDomNode ( index ) ;
}
2013-10-12 16:05:13 +00:00
}
return null ;
} ;
/ *
Find the first DOM node generated by a widget or its children
* /
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 ;
} ;
/ *
Remove any DOM nodes created by this widget or its children
* /
Widget . prototype . removeChildDomNodes = function ( ) {
2014-05-13 09:15:55 +00:00
// If this widget has directly created DOM nodes, delete them and exit. This assumes that any child widgets are contained within the created DOM nodes, which would normally be the case
2013-11-09 16:55:14 +00:00
if ( this . domNodes . length > 0 ) {
$tw . utils . each ( this . domNodes , function ( domNode ) {
domNode . parentNode . removeChild ( domNode ) ;
} ) ;
this . domNodes = [ ] ;
} else {
// Otherwise, ask the child widgets to delete their DOM nodes
$tw . utils . each ( this . children , function ( childWidget ) {
childWidget . removeChildDomNodes ( ) ;
} ) ;
}
2013-10-12 16:05:13 +00:00
} ;
2014-10-08 16:45:26 +00:00
/ *
2015-03-25 22:13:22 +00:00
Invoke the action widgets that are descendents of the current widget .
2014-10-08 16:45:26 +00:00
* /
2015-03-25 22:13:22 +00:00
Widget . prototype . invokeActions = function ( triggeringWidget , event ) {
2014-10-08 16:45:26 +00:00
var handled = false ;
2015-03-25 22:13:22 +00:00
// For each child widget
for ( var t = 0 ; t < this . children . length ; t ++ ) {
2021-07-02 13:33:38 +00:00
var child = this . children [ t ] ,
childIsActionWidget = ! ! child . invokeAction ,
2021-10-26 08:51:00 +00:00
actionRefreshPolicy = child . getVariable ( "tv-action-refresh-policy" ) ; // Default is "once"
2021-07-02 13:33:38 +00:00
// Refresh the child if required
if ( childIsActionWidget || actionRefreshPolicy === "always" ) {
2021-06-02 20:47:28 +00:00
child . refreshSelf ( ) ;
2021-07-02 13:33:38 +00:00
}
// Invoke the child if it is an action widget
if ( childIsActionWidget ) {
2016-10-08 09:19:09 +00:00
if ( child . invokeAction ( triggeringWidget , event ) ) {
handled = true ;
}
2014-10-08 16:45:26 +00:00
}
2015-03-25 22:13:22 +00:00
// Propagate through through the child if it permits it
if ( child . allowActionPropagation ( ) && child . invokeActions ( triggeringWidget , event ) ) {
2015-01-20 12:34:24 +00:00
handled = true ;
}
2014-10-08 16:45:26 +00:00
}
return handled ;
} ;
2016-04-29 17:53:23 +00:00
/ *
Invoke the action widgets defined in a string
* /
2017-03-19 19:33:56 +00:00
Widget . prototype . invokeActionString = function ( actions , triggeringWidget , event , variables ) {
2016-04-29 17:53:23 +00:00
actions = actions || "" ;
var parser = this . wiki . parseText ( "text/vnd.tiddlywiki" , actions , {
parentWidget : this ,
document : this . document
} ) ,
widgetNode = this . wiki . makeWidget ( parser , {
parentWidget : this ,
2017-03-19 19:33:56 +00:00
document : this . document ,
variables : variables
2016-04-29 17:53:23 +00:00
} ) ;
var container = this . document . createElement ( "div" ) ;
widgetNode . render ( container , null ) ;
return widgetNode . invokeActions ( this , event ) ;
} ;
2015-03-25 22:13:22 +00:00
2020-10-25 16:22:21 +00:00
/ *
Execute action tiddlers by tag
* /
2020-11-26 12:41:24 +00:00
Widget . prototype . invokeActionsByTag = function ( tag , event , variables ) {
2020-10-25 16:22:21 +00:00
var self = this ;
$tw . utils . each ( self . wiki . filterTiddlers ( "[all[shadows+tiddlers]tag[" + tag + "]!has[draft.of]]" ) , function ( title ) {
2020-11-26 12:41:24 +00:00
self . invokeActionString ( self . wiki . getTiddlerText ( title ) , self , event , variables ) ;
2020-10-25 16:22:21 +00:00
} ) ;
} ;
2015-03-25 22:13:22 +00:00
Widget . prototype . allowActionPropagation = function ( ) {
return true ;
} ;
2023-04-13 07:42:18 +00:00
/ *
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 ) ;
}
} ) ;
} ;
2013-10-12 16:05:13 +00:00
exports . widget = Widget ;
} ) ( ) ;