nodegame-widgets
Version:
Collections of useful and reusable javascript / HTML snippets for nodeGame
1,484 lines (1,368 loc) • 724 kB
JavaScript
/**
* # Widget
* Copyright(c) 2020 Stefano Balietti <ste@nodegame.org>
* MIT Licensed
*
* Prototype of a widget class
*
* Prototype methods will be injected in every new widget, if missing.
*
* Additional properties can be automatically, depending on configuration.
*
* @see Widgets.get
* @see Widgets.append
*/
(function(node) {
"use strict";
var J = node.JSUS;
var NDDB = node.NDDB;
node.Widget = Widget;
/**
* ### Widget constructor
*
* Creates a new instance of widget
*
* Should create all widgets properties, but the `init` method
* initialize them. Some properties are added automatically
* by `Widgets.get` after the constructor has been called,
* but before `init`.
*
* @see Widgets.get
* @see Widget.init
*/
function Widget() {}
/**
* ### Widget.init
*
* Inits the widget after constructor and default properties are added
*
* @param {object} opts Configuration options
*
* @see Widgets.get
*/
Widget.prototype.init = function(opts) {};
/**
* ### Widget.listeners
*
* Wraps calls event listeners registration
*
* Event listeners registered here are automatically removed
* when widget is destroyed (if still active)
*
* @see EventEmitter.setRecordChanges
* @see Widgets.destroy
*/
Widget.prototype.listeners = function() {};
/**
* ### Widget.append
*
* Creates HTML elements and appends them to the `panelDiv` element
*
* The method is called by `Widgets.append` which evaluates user-options
* and adds the default container elements of a widget:
*
* - panelDiv: the outer container
* - headingDiv: the title container
* - bodyDiv: the main container
* - footerDiv: the footer container
*
* To ensure correct destroyal of the widget, all HTML elements should
* be children of Widget.panelDiv
*
* @see Widgets.append
* @see Widgets.destroy
* @see Widget.panelDiv
* @see Widget.footerDiv
* @see Widget.headingDiv
*/
Widget.prototype.append = function() {};
/**
* ### Widget.getValues
*
* Returns the values currently stored by the widget
*
* @param {mixed} opts Settings controlling the content of return value
*
* @return {mixed} The values of the widget
*/
Widget.prototype.getValues = function(opts) {};
/**
* ### Widget.setValues
*
* Set the stored values directly
*
* The method should not set the values, if widget is disabled
*
* @param {mixed} values The values to store
*/
Widget.prototype.setValues = function(values) {};
/**
* ### Widget.reset
*
* Resets the widget
*
* Deletes current selection, any highlighting, and other data
* that the widget might have collected to far.
*/
Widget.prototype.reset = function(opts) {};
/**
* ### Widget.highlight
*
* Hightlights the user interface of the widget in some way
*
* By default, it adds a red border around the bodyDiv.
*
* If widget was not appended, i.e., no `panelDiv` has been created,
* nothing happens.
*
* @param {mixed} options Settings controlling the type of highlighting
*/
Widget.prototype.highlight = function(border) {
if (border && 'string' !== typeof border) {
throw new TypeError(J.funcName(this.constructor) + '.highlight: ' +
'border must be string or undefined. Found: ' +
border);
}
if (!this.isAppended() || this.isHighlighted()) return;
this.highlighted = true;
this.bodyDiv.style.border = border || '3px solid red';
this.emit('highlighted', border);
};
/**
* ### Widget.highlight
*
* Hightlights the user interface of the widget in some way
*
* Should mark the state of widget as not `highlighted`.
*
* If widget was not appended, i.e., no `panelDiv` has been created,
* nothing happens.
*
* @see Widget.highlight
*/
Widget.prototype.unhighlight = function() {
if (!this.isHighlighted()) return;
this.highlighted = false;
this.bodyDiv.style.border = '';
this.emit('unhighlighted');
};
/**
* ### Widget.isHighlighted
*
* Returns TRUE if widget is currently highlighted
*
* @return {boolean} TRUE, if widget is currently highlighted
*/
Widget.prototype.isHighlighted = function() {
return !!this.highlighted;
};
/**
* ### Widget.isDocked
*
* Returns TRUE if widget is currently docked
*
* @return {boolean} TRUE, if widget is currently docked
*/
Widget.prototype.isDocked = function() {
return !!this.docked;
};
/**
* ### Widget.isActionRequired
*
* Returns TRUE if widget if the widget does not required action from user
*
* If the widget does not have the `required` flag it returns FALSE,
* otherwise it invokes Widget.getValues and looks in the response for
* incorrect or missing values.
*
* @param {object} opts Optional. Options to pass to Widget.getValues.
* Default: { markAttempt: false, highlight: false };
*
* @return {boolean} TRUE, if action is required
*/
Widget.prototype.isActionRequired = function(opts) {
var values;
if (!this.required) return false;
opts = opts || {};
opts.markAttempt = opts.markAttempt || false;
opts.highlight = opts.highlight || false;
values = this.getValues(opts);
if (!values) return false; // Safety check.
// TODO: check removed: values.missValues === true ||
return values.choice === null || values.isCorrect === false;
};
/**
* ### Widget.collapse
*
* Collapses the widget (hides the body and footer)
*
* Only, if it was previously appended to DOM
*
* @see Widget.uncollapse
* @see Widget.isCollapsed
*/
Widget.prototype.collapse = function() {
if (!this.panelDiv) return;
this.bodyDiv.style.display = 'none';
this.collapsed = true;
if (this.collapseButton) {
this.collapseButton.src = '/images/maximize_small2.png';
this.collapseButton.title = 'Restore';
}
if (this.footer) this.footer.style.display = 'none';
// Move into collapse target, if one is specified.
if (this.collapseTarget) this.collapseTarget.appendChild(this.panelDiv);
this.emit('collapsed');
};
/**
* ### Widget.uncollapse
*
* Uncollapses the widget (shows the body and footer)
*
* Only if it was previously appended to DOM
*
* @see Widget.collapse
* @see Widget.isCollapsed
*/
Widget.prototype.uncollapse = function() {
if (!this.panelDiv) return;
if (this.collapseTarget) {
this.originalRoot.appendChild(this.panelDiv);
}
this.bodyDiv.style.display = '';
this.collapsed = false;
if (this.collapseButton) {
this.collapseButton.src = '/images/maximize_small.png';
this.collapseButton.title = 'Minimize';
}
if (this.footer) this.footer.style.display = '';
this.emit('uncollapsed');
};
/**
* ### Widget.isCollapsed
*
* Returns TRUE if widget is currently collapsed
*
* @return {boolean} TRUE, if widget is currently collapsed
*/
Widget.prototype.isCollapsed = function() {
return !!this.collapsed;
};
/**
* ### Widget.enable
*
* Enables the widget
*
* An enabled widget allows the user to interact with it
*/
Widget.prototype.enable = function(options) {
if (!this.disabled) return;
this.disabled = false;
this.emit('enabled', options);
};
/**
* ### Widget.disable
*
* Disables the widget
*
* A disabled widget is still visible, but user cannot interact with it
*/
Widget.prototype.disable = function(options) {
if (this.disabled) return;
this.disabled = true;
this.emit('disabled', options);
};
/**
* ### Widget.isDisabled
*
* Returns TRUE if widget is disabled
*
* @return {boolean} TRUE if widget is disabled
*
* @see Widget.enable
* @see Widget.disable
* @see Widget.disabled
*/
Widget.prototype.isDisabled = function() {
return !!this.disabled;
};
/**
* ### Widget.hide
*
* Hides the widget, if it was previously appended to DOM
*
* Sets the 'display' property of `panelDiv` to 'none'
*
* @see Widget.show
* @see Widget.toggle
*/
Widget.prototype.hide = function() {
if (!this.panelDiv) return;
if (this.hidden) return;
this.panelDiv.style.display = 'none';
this.hidden = true;
this.emit('hidden');
};
/**
* ### Widget.show
*
* Shows the widget, if it was previously appended and hidden
*
* Sets the 'display' property of `panelDiv` to ''
*
* @param {string} display Optional. The value of the display
* property. Default: ''
*
* @see Widget.hide
* @see Widget.toggle
*/
Widget.prototype.show = function(display) {
if (this.panelDiv && this.panelDiv.style.display === 'none') {
this.panelDiv.style.display = display || '';
this.hidden = false;
this.emit('shown');
}
};
/**
* ### Widget.toggle
*
* Toggles the display of the widget, if it was previously appended
*
* @param {string} display Optional. The value of the display
* property in case the widget is currently hidden. Default: ''
*
* @see Widget.hide
*/
Widget.prototype.toggle = function(display) {
if (!this.panelDiv) return;
if (this.hidden) this.show();
else this.hide();
};
/**
* ### Widget.isHidden
*
* TRUE if widget is hidden or not yet appended
*
* @return {boolean} TRUE if widget is hidden, or if it was not
* appended to the DOM yet
*/
Widget.prototype.isHidden = function() {
return !!this.hidden;
};
/**
* ### Widget.destroy
*
* Performs cleanup operations
*
* `Widgets.get` wraps this method in an outer callback performing
* default cleanup operations.
*
* @see Widgets.get
*/
Widget.prototype.destroy = null;
/**
* ### Widget.setTitle
*
* Creates/removes an heading div with a given title
*
* Adds/removes a div with class `panel-heading` to the `panelDiv`.
*
* @param {string|HTMLElement|false} Optional. The title for the heading,
* div an HTML element, or false to remove the header completely.
* @param {object} Optional. Options to be passed to `W.add` if a new
* heading div is created. Default: { className: 'panel-heading' }
*
* @see Widget.headingDiv
* @see GameWindow.add
*/
Widget.prototype.setTitle = function(title, options) {
var tmp;
if (!this.panelDiv) {
throw new Error('Widget.setTitle: panelDiv is missing.');
}
// Remove heading with false-ish argument.
if (!title) {
if (this.headingDiv) {
this.panelDiv.removeChild(this.headingDiv);
this.headingDiv = null;
}
}
else {
if (!this.headingDiv) {
// Add heading.
if (!options) {
// Bootstrap 3
// options = { className: 'panel-heading' };
options = { className: 'card-header' };
}
else if ('object' !== typeof options) {
throw new TypeError('Widget.setTitle: options must ' +
'be object or undefined. Found: ' +
options);
}
this.headingDiv = W.add('div', this.panelDiv, options);
// Move it to before the body (IE cannot have undefined).
tmp = (this.bodyDiv && this.bodyDiv.childNodes[0]) || null;
this.panelDiv.insertBefore(this.headingDiv, tmp);
}
// Set title.
if (W.isElement(title)) {
// The given title is an HTML element.
this.headingDiv.innerHTML = '';
this.headingDiv.appendChild(title);
}
else if ('string' === typeof title) {
this.headingDiv.innerHTML = title;
}
else {
throw new TypeError(J.funcName(this.constructor) +
'.setTitle: title must be string, ' +
'HTML element or falsy. Found: ' + title);
}
if (this.collapsible) {
// Generates a button that hides the body of the panel.
(function(that) {
var link, img;
link = document.createElement('span');
link.className = 'panel-collapse-link';
img = document.createElement('img');
img.src = '/images/minimize_small.png';
link.appendChild(img);
link.onclick = function() {
if (that.isCollapsed()) that.uncollapse();
else that.collapse();
};
that.headingDiv.appendChild(link);
})(this);
}
if (this.closable) {
(function(that) {
var link, img;
link = document.createElement('span');
link.className = 'panel-collapse-link';
// link.style['margin-right'] = '8px';
img = document.createElement('img');
img.src = '/images/close_small.png';
link.appendChild(img);
link.onclick = function() {
that.destroy();
};
that.headingDiv.appendChild(link);
})(this);
}
if (this.info) {
(function(that) {
var link, img, a;
link = W.add('span', that.headingDiv);
link.className = 'panel-collapse-link';
// link.style['margin-right'] = '8px';
a = W.add('a', link);
a.href = that.info;
a.target = '_blank';
img = W.add('img', a);
img.src = '/images/info.png';
})(this);
}
}
};
/**
* ### Widget.setFooter
*
* Creates/removes a footer div with a given content
*
* Adds/removes a div with class `panel-footer` to the `panelDiv`.
*
* @param {string|HTMLElement|false} Optional. The title for the header,
* an HTML element, or false to remove the header completely.
* @param {object} Optional. Options to be passed to `W.add` if a new
* footer div is created. Default: { className: 'panel-footer' }
*
* @see Widget.footerDiv
* @see GameWindow.add
*/
Widget.prototype.setFooter = function(footer, options) {
if (!this.panelDiv) {
throw new Error('Widget.setFooter: panelDiv is missing.');
}
// Remove footer with false-ish argument.
if (!footer) {
if (this.footerDiv) {
this.panelDiv.removeChild(this.footerDiv);
delete this.footerDiv;
}
}
else {
if (!this.footerDiv) {
// Add footer.
if (!options) {
// Bootstrap 3.
// options = { className: 'panel-footer' };
// Bootstrap 5.
options = { className: 'card-footer' };
}
else if ('object' !== typeof options) {
throw new TypeError('Widget.setFooter: options must ' +
'be object or undefined. Found: ' +
options);
}
this.footerDiv = W.add('div', this.panelDiv, options);
}
// Set footer contents.
if (W.isElement(footer)) {
// The given footer is an HTML element.
this.footerDiv.innerHTML = '';
this.footerDiv.appendChild(footer);
}
else if ('string' === typeof footer) {
this.footerDiv.innerHTML = footer;
}
else {
throw new TypeError(J.funcName(this.constructor) +
'.setFooter: footer must be string, ' +
'HTML element or falsy. Found: ' + footer);
}
}
};
/**
* ### Widget.setContext
*
* @deprecated
*/
Widget.prototype.setContext = function() {
console.log('*** Deprecation warning: setContext no longer ' +
'available in Bootstrap5.');
};
/**
* ### Widget.addFrame
*
* Adds a border and margins around the bodyDiv element
*
* @param {string} context The type of bootstrap context.
* Default: 'default'
*
* @see Widget.panelDiv
* @see Widget.bodyDiv
*/
Widget.prototype.addFrame = function(context) {
if ('undefined' === typeof context) {
context = 'default';
}
else if ('string' !== typeof context || context.trim() === '') {
throw new TypeError(J.funcName(this.constructor) +
'.addFrame: context must be a non-empty ' +
'string or undefined. Found: ' + context);
}
if (this.panelDiv) {
if (this.panelDiv.className.indexOf('panel-') === -1) {
W.addClass(this.panelDiv, 'panel-' + context);
}
}
if (this.bodyDiv) {
if (this.bodyDiv.className.indexOf('panel-body') === -1) {
W.addClass(this.bodyDiv, 'panel-body');
}
}
};
/**
* ### Widget.removeFrame
*
* Removes the border and the margins around the bodyDiv element
*
* @see Widget.panelDiv
* @see Widget.bodyDiv
*/
Widget.prototype.removeFrame = function() {
if (this.panelDiv) W.removeClass(this.panelDiv, 'panel-[a-z]*');
if (this.bodyDiv) W.removeClass(this.bodyDiv, 'panel-body');
};
/**
* ### Widget.isAppended
*
* Returns TRUE if widget was appended to DOM (using Widget API)
*
* Checks if the panelDiv element has been created.
*
* @return {boolean} TRUE, if node.widgets.append was called
*/
Widget.prototype.isAppended = function() {
return !!this.panelDiv;
};
/**
* ### Widget.isDestroyed
*
* Returns TRUE if widget has been destroyed
*
* @return {boolean} TRUE, if the widget has been destroyed
*/
Widget.prototype.isDestroyed = function() {
return !!this.destroyed;
};
/**
* ### Widget.setSound
*
* Checks and assigns the value of a sound to play to user
*
* Throws an error if value is invalid
*
* @param {string} name The name of the sound to check
* @param {mixed} path Optional. The path to the audio file. If undefined
* the default value from Widget.sounds is used
*
* @see Widget.sounds
* @see Widget.getSound
* @see Widget.setSounds
* @see Widget.getSounds
*/
Widget.prototype.setSound = function(name, value) {
strSetter(this, name, value, 'sounds', 'Widget.setSound');
};
/**
* ### Widget.setSounds
*
* Assigns multiple sounds at the same time
*
* @param {object} sounds Optional. Object containing sound paths
*
* @see Widget.sounds
* @see Widget.setSound
* @see Widget.getSound
* @see Widget.getSounds
*/
Widget.prototype.setSounds = function(sounds) {
strSetterMulti(this, sounds, 'sounds', 'setSound',
J.funcName(this.constructor) + '.setSounds');
};
/**
* ### Widget.getSound
*
* Returns the requested sound path
*
* @param {string} name The name of the sound variable.
* @param {mixed} param Optional. Additional info to pass to the
* callback, if any
*
* @return {string} The requested sound
*
* @see Widget.sounds
* @see Widget.setSound
* @see Widget.getSound
* @see Widget.getSounds
*/
Widget.prototype.getSound = function(name, param) {
return strGetter(this, name, 'sounds',
J.funcName(this.constructor) + '.getSound', param);
};
/**
* ### Widget.getSounds
*
* Returns an object with selected sounds (paths)
*
* @param {object|array} keys Optional. An object whose keys, or an array
* whose values, are used of to select the properties to return.
* Default: all properties in the collection object.
* @param {object} param Optional. Object containing parameters to pass
* to the sounds functions (if any)
*
* @return {object} Selected sounds (paths)
*
* @see Widget.sounds
* @see Widget.setSound
* @see Widget.getSound
* @see Widget.setSounds
*/
Widget.prototype.getSounds = function(keys, param) {
return strGetterMulti(this, 'sounds', 'getSound',
J.funcName(this.constructor)
+ '.getSounds', keys, param);
};
/**
* ### Widget.getAllSounds
*
* Returns an object with all current sounds
*
* @param {object} param Optional. Object containing parameters to pass
* to the sounds functions (if any)
*
* @return {object} All current sounds
*
* @see Widget.getSound
*/
Widget.prototype.getAllSounds = function(param) {
return strGetterMulti(this, 'sounds', 'getSound',
J.funcName(this.constructor) + '.getAllSounds',
undefined, param);
};
/**
* ### Widget.setText
*
* Checks and assigns the value of a text to display to user
*
* Throws an error if value is invalid
*
* @param {string} name The name of the property to check
* @param {mixed} value Optional. The value for the text. If undefined
* the default value from Widget.texts is used
*
* @see Widget.texts
* @see Widget.getText
* @see Widget.setTexts
* @see Widget.getTexts
*/
Widget.prototype.setText = function(name, value) {
strSetter(this, name, value, 'texts',
J.funcName(this.constructor) + '.setText');
};
/**
* ### Widget.setTexts
*
* Assigns all texts
*
* @param {object} texts Optional. Object containing texts
*
* @see Widget.texts
* @see Widget.setText
* @see Widget.getText
* @see Widget.getTexts
*/
Widget.prototype.setTexts = function(texts) {
strSetterMulti(this, texts, 'texts', 'setText',
J.funcName(this.constructor) + '.setTexts');
};
/**
* ### Widget.getText
*
* Returns the requested text
*
* @param {string} name The name of the text variable.
* @param {mixed} param Optional. Additional to pass to the callback, if any
*
* @return {string} The requested text
*
* @see Widget.texts
* @see Widget.setText
* @see Widget.setTexts
* @see Widget.getTexts
*/
Widget.prototype.getText = function(name, param) {
return strGetter(this, name, 'texts',
J.funcName(this.constructor) + '.getText', param);
};
/**
* ### Widget.getTexts
*
* Returns an object with selected texts
*
* @param {object|array} keys Optional. An object whose keys, or an array
* whose values, are used of to select the properties to return.
* Default: all properties in the collection object.
* @param {object} param Optional. Object containing parameters to pass
* to the sounds functions (if any)
*
* @return {object} Selected texts
*
* @see Widget.texts
* @see Widget.setText
* @see Widget.getText
* @see Widget.setTexts
* @see Widget.getAllTexts
*/
Widget.prototype.getTexts = function(keys, param) {
return strGetterMulti(this, 'texts', 'getText',
J.funcName(this.constructor)
+ '.getTexts', keys, param);
};
/**
* ### Widget.getAllTexts
*
* Returns an object with all current texts
*
* @param {object|array} param Optional. Object containing parameters
* to pass to the texts functions (if any)
*
* @return {object} All current texts
*
* @see Widget.texts
* @see Widget.setText
* @see Widget.setTexts
* @see Widget.getText
*/
Widget.prototype.getAllTexts = function(param) {
return strGetterMulti(this, 'texts', 'getText',
J.funcName(this.constructor)
+ '.getAllTexts', undefined, param);
};
// ## Event-Emitter methods borrowed from NDDB
/**
* ### Widget.on
*
* Registers an event listener for the widget
*
* @see NDDB.off
*/
Widget.prototype.on = function() {
NDDB.prototype.on.apply(this, arguments);
};
/**
* ### Widget.off
*
* Removes and event listener for the widget
*
* @see NDDB.off
*/
Widget.prototype.off = function() {
NDDB.prototype.off.apply(this, arguments);
};
/**
* ### Widget.emit
*
* Emits an event within the widget
*
* @see NDDB.emit
*/
Widget.prototype.emit = function() {
NDDB.prototype.emit.apply(this, arguments);
};
/**
* ### Widget.throwErr
*
* Get the name of the actual widget and throws the error
*
* It does **not** perform type checking on itw own input parameters.
*
* @param {string} type Optional. The error type, e.g. 'TypeError'.
* Default, 'Error'
* @param {string} method Optional. The name of the method
* @param {string|object} err Optional. The error. Default, 'generic error'
*
* @see NDDB.throwErr
*/
Widget.prototype.throwErr = function(type, method, err) {
var errMsg;
errMsg = J.funcName(this.constructor) + '.' + method + ': ';
if ('object' === typeof err) errMsg += err.stack || err;
else if ('string' === typeof err) errMsg += err;
if (type === 'TypeError') throw new TypeError(errMsg);
throw new Error(errMsg);
};
// ## Helper methods.
/**
* ### strGetter
*
* Returns the value a property from a collection in instance/constructor
*
* If the string is not found in the live instance, the default value
* from the same collection inside the contructor is returned instead.
*
* If the property is not found in the corresponding static
* collection in the constructor of the instance, an error is thrown.
*
* @param {object} that The main instance
* @param {string} name The name of the property inside the collection
* @param {string} collection The name of the collection inside the instance
* @param {string} method The name of the invoking method (for error string)
* @param {mixed} param Optional. If the value of the requested property
* is a function, this parameter is passed to it to get a return value.
*
* @return {string} res The value of requested property as found
* in the instance, or its default value as found in the constructor
*/
function strGetter(that, name, collection, method, param) {
var res;
if (!that.constructor[collection].hasOwnProperty(name)) {
throw new Error(method + ': name not found: ' + name);
}
res = 'undefined' !== typeof that[collection][name] ?
that[collection][name] : that.constructor[collection][name];
if ('function' === typeof res) {
res = res(that, param);
if ('string' !== typeof res && res !== false) {
throw new TypeError(method + ': cb "' + name + '" did not ' +
'return neither string or false. Found: ' +
res);
}
}
return res;
}
/**
* ### strGetterMulti
*
* Same as strGetter, but returns multiple values at once
*
* @param {object} that The main instance
* @param {string} collection The name of the collection inside the instance
* @param {string} getMethod The name of the method to get each value
* @param {string} method The name of the invoking method (for error string)
* @param {object|array} keys Optional. An object whose keys, or an array
* whose values, are used of this object are to select the properties
* to return. Default: all properties in the collection object.
* @param {mixed} param Optional. If the value of the requested property
* is a function, this parameter is passed to it, when invoked to get
* a return value. Default: undefined
*
* @return {string} out The requested value.
*
* @see strGetter
*/
function strGetterMulti(that, collection, getMethod, method, keys, param) {
var out, k, len;
if (!keys) keys = that.constructor[collection];
if ('undefined' === typeof param) {
param = {};
}
out = {};
if (J.isArray(keys)) {
k = -1, len = keys.length;
for ( ; ++k < len;) {
out[keys[k]] = that[getMethod](keys[k], param);
}
}
else {
for (k in keys) {
if (keys.hasOwnProperty(k)) {
out[k] = that[getMethod](k, param);
}
}
}
return out;
}
/**
* ### strSetterMulti
*
* Same as strSetter, but sets multiple values at once
*
* @param {object} that The main instance
* @param {object} obj List of properties to set and their values
* @param {string} collection The name of the collection inside the instance
* @param {string} setMethod The name of the method to set each value
* @param {string} method The name of the invoking method (for error string)
*
* @see strSetter
*/
function strSetterMulti(that, obj, collection, setMethod, method) {
var i;
if ('object' !== typeof obj && 'undefined' !== typeof obj) {
throw new TypeError(method + ': ' + collection +
' must be object or undefined. Found: ' + obj);
}
for (i in obj) {
if (obj.hasOwnProperty(i)) {
that[setMethod](i, obj[i]);
}
}
}
/**
* ### strSetter
*
* Sets the value of a property in a collection if string, function or false
*
* @param {object} that The main instance
* @param {string} name The name of the property to set
* @param {string|function|false} value The value for the property
* @param {string} collection The name of the collection inside the instance
* @param {string} method The name of the invoking method (for error string)
*
* @see strSetter
*/
function strSetter(that, name, value, collection, method) {
if ('undefined' === typeof that.constructor[collection][name]) {
throw new TypeError(method + ': name not found: ' + name);
}
if ('string' === typeof value ||
'function' === typeof value ||
false === value) {
that[collection][name] = value;
}
else {
throw new TypeError(method + ': value for item "' + name +
'" must be string, function or false. ' +
'Found: ' + value);
}
}
})(
// Widgets works only in the browser environment.
('undefined' !== typeof node) ? node : module.parent.exports.node
);
/**
* # Widgets
* Copyright(c) 2021 Stefano Balietti
* MIT Licensed
*
* Helper class to interact with nodeGame widgets
*
* http://nodegame.org
*/
(function(window, node) {
"use strict";
// ## Widgets constructor
function Widgets() {
var that;
/**
* ### Widgets.widgets
*
* Container of currently registered widgets
*
* @see Widgets.register
*/
this.widgets = {};
/**
* ### Widgets.instances
*
* Container of appended widget instances
*
* @see Widgets.append
* @see Widgets.lastAppended
*/
this.instances = [];
/**
* ### Widgets.lastAppended
*
* Reference to lastAppended widget
*
* @see Widgets.append
*/
this.lastAppended = null;
/**
* ### Widgets.docked
*
* List of docked widgets
*/
this.docked = [];
/**
* ### Widgets.dockedHidden
*
* List of hidden docked widgets (cause not enough space on page)
*/
this.dockedHidden = [];
/**
* ### Widgets.boxSelector
*
* A box selector widget containing hidden docked widgets
*/
this.boxSelector = null;
/**
* ### Widgets.collapseTarget
*
* Collapsed widgets are by default moved inside element
*/
this.collapseTarget = null;
that = this;
node.registerSetup('widgets', function(conf) {
var name, root, collapseTarget;
if (!conf) return;
// Add new widgets.
if (conf.widgets) {
for (name in conf.widgets) {
if (conf.widgets.hasOwnProperty(name)) {
that.register(name, conf.widgets[name]);
}
}
}
// Destroy all existing widgets.
if (conf.destroyAll) that.destroyAll();
// Append existing widgets.
if (conf.append) {
for (name in conf.append) {
if (conf.append.hasOwnProperty(name)) {
// Determine root.
root = conf.append[name].root;
if ('function' === typeof root) {
root = root();
}
else if ('string' === typeof root) {
root = W.getElementById(root);
}
if (!root) root = W.getScreen();
if (!root) {
node.warn('setup widgets: could not find a root ' +
'for widget ' + name + '. Requested: ' +
conf.append[name].root);
}
else {
that.append(name, root, conf.append[name]);
}
}
}
}
if (conf.collapseTarget) {
if ('function' === typeof conf.collapseTarget) {
collapseTarget = conf.collapseTarget();
}
else if ('string' === typeof conf.collapseTarget) {
collapseTarget = W.getElementById(conf.collapseTarget);
}
else if (J.isElement(conf.collapseTarget)) {
collapseTarget = conf.collapseTarget;
}
if (!collapseTarget) {
node.warn('setup widgets: could not find collapse target.');
}
else {
that.collapseTarget = collapseTarget;
}
}
return conf;
});
// Garbage collection.
node.on('FRAME_LOADED', function() {
node.widgets.garbageCollection();
});
node.info('node-widgets: loading');
}
// ## Widgets methods
/**
* ### Widgets.register
*
* Registers a new widget in the collection
*
* A name and a prototype class must be provided. All properties
* that are present in `node.Widget`, but missing in the prototype
* are added.
*
* Registered widgets can be loaded with Widgets.get or Widgets.append.
*
* @param {string} name The id under which to register the widget
* @param {function} w The widget to add
*
* @return {object|boolean} The registered widget,
* or FALSE if an error occurs
*/
Widgets.prototype.register = function(name, w) {
if ('string' !== typeof name) {
throw new TypeError('Widgets.register: name must be string. ' +
'Found: ' + name);
}
if ('function' !== typeof w) {
throw new TypeError('Widgets.register: w must be function.' +
'Found: ' + w);
}
if ('undefined' === typeof w.sounds) w.sounds = {};
if ('undefined' === typeof w.texts) w.texts = {};
// Add default properties to widget prototype.
J.mixout(w.prototype, new node.Widget());
this.widgets[name] = w;
return this.widgets[name];
};
/**
* ### Widgets.get
*
* Retrieves, instantiates and returns the specified widget
*
* Performs the following checkings:
*
* - dependencies, as specified by widget prototype, must exist
* - id, if specified in options, must be string
*
* and throws an error if conditions are not met.
*
* Adds the following properties to the widget object:
*
* - title: as specified by the user or as found in the prototype
* - footer: as specified by the user or as found in the prototype
* - context: as specified by the user or as found in the prototype
* - className: as specified by the user or as found in the prototype
* - id: user-defined id
* - wid: random unique widget id
* - hooks: object containing event listeners
* - disabled: boolean flag indicating the widget state, set to FALSE
* - highlighted: boolean flag indicating whether the panelDiv is
* highlighted, set to FALSE
* - collapsible: boolean flag, TRUE if the widget can be collapsed
* and a button to hide body is added to the header
* - collapsed: boolan flag, TRUE if widget is collapsed (body hidden)
* - closable: boolean flag, TRUE if the widget can be closed (destroyed)
*
* Calls the `listeners` method of the widget. Any event listener
* registered here will be automatically removed when the widget
* is destroyed. !Important: it will erase previously recorded changes
* by the event listener. If `options.listeners` is equal to false, the
* listeners method is skipped.
*
* A `.destroy` method is added to the widget that perform the
* following operations:
*
* - removes the widget from DOM (if it was appended),
* - removes listeners defined during the creation,
* - and remove the widget from Widget.instances,
* - invoke the event 'destroyed'.
*
*
* Finally, a reference to the widget is added in `Widgets.instances`.
*
* @param {string} widgetName The name of the widget to load
* @param {object} options Optional. Configuration options, will be
* mixed out with attributes in the `defaults` property
* of the widget prototype.
*
* @return {object} widget The requested widget
*
* @see Widgets.append
* @see Widgets.instances
*/
Widgets.prototype.get = function(widgetName, options) {
var WidgetPrototype, widget, changes, tmp;
if ('string' !== typeof widgetName) {
throw new TypeError('Widgets.get: widgetName must be string.' +
'Found: ' + widgetName);
}
if (!options) {
options = {};
}
else if ('object' !== typeof options) {
throw new TypeError('Widgets.get: ' + widgetName + ' options ' +
'must be object or undefined. Found: ' +
options);
}
if (options.storeRef === false) {
if (options.docked === true) {
throw new TypeError('Widgets.get: ' + widgetName +
'options.storeRef cannot be false ' +
'if options.docked is true.');
}
}
WidgetPrototype = J.getNestedValue(widgetName, this.widgets);
if (!WidgetPrototype) {
throw new Error('Widgets.get: ' + widgetName + ' not found');
}
node.info('creating widget ' + widgetName +
' v.' + WidgetPrototype.version);
if (!this.checkDependencies(WidgetPrototype)) {
throw new Error('Widgets.get: ' + widgetName + ' has unmet ' +
'dependencies');
}
// Create widget.
widget = new WidgetPrototype(options);
// Set ID.
tmp = options.id;
if ('undefined' !== typeof tmp) {
if ('number' === typeof tmp) tmp += '';
if ('string' === typeof tmp) {
if ('undefined' !== typeof options.idPrefix) {
if ('string' === typeof options.idPrefix &&
'number' !== typeof options.idPrefix) {
tmp = options.idPrefix + tmp;
}
else {
throw new TypeError('Widgets.get: options.idPrefix ' +
'must be string, number or ' +
'undefined. Found: ' +
options.idPrefix);
}
}
widget.id = tmp;
}
else {
throw new TypeError('Widgets.get: options.id must be ' +
'string, number or undefined. Found: ' +
tmp);
}
}
// Assign step id as widget id, if widget step and no custom id.
else if (options.widgetStep) {
widget.id = node.game.getStepId();
}
// Set prototype values or options values.
if ('undefined' !== typeof options.title) {
widget.title = options.title;
}
else if ('undefined' !== typeof WidgetPrototype.title) {
widget.title = WidgetPrototype.title;
}
else {
widget.title = ' ';
}
widget.panel = 'undefined' === typeof options.panel ?
WidgetPrototype.panel : options.panel;
widget.footer = 'undefined' === typeof options.footer ?
WidgetPrototype.footer : options.footer;
widget.className = WidgetPrototype.className;
if (J.isArray(options.className)) {
widget.className += ' ' + options.className.join(' ');
}
else if ('string' === typeof options.className) {
widget.className += ' ' + options.className;
}
else if ('undefined' !== typeof options.className) {
throw new TypeError('Widgets.get: className must be array, ' +
'string, or undefined. Found: ' +
options.className);
}
widget.context = 'undefined' === typeof options.context ?
WidgetPrototype.context : options.context;
widget.sounds = 'undefined' === typeof options.sounds ?
WidgetPrototype.sounds : options.sounds;
widget.texts = 'undefined' === typeof options.texts ?
WidgetPrototype.texts : options.texts;
widget.collapsible = options.collapsible || false;
widget.closable = options.closable || false;
widget.collapseTarget =
options.collapseTarget || this.collapseTarget || null;
widget.info = options.info || false;
widget.hooks = {
hidden: [],
shown: [],
collapsed: [],
uncollapsed: [],
disabled: [],
enabled: [],
destroyed: [],
highlighted: [],
unhighlighted: []
};
// By default destroy widget on exit step.
widget.destroyOnExit = options.destroyOnExit !== false;
// Required widgets require action from user, otherwise they will
// block node.done().
if (options.required ||
options.requiredChoice ||
'undefined' !== typeof options.correctChoice) {
// Flag required is undefined, if not set to false explicitely.
widget.required = true;
}
// Fixed properties.
// Widget Name.
widget.widgetName = widgetName;
// Add random unique widget id.
widget.wid = '' + J.randomInt(0,10000000000000000000);
// UI properties.
widget.disabled = null;
widget.highlighted = null;
widget.collapsed = null;
widget.hidden = null;
widget.docked = null;
// Properties that will modify the UI of the widget once appended.
if (options.disabled) widget._disabled = true;
if (options.highlighted) widget._highlighted = true;
if (options.collapsed) widget._collapsed = true;
if (options.hidden) widget._hidden = true;
if (options.docked) widget._docked = true;
// Call init.
widget.init(options);
// Call listeners.
if (options.listeners !== false) {
// TODO: future versions should pass the right event listener
// to the listeners method. However, the problem is that it
// does not have `on.data` methods, those are aliases.
// if ('undefined' === typeof options.listeners) {
// ee = node.getCurrentEventEmitter();
// }
// else if ('string' === typeof options.listeners) {
// if (options.listeners !== 'game' &&
// options.listeners !== 'stage' &&
// options.listeners !== 'step') {
//
// throw new Error('Widget.get: widget ' + widgetName +
// ' has invalid value for option ' +
// 'listeners: ' + options.listeners);
// }
// ee = node.events[options.listeners];
// }
// else {
// throw new Error('Widget.get: widget ' + widgetName +
// ' options.listeners must be false, string ' +
// 'or undefined. Found: ' + options.listeners);
// }
// Start recording changes.
node.events.setRecordChanges(true);
widget.listeners.call(widget);
// Get registered listeners, clear changes, and stop recording.
changes = node.events.getCha