UNPKG

nodegame-widgets

Version:

Collections of useful and reusable javascript / HTML snippets for nodeGame

1,484 lines (1,368 loc) 724 kB
/** * # 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 = '&nbsp;'; } 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