diff --git a/CHANGELOG b/CHANGELOG index 2661552..17c7a85 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # nodegame-widgets change log +## current +- Consent widget disconnect option. + ## 7.0.3 - Fixed Consent form not receiving a consent object. - ChoiceManager adds freetext on simplify, if available. diff --git a/build/nodegame-widgets.js b/build/nodegame-widgets.js index f70573e..fb5e063 100644 --- a/build/nodegame-widgets.js +++ b/build/nodegame-widgets.js @@ -1,6 +1,6 @@ /** * # Widget - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Prototype of a widget class @@ -156,6 +156,7 @@ if (!this.isHighlighted()) return; this.highlighted = false; this.bodyDiv.style.border = ''; + if (this.setError) this.setError(); this.emit('unhighlighted'); }; @@ -339,10 +340,26 @@ * @see Widget.hide * @see Widget.toggle */ - Widget.prototype.show = function(display) { + Widget.prototype.show = function(opts) { if (this.panelDiv && this.panelDiv.style.display === 'none') { - this.panelDiv.style.display = display || ''; + // Backward compatible. + opts = opts || {}; + if ('string' === typeof opts) opts = { display: opts }; + this.panelDiv.style.display = opts.display || ''; this.hidden = false; + + W.adjustFrameHeight(); + if (opts.scroll !== false) { + // Scroll into the slider. + if ('function' === typeof this.bodyDiv.scrollIntoView) { + this.bodyDiv.scrollIntoView({ behavior: 'smooth' }); + } + else if (window.scrollTo) { + // Scroll to bottom of page. + window.scrollTo(0, document.body.scrollHeight); + } + } + this.emit('shown'); } }; @@ -535,7 +552,7 @@ // Bootstrap 5. options = { className: 'card-footer' }; } - else if ('object' !== typeof options) { + else if ('object' !== typeof options && 'function') { throw new TypeError('Widget.setFooter: options must ' + 'be object or undefined. Found: ' + options); @@ -552,6 +569,9 @@ else if ('string' === typeof footer) { this.footerDiv.innerHTML = footer; } + else if ('function' === typeof footer) { + footer.call(this, this.footerDiv); + } else { throw new TypeError(J.funcName(this.constructor) + '.setFooter: footer must be string, ' + @@ -901,6 +921,28 @@ throw new Error(errMsg); }; + /** + * ### Widget.next + * + * Updates the widget with the next visualization within the same step + * + * @param {boolean} FALSE if there is no next visualization. + * + * @see Widget.prev + */ + Widget.prototype.next = function() { return false; }; + + /** + * ### Widget.prev + * + * Updates the widget with the previous visualization within the same step + * + * @param {boolean} FALSE if there is no prev visualization. + * + * @see Widget.next + */ + Widget.prototype.prev = function() { return false; }; + // ## Helper methods. /** @@ -926,11 +968,12 @@ */ 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 ('undefined' === typeof res) { + throw new Error(method + ': name not found: ' + name); + } + if ('function' === typeof res) { res = res(that, param); if ('string' !== typeof res && res !== false) { @@ -1081,18 +1124,18 @@ * Container of appended widget instances * * @see Widgets.append - * @see Widgets.lastAppended + * @see Widgets.last */ this.instances = []; /** - * ### Widgets.lastAppended + * ### Widgets.last|lastAppended * * Reference to lastAppended widget * * @see Widgets.append */ - this.lastAppended = null; + this.last = this.lastAppended = null; /** * ### Widgets.docked @@ -1122,6 +1165,15 @@ */ this.collapseTarget = null; + /** + * ### Widgets.decorators + * + * Map of decorators callbacks for widgets + * + * @see Widgets.decorator + */ + this.decorators = {}; + that = this; node.registerSetup('widgets', function(conf) { var name, root, collapseTarget; @@ -1277,7 +1329,7 @@ * 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 + * @param {object} opts Optional. Configuration options, will be * mixed out with attributes in the `defaults` property * of the widget prototype. * @@ -1286,116 +1338,124 @@ * @see Widgets.append * @see Widgets.instances */ - Widgets.prototype.get = function(widgetName, options) { - var WidgetPrototype, widget, changes, tmp; + Widgets.prototype.get = function(widgetName, opts) { + var WidgetProto, widget, changes, tmp, err; + + err = 'Widgets.get'; if ('string' !== typeof widgetName) { - throw new TypeError('Widgets.get: widgetName must be string.' + + throw new TypeError(err + ': 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); + + err += widgetName + ': '; + + if (!opts) { + opts = {}; } - if (options.storeRef === false) { - if (options.docked === true) { - throw new TypeError('Widgets.get: ' + widgetName + - 'options.storeRef cannot be false ' + - 'if options.docked is true.'); - } + else if ('object' !== typeof opts) { + throw new TypeError(err + ' opts must be object or undefined. ' + + 'Found: ' + opts); } - WidgetPrototype = J.getNestedValue(widgetName, this.widgets); + WidgetProto = J.getNestedValue(widgetName, this.widgets); - if (!WidgetPrototype) { - throw new Error('Widgets.get: ' + widgetName + ' not found'); - } + if (!WidgetProto) throw new Error(err + ' not found'); + + node.info('creating widget ' + widgetName + ' v.' + + WidgetProto.version); + + // Merge shared options (if any). + tmp = this.decorators['*']; + if (tmp) tmp(opts); + tmp = this.decorators[widgetName]; + if (tmp) tmp(opts); - node.info('creating widget ' + widgetName + - ' v.' + WidgetPrototype.version); + if (opts.storeRef === false) { + if (opts.docked === true || WidgetProto.docked) { + node.warn(err + ' storeRef=false ignored, widget is docked'); + } + } - if (!this.checkDependencies(WidgetPrototype)) { - throw new Error('Widgets.get: ' + widgetName + ' has unmet ' + - 'dependencies'); + if (!this.checkDependencies(WidgetProto)) { + throw new Error(err + ' has unmet dependencies'); } // Create widget. - widget = new WidgetPrototype(options); + widget = new WidgetProto(opts); // Set ID. - tmp = options.id; + tmp = opts.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) { + if ('undefined' !== typeof opts.idPrefix) { + if ('string' === typeof opts.idPrefix && + 'number' !== typeof opts.idPrefix) { - tmp = options.idPrefix + tmp; + tmp = opts.idPrefix + tmp; } else { - throw new TypeError('Widgets.get: options.idPrefix ' + + throw new TypeError('Widgets.get: opts.idPrefix ' + 'must be string, number or ' + 'undefined. Found: ' + - options.idPrefix); + opts.idPrefix); } } widget.id = tmp; } else { - throw new TypeError('Widgets.get: options.id must be ' + + throw new TypeError('Widgets.get: opts.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) { + else if (opts.widgetStep) { widget.id = node.game.getStepId(); } - // Set prototype values or options values. - if ('undefined' !== typeof options.title) { - widget.title = options.title; + // Set prototype values or opts values. + if ('undefined' !== typeof opts.title) { + widget.title = opts.title; } - else if ('undefined' !== typeof WidgetPrototype.title) { - widget.title = WidgetPrototype.title; - } - else { - widget.title = ' '; + else if ('undefined' !== typeof WidgetProto.title) { + widget.title = WidgetProto.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(' '); + + widget.panel = 'undefined' === typeof opts.panel ? + WidgetProto.panel : opts.panel; + widget.footer = 'undefined' === typeof opts.footer ? + WidgetProto.footer : opts.footer; + widget.className = WidgetProto.className; + if (J.isArray(opts.className)) { + widget.className += ' ' + opts.className.join(' '); } - else if ('string' === typeof options.className) { - widget.className += ' ' + options.className; + else if ('string' === typeof opts.className) { + widget.className += ' ' + opts.className; } - else if ('undefined' !== typeof options.className) { + else if ('undefined' !== typeof opts.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; + opts.className); + } + widget.context = 'undefined' === typeof opts.context ? + WidgetProto.context : opts.context; + widget.sounds = 'undefined' === typeof opts.sounds ? + WidgetProto.sounds : opts.sounds; + widget.texts = 'undefined' === typeof opts.texts ? + WidgetProto.texts : opts.texts; + + widget.docked = 'undefined' === typeof opts.docked ? + WidgetProto.docked : opts.docked; + + widget.collapsible = opts.collapsible || false; + widget.closable = opts.closable || false; widget.collapseTarget = - options.collapseTarget || this.collapseTarget || null; - widget.info = options.info || false; + opts.collapseTarget || this.collapseTarget || null; + widget.info = opts.info || false; widget.hooks = { hidden: [], @@ -1410,18 +1470,26 @@ }; // By default destroy widget on exit step. - widget.destroyOnExit = options.destroyOnExit !== false; + widget.destroyOnExit = opts.destroyOnExit !== false; // Required widgets require action from user, otherwise they will // block node.done(). - if (options.required || - options.requiredChoice || - 'undefined' !== typeof options.correctChoice) { + if (opts.required === false) { + widget.required = false; + } + else if (opts.required || opts.requiredChoice || + ('undefined' !== typeof opts.correctChoice && + opts.correctChoice !== false)) { // Flag required is undefined, if not set to false explicitely. widget.required = true; } + // Display required mark (in some widgets). + widget.displayRequired = opts.displayRequired === false ? false : true; + widget.requiredMark = 'undefined' !== typeof opts.requiredMark ? + opts.requiredMark : '✳️'; // * + // Fixed properties. // Widget Name. @@ -1435,44 +1503,49 @@ 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; + // Option already checked. + if (widget.docked) widget._docked = true; + + // Bootstrap 5 by default. + if (opts.bootstrap5 !== false) widget._bootstrap5 = true; + + if (opts.disabled) widget._disabled = true; + if (opts.highlighted) widget._highlighted = true; + if (opts.collapsed) widget._collapsed = true; + if (opts.hidden) widget._hidden = true; + // Call init. - widget.init(options); + widget.init(opts); // Call listeners. - if (options.listeners !== false) { + if (opts.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) { + // if ('undefined' === typeof opts.listeners) { // ee = node.getCurrentEventEmitter(); // } - // else if ('string' === typeof options.listeners) { - // if (options.listeners !== 'game' && - // options.listeners !== 'stage' && - // options.listeners !== 'step') { + // else if ('string' === typeof opts.listeners) { + // if (opts.listeners !== 'game' && + // opts.listeners !== 'stage' && + // opts.listeners !== 'step') { // // throw new Error('Widget.get: widget ' + widgetName + // ' has invalid value for option ' + - // 'listeners: ' + options.listeners); + // 'listeners: ' + opts.listeners); // } - // ee = node.events[options.listeners]; + // ee = node.events[opts.listeners]; // } // else { // throw new Error('Widget.get: widget ' + widgetName + - // ' options.listeners must be false, string ' + - // 'or undefined. Found: ' + options.listeners); + // ' opts.listeners must be false, string ' + + // 'or undefined. Found: ' + opts.listeners); // } // Start recording changes. @@ -1531,11 +1604,11 @@ } } // Remove from lastAppended. - if (node.widgets.lastAppended && - node.widgets.lastAppended.wid === this.wid) { + if (node.widgets.last && + node.widgets.last.wid === this.wid) { - node.warn('node.widgets.lastAppended destroyed.'); - node.widgets.lastAppended = null; + node.warn('node.widgets.last destroyed.'); + node.widgets.lastAppended = node.widgets.last = null; } } @@ -1550,14 +1623,14 @@ }; // Store widget instance (e.g., used for destruction). - if (options.storeRef !== false) this.instances.push(widget); + if (opts.storeRef !== false) this.instances.push(widget); else widget.storeRef = false; return widget; }; /** - * ### Widgets.append + * ### Widgets.append|add * * Appends a widget to the specified root element * @@ -1581,6 +1654,7 @@ * * @see Widgets.get */ + Widgets.prototype.add = Widgets.prototype.append = function(w, root, options) { var tmp; @@ -1628,10 +1702,10 @@ // Add panelDiv (with or without panel). tmp = options.panel === false ? true : w.panel === false; - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5 tmp = { - className: tmp ? [ 'ng_widget', 'no-panel', w.className ] : + className: tmp ? [ 'ng_widget', w.className ] : [ 'ng_widget', 'card', w.className ] }; } @@ -1656,7 +1730,7 @@ // Optionally add title (and div). if (options.title !== false && w.title) { - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5. tmp = options.panel === false ? 'no-panel-heading' : 'card-header'; @@ -1671,7 +1745,7 @@ } // Add body (with or without panel). - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5. tmp = options.panel !== false ? 'card-body' : 'no-panel-body'; } @@ -1684,15 +1758,15 @@ // Optionally add footer. if (w.footer) { - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5. tmp = options.panel === false ? 'no-panel-heading' : 'card-footer'; } else { - // Bootstrap 3. - tmp = options.panel === false ? - 'no-panel-heading' : 'panel-heading'; + // Bootstrap 3. + tmp = options.panel === false ? + 'no-panel-heading' : 'panel-heading'; } w.setFooter(w.footer); @@ -1724,17 +1798,11 @@ } // Store reference of last appended widget (.get method set storeRef). - if (w.storeRef !== false) this.lastAppended = w; + if (w.storeRef !== false) this.lastAppended = this.last = w; return w; }; - Widgets.prototype.add = function(w, root, options) { - console.log('***Widgets.add is deprecated. Use ' + - 'Widgets.append instead.***'); - return this.append(w, root, options); - }; - /** * ### Widgets.isWidget * @@ -1777,7 +1845,7 @@ for ( ; ++i < len ; ) { this.instances[0].destroy(); } - this.lastAppended = null; + this.lastAppended = this.last = null; if (this.instances.length) { node.warn('node.widgets.destroyAll: some widgets could ' + 'not be destroyed.'); @@ -1904,6 +1972,23 @@ return res; }; + /** + * ### Widgets.decorator + * + * Adds a callback to decorate options for all widgets + * + * @param {string} widget optional The name of the widget for which + * the options are decorated; '*' means valid for all widgets. + * @param {function} cb The callback function decorating the options. + */ + Widgets.prototype.decorator = function(widget, cb) { + if ('function' !== typeof cb) { + throw new TypeError('Widgets.decorator: cb must be function. ' + + 'Found: ' + cb); + } + this.decorators[widget] = cb + }; + // ## Helper functions // ### checkDepErrMsg @@ -2041,7 +2126,7 @@ /** * # BackButton - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a button that if pressed goes to the previous step @@ -2056,20 +2141,14 @@ // ## Meta-data - BackButton.version = '0.4.0'; + BackButton.version = '0.5.0'; BackButton.description = 'Creates a button that if ' + 'pressed goes to the previous step.'; - BackButton.title = false; + BackButton.panel = false; BackButton.className = 'backbutton'; BackButton.texts.back = 'Back'; - // ## Dependencies - - BackButton.dependencies = { - JSUS: {} - }; - /** * ## BackButton constructor * @@ -2094,8 +2173,9 @@ this.button = options.button; } else if ('undefined' === typeof options.button) { - this.button = document.createElement('input'); - this.button.type = 'button'; + // this.button = document.createElement('input'); + this.button = document.createElement('button'); + // this.button.type = 'button'; } else { throw new TypeError('BackButton constructor: options.button must ' + @@ -2106,6 +2186,14 @@ this.button.onclick = function() { var res; that.disable(); + if (that.onclick && false === that.onclick()) return; + if (node.game.isWidgetStep()) { + // Widget has a next visualization in the same step. + if (node.widgets.last.prev() !== false) { + that.enable(); + return; + } + } res = node.game.stepBack(that.stepOptions); if (res === false) that.enable(); }; @@ -2134,6 +2222,18 @@ // ## @api: private. noZeroStep: true }; + + + /** + * #### BackButton.onclick + * + * A callback function executed when the button is clicked + * + * If the function returns FALSE, the procedure is aborted. + * + * Default: TRUE + */ + this.onclick = null; } // ## BackButton methods @@ -2178,28 +2278,30 @@ } this.button.id = tmp; - if ('undefined' === typeof opts.className) { + if ('undefined' === typeof opts.classNameBtn) { tmp = 'btn btn-lg btn-secondary'; } - else if (opts.className === false) { + else if (opts.classNameBtn === false) { tmp = ''; } - else if ('string' === typeof opts.className) { - tmp = opts.className; + else if ('string' === typeof opts.classNameBtn) { + tmp = opts.classNameBtn; } - else if (J.isArray(opts.className)) { - tmp = opts.className.join(' '); + else if (J.isArray(opts.classNameBtn)) { + tmp = opts.classNameBtn.join(' '); } else { - throw new TypeError('BackButton.init: opts.className must ' + + throw new TypeError('BackButton.init: classNameBtn must ' + 'be string, array, or undefined. Found: ' + - opts.className); + opts.classNameBtn); } this.button.className = tmp; // Button text. - this.button.value = 'string' === typeof opts.text ? - opts.text : this.getText('back'); + // this.button.value = 'string' === typeof opts.text ? + // opts.text : this.getText('back'); + this.button.innerHTML = 'string' === typeof opts.text ? + opts.text : this.getText('back'); this.stepOptions.acrossStages = 'undefined' === typeof opts.acrossStages ? @@ -2207,6 +2309,8 @@ this.stepOptions.acrossRounds = 'undefined' === typeof opts.acrossRounds ? true : !!opts.acrossRounds; + + setOnClick(this, opts.onclick); }; BackButton.prototype.append = function() { @@ -2229,19 +2333,32 @@ step = node.game.getPreviousStep(1, that.stepOptions); prop = node.game.getProperty('backbutton'); - if (!step || prop === false || - (prop && prop.enableOnPlaying === false)) { + if (prop !== true && + (!step || prop === false || + (prop && prop.enableOnPlaying === false))) { // It might be disabled already, but we do it again. that.disable(); } else { // It might be enabled already, but we do it again. - if (step) that.enable(); + if (prop === true || step) that.enable(); + } + + // if ('string' === typeof prop) that.button.value = prop; + // else if (prop && prop.text) that.button.value = prop.text; + if ('string' === typeof prop) that.button.innerHTML = prop; + else if (prop && prop.text) that.button.innerHTML = prop.text; + + if (prop) { + setOnClick(that, prop.onclick, true); + if (prop.enable) that.enable(); } + }); - if ('string' === typeof prop) that.button.value = prop; - else if (prop && prop.text) that.button.value = prop.text; + // Catch those events. + node.events.game.on('WIDGET_NEXT', function() { + that.enable(); }); }; @@ -2251,7 +2368,10 @@ * Disables the back button */ BackButton.prototype.disable = function() { - this.button.disabled = 'disabled'; + if (this.disabled) return; + this.disabled = true; + this.button.disabled = true; + this.emit('disabled'); }; /** @@ -2260,14 +2380,33 @@ * Enables the back button */ BackButton.prototype.enable = function() { + if (!this.disabled) return; + this.disabled = false; this.button.disabled = false; + this.emit('enabled'); }; + // ## Helper functions. + + // Checks and sets the onclick function. + function setOnClick(that, onclick, step) { + var str; + if ('undefined' !== typeof onclick) { + if ('function' !== typeof onclick && onclick !== null) { + str = 'BackButton.init'; + if (step) str += ' (step property)'; + throw new TypeError(str + ': onclick must be function, null,' + + ' or undefined. Found: ' + onclick); + } + that.onclick = onclick; + } + } + })(node); /** * # BoxSelector - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a simple box that opens a menu of items to choose from @@ -2289,15 +2428,8 @@ 'of items to choose from.'; BoxSelector.panel = false; - BoxSelector.title = false; BoxSelector.className = 'boxselector'; - // ## Dependencies - - BoxSelector.dependencies = { - JSUS: {} - }; - /** * ## BoxSelector constructor * @@ -2541,7 +2673,7 @@ /** * # Chat - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a simple configurable chat @@ -2603,17 +2735,10 @@ Chat.description = 'Offers a uni-/bi-directional communication interface ' + 'between players, or between players and the server.'; - Chat.title = 'Chat'; Chat.className = 'chat'; Chat.panel = false; - // ## Dependencies - - Chat.dependencies = { - JSUS: {} - }; - /** * ## Chat constructor * @@ -3405,7 +3530,7 @@ /** * # ChernoffFaces - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays multidimensional data in the shape of a Chernoff Face. @@ -3426,12 +3551,10 @@ ChernoffFaces.description = 'Display parametric data in the form of a Chernoff Face.'; - ChernoffFaces.title = 'ChernoffFaces'; ChernoffFaces.className = 'chernofffaces'; // ## Dependencies ChernoffFaces.dependencies = { - JSUS: {}, Table: {}, Canvas: {}, SliderControls: {} @@ -4447,7 +4570,6 @@ // ## Dependencies ChernoffFaces.dependencies = { - JSUS: {}, Table: {}, Canvas: {}, 'Controls.Slider': {} @@ -5078,7 +5200,7 @@ /** * # ChoiceManager - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates and manages a set of selectable choices forms (e.g., ChoiceTable). @@ -5093,16 +5215,19 @@ // ## Meta-data - ChoiceManager.version = '1.4.1'; + ChoiceManager.version = '1.9.0'; ChoiceManager.description = 'Groups together and manages a set of ' + 'survey forms (e.g., ChoiceTable).'; - ChoiceManager.title = false; ChoiceManager.className = 'choicemanager'; // ## Dependencies - ChoiceManager.dependencies = {}; + ChoiceManager.dependencies = { + BackButton: {}, DoneButton: {} + }; + + var C = 'ChoiceManager.'; /** * ## ChoiceManager constructor @@ -5110,6 +5235,7 @@ * Creates a new instance of ChoiceManager */ function ChoiceManager() { + /** * ### ChoiceManager.dl * @@ -5181,7 +5307,6 @@ */ this.groupOrder = null; - // TODO: rename in sharedOptions. /** * ### ChoiceManager.formsOptions * @@ -5196,13 +5321,12 @@ storeRef: false }; - /** * ### ChoiceManager.simplify * - * If TRUE, it returns getValues() returns forms.values + * If TRUE, method `ChoiceManager.getValues()` returns only forms.values * - * @see ChoiceManager.getValue + * @see ChoiceManager.getValues */ this.simplify = null; @@ -5225,9 +5349,109 @@ /** * ### ChoiceManager.required * - * TRUE if widget should be checked upon node.done. + * If TRUE, the widget is checked upon node.done. */ this.required = null; + + /** + * ### ChoiceManager.oneByOne + * + * If, TRUE the widget displays only one form at the time + * + * Calling node.done will display the next form. + */ + this.oneByOne = null; + + /** + * ### ChoiceManager.oneByOneCounter + * + * Index the currently displayed form if oneByOne is TRUE + */ + this.oneByOneCounter = 0; + + /** + * ### ChoiceManager.oneByOneResults + * + * Contains partial results from forms if OneByOne is true + */ + this.oneByOneResults = {}; + + /** + * ### ChoiceManager.conditionals + * + * Contains conditions to display or hide forms based on other forms + */ + this.conditionals = {}; + + /** + * ### ChoiceManager.doneBtn + * + * Button to go to the next visualization/step + */ + this.doneBtn = null; + + /** + * ### ChoiceManager.backBtn + * + * Button to go to the previous visualization/step + */ + this.backBtn = null; + + /** + * ### ChoiceManager.honeypot + * + * Array of unused input forms to detect bots. + */ + this.honeypot = null; + + /** + * ### ChoiceManager.qCounter + * + * Adds question number starting from the integer. + * + * If FALSE, no question number is added. + */ + this.qCounter = 1; + + /** + * ### ChoiceManager.qCounterSymbol + * + * The symbol used to count the questions. + */ + this.qCounterSymbol = 'Q'; + + /** + * ### ChoiceManager.qCounterCb + * + * The callback creating the question counter. + */ + this.qCounterCb = function(w, mainText, form, idx) { + return '' + + w.qCounterSymbol + w.qCounter++ + ' ' + mainText; + }; + + /** + * ### ChoiceManager.autoId + * + * If TRUE, id forms are auto-assigned if undefined + */ + this.autoId = true; + + /** + * ### ChoiceManager.delayOnNext + * + * The number of milliseconds the _next_ form is initially disabled + * + * Next and back buttons are also disabled in the process. + * + * Set to falsy to prevent this default behavior. + * + * @see ChoiceManager.next + * @see ChoiceManager.doneBtn + * @see ChoiceManager.backBtn + */ + this.delayOnNext = 350; + } // ## ChoiceManager methods @@ -5264,7 +5488,6 @@ else tmp = !!options.shuffleForms; this.shuffleForms = tmp; - // Set the group, if any. if ('string' === typeof options.group || 'number' === typeof options.group) { @@ -5272,7 +5495,7 @@ this.group = options.group; } else if ('undefined' !== typeof options.group) { - throw new TypeError('ChoiceManager.init: options.group must ' + + throw new TypeError(C + 'init: options.group must ' + 'be string, number or undefined. Found: ' + options.group); } @@ -5283,7 +5506,7 @@ this.groupOrder = options.groupOrder; } else if ('undefined' !== typeof options.group) { - throw new TypeError('ChoiceManager.init: options.groupOrder must ' + + throw new TypeError(C + 'init: options.groupOrder must ' + 'be number or undefined. Found: ' + options.groupOrder); } @@ -5293,7 +5516,7 @@ this.mainText = options.mainText; } else if ('undefined' !== typeof options.mainText) { - throw new TypeError('ChoiceManager.init: options.mainText must ' + + throw new TypeError(C + 'init: options.mainText must ' + 'be string or undefined. Found: ' + options.mainText); } @@ -5301,15 +5524,11 @@ // formsOptions. if ('undefined' !== typeof options.formsOptions) { if ('object' !== typeof options.formsOptions) { - throw new TypeError('ChoiceManager.init: options.formsOptions' + + throw new TypeError(C + 'init: options.formsOptions' + ' must be object or undefined. Found: ' + options.formsOptions); } - if (options.formsOptions.hasOwnProperty('name')) { - throw new Error('ChoiceManager.init: options.formsOptions ' + - 'cannot contain property name. Found: ' + - options.formsOptions); - } + this.formsOptions = J.mixin(this.formsOptions, options.formsOptions); } @@ -5324,9 +5543,49 @@ // If TRUE, it returns getValues returns forms.values. this.simplify = !!options.simplify; + // If TRUE, forms are displayed one by one. + this.oneByOne = !!options.oneByOne; + + // If truthy, a next button is added at the bottom. If object, it + // is passed as conf object to DoneButton. + this.doneBtn = options.doneBtn; + + // If truthy, a back button is added at the bottom. If object, it + // is passed as conf object to BackButton. + this.backBtn = options.backBtn; + + // If truthy a useless form is added to detect bots. + this.honeypot = options.honeypot; + + if ('undefined' !== typeof options.qCounter) { + this.qCounter = options.qCounter; + } + + if ('undefined' !== typeof options.qCounterSymbol) { + this.qCounterSymbol = options.qCounterSymbol; + } + + if ('undefined' !== typeof options.qCounterCb) { + this.qCounterCb = options.qCounterCb; + } + + if ('undefined' !== typeof options.autoId) { + this.autoId = options.autoId; + } + + tmp = options.delayOnNext; + if ('undefined' !== typeof tmp) { + if (J.isNumber(tmp, 0)) { + throw new TypeError('ChoiceManager.init: delayOnNext must ' + + 'be a positive number or undefined. Found: ' + tmp); + } + this.delayOnNext = tmp; + } + // After all configuration options are evaluated, add forms. if ('undefined' !== typeof options.forms) this.setForms(options.forms); + }; /** @@ -5358,11 +5617,11 @@ * @see ChoiceManager.buildTableAndForms */ ChoiceManager.prototype.setForms = function(forms) { - var form, formsById, i, len, parsedForms, name; + var i, formIdx, len, parsedForms; if ('function' === typeof forms) { parsedForms = forms.call(node.game); if (!J.isArray(parsedForms)) { - throw new TypeError('ChoiceManager.setForms: forms is a ' + + throw new TypeError(C + 'setForms: forms is a ' + 'callback, but did not returned an ' + 'array. Found: ' + parsedForms); } @@ -5371,89 +5630,37 @@ parsedForms = forms; } else { - throw new TypeError('ChoiceManager.setForms: forms must be array ' + + throw new TypeError(C + 'setForms: forms must be array ' + 'or function. Found: ' + forms); } len = parsedForms.length; if (!len) { - throw new Error('ChoiceManager.setForms: forms is an empty array.'); + throw new Error(C + 'setForms: forms is an empty array.'); } // Manual clone forms. - formsById = {}; - forms = new Array(len); - i = -1; - for ( ; ++i < len ; ) { - form = parsedForms[i]; - if (!node.widgets.isWidget(form)) { - // TODO: smart checking form name. Maybe in Stager already? - name = form.name || 'ChoiceTable'; - // Add defaults. - J.mixout(form, this.formsOptions); - form = node.widgets.get(name, form); - } - - if (form.id) { - if (formsById[form.id]) { - throw new Error('ChoiceManager.setForms: duplicated ' + - 'form id: ' + form.id); - } - - } - else { - form.id = form.className + '_' + i; - } - forms[i] = form; - formsById[form.id] = forms[i]; - - if (form.required || form.requiredChoice || form.correctChoice) { - // False is set manually, otherwise undefined. - if (this.required === false) { - throw new Error('ChoiceManager.setForms: required is ' + - 'false, but form "' + form.id + - '" has required truthy'); - } - this.required = true; - } - } - // Assigned verified forms. - this.forms = forms; - this.formsById = formsById; + this.formsById = {}; + this.forms = new Array(len); - // Save the order in which the choices will be added. + // Shuffle, if needed. this.order = J.seq(0, len-1); if (this.shuffleForms) this.order = J.shuffle(this.order); - }; - - /** - * ### ChoiceManager.buildDl - * - * Builds the list of all forms - * - * Must be called after forms have been set already. - * - * @see ChoiceManager.setForms - * @see ChoiceManager.order - */ - ChoiceManager.prototype.buildDl = function() { - var i, len, dt; - var form; - i = -1, len = this.forms.length; + i = -1; for ( ; ++i < len ; ) { - dt = document.createElement('dt'); - dt.className = 'question'; - form = this.forms[this.order[i]]; - node.widgets.append(form, dt); - this.dl.appendChild(dt); + formIdx = this.order[i]; + this.addForm(parsedForms[formIdx], false, i); + // Save the order in which the choices will be added. } }; ChoiceManager.prototype.append = function() { + var div, opts; + // Id must be unique. if (W.getElementById(this.id)) { - throw new Error('ChoiceManager.append: id is not ' + + throw new Error(C + 'append: id is not ' + 'unique: ' + this.id); } @@ -5467,9 +5674,8 @@ } // Dl. - this.dl = document.createElement('dl'); - this.buildDl(); - // Append Dl. + this.dl = buildDL(this); + // Append it. this.bodyDiv.appendChild(this.dl); // Creates a free-text textarea, possibly with placeholder text. @@ -5483,6 +5689,27 @@ // Append textarea. this.bodyDiv.appendChild(this.textarea); } + + if (this.backBtn || this.doneBtn) { + div = W.append('div', this.bodyDiv); + div.className = 'choicemanager-buttons'; + + if (this.backBtn) { + opts = this.backBtn; + if ('string' === typeof opts) opts = { text: opts }; + else opts = J.mixin({ text: 'Back' }, opts); + this.backBtn = node.widgets.append('BackButton', div, opts); + } + + if (this.doneBtn) { + opts = this.doneBtn; + if ('string' === typeof opts) opts = { text: opts }; + opts = J.mixin({ text: 'Next' }, opts); + this.doneBtn = node.widgets.append('DoneButton', div, opts); + } + } + + if (this.honeypot) this.addHoneypot(this.honeypot); }; /** @@ -5522,25 +5749,124 @@ }; /** - * ### ChoiceManager.enable + * ### ChoiceManager.addForm * - * Enables all forms + * Adds a new form at the bottom. */ - ChoiceManager.prototype.enable = function() { - var i, len; - if (!this.disabled) return; - i = -1, len = this.forms.length; - for ( ; ++i < len ; ) { - this.forms[i].enable(); - } - this.disabled = false; - this.emit('enabled') - }; + ChoiceManager.prototype.addForm = function(form, scrollIntoView, idx) { + var name; - /** - * ### ChoiceManager.verifyChoice - * - * Compares the current choice/s with the correct one/s + if ('undefined' === typeof idx) idx = this.forms.length; + if ('undefined' === typeof scrollIntoView) scrollIntoView = true; + + if (!node.widgets.isWidget(form)) { + + // Add defaults. + J.mixout(form, this.formsOptions); + + + if (!form.id && this.autoId) { + name = this.autoId === true ? + node.game.getStepId() : this.autoId; + form.id = name + '_' + (idx + 1); + } + + // By default correctChoice means required. + // However, it is possible to add required = false and correctChoice + // truthy, for instance if there is a solution to display. + if ((form.required !== false && form.requiredChoice !== false) && + (form.required || form.requiredChoice || + ('undefined' !== typeof form.correctChoice && + form.correctChoice !== false))) { + + // False is set manually, otherwise undefined. + if (this.required === false) { + throw new Error(C + 'setForms: required is ' + + 'false, but form "' + form.id + + '" has required truthy'); + } + this.required = true; + } + + // Display forms one by one. + if (this.oneByOne && this.oneByOneCounter !== idx) { + form.hidden = true; + } + + if (form.conditional) { + this.conditionals[form.id] = form.conditional; + } + + if (this._bootstrap5 && 'undefined' === typeof form.bootstrap5) { + form.bootstrap5 = true; + } + + if (this.qCounter !== false) { + if (form.mainText && !form.qCounterAdded) { + form.mainText = + this.qCounterCb(this, form.mainText, form, idx); + form.qCounterAdded = true; + } + } + + // TODO: smart checking form name. Maybe in Stager already? + name = form.name || 'ChoiceTable'; + + form = node.widgets.get(name, form); + + } + + if (form.id) { + if (this.formsById[form.id]) { + throw new Error(C + 'setForms: duplicated form id: ' + form.id); + } + + } + else { + form.id = form.className + '_' + idx; + } + this.forms[idx] = form; + this.formsById[form.id] = form; + + if (this.dl) { + + // Add the last added form to the order array. + this.order.push(this.order.length); + + appendDT(this.dl, form); + W.adjustFrameHeight(); + if (!scrollIntoView) return; + // Scroll into the slider. + if ('function' === typeof form.bodyDiv.scrollIntoView) { + form.bodyDiv.scrollIntoView({ behavior: 'smooth' }); + } + else if (window.scrollTo) { + // Scroll to bottom of page. + window.scrollTo(0, document.body.scrollHeight); + } + } + }; + + /** + * ### ChoiceManager.enable + * + * Enables all forms + */ + ChoiceManager.prototype.enable = function() { + var i, len; + if (!this.disabled) return; + i = -1, len = this.forms.length; + for ( ; ++i < len ; ) { + this.forms[i].enable(); + } + this.disabled = false; + this.emit('enabled') + }; + + /** + * ### ChoiceManager.verifyChoice + * + * Compares the current choice/s with the correct one/s * * @param {boolean} markAttempt Optional. If TRUE, the value of * current choice is added to the attempts array. Default @@ -5617,7 +5943,7 @@ */ ChoiceManager.prototype.highlight = function(border) { if (border && 'string' !== typeof border) { - throw new TypeError('ChoiceManager.highlight: border must be ' + + throw new TypeError(C + 'highlight: border must be ' + 'string or undefined. Found: ' + border); } if (!this.dl || this.highlighted === true) return; @@ -5670,13 +5996,16 @@ * to find the correct answer. Default: TRUE. * - highlight: If TRUE, forms that do not have a correct value * will be highlighted. Default: TRUE. + * - simplify: If TRUE, forms are not nested under `.forms`, but + * available at the first level. Duplicated keys will be overwritten. + * TODO: rename "flatten." * * @return {object} Object containing the choice and paradata * * @see ChoiceManager.verifyChoice */ ChoiceManager.prototype.getValues = function(opts) { - var obj, i, len, form, lastErrored, res; + var obj, i, len, form, lastErrored, res, toCheck; obj = { order: this.order, forms: {}, @@ -5687,39 +6016,72 @@ if ('undefined' === typeof opts.markAttempt) opts.markAttempt = true; if ('undefined' === typeof opts.highlight) opts.highlight = true; if (opts.markAttempt) obj.isCorrect = true; - i = -1, len = this.forms.length; - for ( ; ++i < len ; ) { - form = this.forms[i]; - // If it is hidden or disabled we do not do validation. - if (form.isHidden() || form.isDisabled()) { - res = form.getValues({ - markAttempt: false, - highlight: false - }); - if (res) obj.forms[form.id] = res; - } - else { - // ContentBox does not return a value. - res = form.getValues(opts); - if (!res) continue; - obj.forms[form.id] = res; - // Backward compatible (requiredChoice). - if ((form.required || form.requiredChoice) && - (obj.forms[form.id].choice === null || - (form.selectMultiple && - !obj.forms[form.id].choice.length))) { - - obj.missValues.push(form.id); - lastErrored = form; - } - if (opts.markAttempt && - obj.forms[form.id].isCorrect === false) { - // obj.isCorrect = false; - lastErrored = form; + len = this.forms.length; + + + // TODO: we could save the results when #next() is called or + // have an option to get the values of current form or a specific form. + // The code below is a old and created before #next() was created. + // Only one form displayed. + // if (this.oneByOne) { + // + // // Evaluate one-by-one and store partial results. + // if (this.oneByOneCounter < (len-1)) { + // form = this.forms[this.oneByOneCounter]; + // res = form.getValues(opts); + // if (res) { + // this.oneByOneResults[form.id] = res; + // lastErrored = checkFormResult(res, form, opts); + // + // if (!lastErrored) { + // this.forms[this.oneByOneCounter].hide(); + // this.oneByOneCounter++; + // this.forms[this.oneByOneCounter].show(); + // W.adjustFrameHeight(); + // // Prevent stepping. + // obj.isCorrect = false; + // } + // } + // } + // // All one-by-one pages executed. + // else { + // // Copy all partial results in the obj returning the + // obj.forms = this.oneByOneResults; + // } + // + // } + // All forms on the page. + // else { + i = -1; + for ( ; ++i < len ; ) { + form = this.forms[i]; + + // Not one-by-one because there could be many hidden. + // If it is hidden or disabled we do not do validation. + + if (this.oneByOne) toCheck = form._shown && form.required; + else toCheck = !(form.isDisabled() || form.isHidden()); + + if (toCheck) { + // ContentBox does not return a value. + res = form.getValues(opts); + if (!res) continue; + obj.forms[form.id] = res; + + res = checkFormResult(res, form, opts, obj); + if (res) lastErrored = res; + } + else { + res = form.getValues({ + markAttempt: false, + highlight: false + }); + if (res) obj.forms[form.id] = res; } } - } + // } + if (lastErrored) { if (opts.highlight && 'function' === typeof lastErrored.bodyDiv.scrollIntoView) { @@ -5736,12 +6098,20 @@ if (this.textarea) obj.freetext = this.textarea.value; // Simplify everything, if requested. - if (opts.simplify || this.simplify) { + if (opts.simplify === true || this.simplify) { res = obj; obj = obj.forms; if (res.isCorrect === false) obj.isCorrect = false; if (res.freetext) obj.freetext = res.freetext; } + + if (this.honeypot) { + obj.honeypotHit = 0; + obj.honeypot = this.honeypot.map(function(h) { + if (h.value) obj.honeypotHit++; + return h.value || false; + }); + } return obj; }; @@ -5757,7 +6127,7 @@ ChoiceManager.prototype.setValues = function(opts) { var i, len; if (!this.forms || !this.forms.length) { - throw new Error('ChoiceManager.setValues: no forms found.'); + throw new Error(C + 'setValues: no forms found.'); } opts = opts || {}; i = -1, len = this.forms.length; @@ -5769,8 +6139,291 @@ if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); }; + /** + * ### ChoiceManager.addHoneypot + * + * Adds a hidden
tag with nested that bots should fill + * + * The inputs created are added under ChoiceManager.honeypot + * + * @param {object} opts Optional. Options to configure the honeypot. + * - id: id of the tag + * - action: action attribute of the tag + * - forms: array of forms to add to the tag. Format: + * - id: id of input and "for" attribute of the label + * - label: text of the label + * - placeholder: placeholder for the input + * - type: type of input (default 'text') + */ + ChoiceManager.prototype.addHoneypot = function(opts) { + var h, forms, that; + if (!this.isAppended()) { + node.warn(C + 'addHoneypot: not appended yet'); + return; + } + if ('object' !== typeof opts) opts = {}; + h = W.add('form', this.panelDiv, { + id: opts.id || (this.id + 'form'), + action: opts.action || ('/' + this.id + 'receive') + }); + + h.style.opacity = 0; + h.style.position = 'absolute'; + h.style.top = 0; + h.style.left = 0; + h.style.height = 0; + h.style.width = 0; + h.style['z-index'] = -1; + + if (!opts.forms) { + forms = [ + { id: 'name', label: 'Your name', + placeholder: 'Enter your name' }, + { id: 'email', label: 'Your email', + placeholder: 'Type your email', type: 'email' } + ]; + } + else { + forms = opts.forms; + } + + // Change from options to array linking to honeypot inputs. + this.honeypot = []; + + that = this; + forms.forEach(function(f) { + var hh; + W.add('label', h, { 'for': f.id }); + hh = W.add('input', h, { + id: f.id, + type: f.type || 'text', + placeholder: f.placeholder, + required: true, + autocomplete: 'off' + }); + that.honeypot.push(hh); + }); + }; + + /** + * ### ChoiceManager.next + * + * Sets values for forms in manager as specified by the options + * + * @return {boolean} FALSE, if there is not another visualization. + */ + ChoiceManager.prototype.next = function() { + var form, conditional, failsafe, that; + if (!this.oneByOne) return false; + if (!this.forms || !this.forms.length) { + throw new Error(C + 'next: no forms found.'); + } + form = this.forms[this.oneByOneCounter]; + if (!form) return false; + + if (form.next()) return true; + if (this.oneByOneCounter >= (this.forms.length-1)) return false; + + form.hide(); + if (this.backBtn) this.backBtn.disable(); + if (this.doneBtn) this.doneBtn.disable(); + + failsafe = 500; + while (form && !conditional && this.oneByOneCounter < failsafe) { + form = this.forms[++this.oneByOneCounter]; + if (!form) return false; + conditional = checkConditional(this, form.id); + } + + // TODO: make this property a reserved keyword. + form._shown = true; + + // Delay the activation of the form to prevent accidental clicking. + if (this.delayOnNext) form.disable(); + + if ('undefined' !== typeof $) { + $(form.panelDiv).fadeIn(); + form.hidden = false; // for nodeGame. + } + else { + form.show(); + } + window.scrollTo(0,0); + + if (this.delayOnNext) { + that = this; + setTimeout(function() { + if (node.game.isPaused()) return; + form.enable(); + if (that.backBtn) that.backBtn.enable(); + if (that.doneBtn) that.doneBtn.enable(); + }, this.delayOnNext); + } + + W.adjustFrameHeight(); + + node.emit('WIDGET_NEXT', this); + + return true; + }; + + ChoiceManager.prototype.prev = function() { + var form, conditional, failsafe; + if (!this.oneByOne) return false; + if (!this.forms || !this.forms.length) { + throw new Error(C + 'prev: no forms found.'); + } + form = this.forms[this.oneByOneCounter]; + if (!form) return false; + if (form.prev()) return true; + if (this.oneByOneCounter <= 0) return false; + form.hide(); + + failsafe = 500; + while (form && !conditional && this.oneByOneCounter < failsafe) { + form = this.forms[--this.oneByOneCounter]; + if (!form) return false; + conditional = checkConditional(this, form.id); + } + + if ('undefined' !== typeof $) { + $(form.panelDiv).fadeIn(); + form.hidden = false; // for nodeGame. + } + else { + form.show(); + } + window.scrollTo(0,0); + + W.adjustFrameHeight(); + node.emit('WIDGET_PREV', this); + + return true; + }; + + // TODO: better to have .getForms({ hidden: false }); or similar + ChoiceManager.prototype.getVisibleForms = function() { + if (this.oneByOne) return [this.forms[this.oneByOneCounter]]; + return this.forms.map(function(f) { if (!f.isHidden()) return f; }); + }; + // ## Helper methods. + /** + * ### checkFormResult + * + * Checks if the values returned by a form are valid + * + * @param {object} res The values returned by a form + * @param {object} form The form object + * @param {object} opts Configuration options changing the checking behavior + * @param {object} out Optional The object returned by + * `ChoiceManager.getValues()` + * + * @return {bool} TRUE, if conditions for display are met + * + * @see ChoiceManager.getValues + */ + function checkFormResult(res, form, opts, out) { + var err; + // Backward compatible (requiredChoice). + if ((form.required || form.requiredChoice) && + (res.choice === null || + (form.selectMultiple && !res.choice.length))) { + + if (out) out.missValues.push(form.id); + err = form; + } + if (opts.markAttempt && res.isCorrect === false) { + // out.isCorrect = false; + err = form; + } + + return err; + } + + /** + * ### checkConditional + * + * Checks if the conditions for the display of a form are met + * + * @param {ChoiceManager} w This widget instance + * @param {string} form The id of the conditional to check + * + * @return {bool} TRUE, if conditions for display are met + * + * @see ChoiceManager.conditionals + */ + function checkConditional(w, id) { + var f, c, form; + f = w.conditionals[id]; + if (f) { + if ('function' === typeof f) { + return f.call(w, w.formsById); + } + for (c in f) { + if (f.hasOwnProperty(c)) { + form = w.formsById[c]; + if (!form) continue; + // No multiple choice allowed. + if (J.isArray(f[c])) { + if (!J.inArray(form.currentChoice, f[c])) return false; + } + else if (form.currentChoice !== f[c]) { + return false; + } + } + } + } + return true; + } + + /** + * ### buildDL + * + * Builds the list of all forms + * + * Must be called after forms have been set already. + * + * @param {ChoiceManager} w This widget instance + * + * @return {HTMLElement} The
HTML element + * + * @see ChoiceManager.setForms + * @see ChoiceManager.order + * @see appendDT + */ + function buildDL(w) { + var i, len, form, dl; + dl = document.createElement('dl'); + i = -1, len = w.forms.length; + for ( ; ++i < len ; ) { + // If shuffled, w.forms already follows the shuffled order. + form = w.forms[i]; + // form = w.forms[w.order[i]]; + appendDT(dl, form); + } + return dl; + } + + /** + * ### appendDT + * + * Creates a
, adds a widget to it, and
to a
+ * + * @param {HTMLElement} dl The
HTML element + * @param {object} form The widget settings to create a new form + * + * @see buildDL + */ + function appendDT(dl, form) { + var dt; + dt = document.createElement('dt'); + dt.className = 'question'; + node.widgets.add(form, dt); + dl.appendChild(dt); + } + // In progress. // const createOnClick = (choice, question) => { // return function(value, removed, td) { @@ -5793,7 +6446,7 @@ /** * # ChoiceTable - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2024 Stefano Balietti * MIT Licensed * * Creates a configurable table where each cell is a selectable choice @@ -5810,11 +6463,10 @@ // ## Meta-data - ChoiceTable.version = '1.8.1'; + ChoiceTable.version = '1.11.0'; ChoiceTable.description = 'Creates a configurable table where ' + 'each cell is a selectable choice.'; - ChoiceTable.title = 'Make your choice'; ChoiceTable.className = 'choicetable'; ChoiceTable.texts = { @@ -5822,7 +6474,9 @@ autoHint: function(w) { var res; if (!w.requiredChoice && !w.selectMultiple) return false; - if (!w.selectMultiple) return '*'; + if (!w.selectMultiple) { + return w.displayRequired ? w.requiredMark : false; + } res = '('; if (!w.requiredChoice) { if ('number' === typeof w.selectMultiple) { @@ -5847,9 +6501,12 @@ } } res += ')'; - if (w.requiredChoice) res += ' *'; + if (w.requiredChoice && w.displayRequired) { + res += ' ' + w.requiredMark; + } return res; }, + error: function(w, value) { if (value !== null && ('number' === typeof w.correctChoice || @@ -5858,18 +6515,16 @@ return 'Not correct, try again.'; } return 'Selection required.'; - } - // correct: 'Correct.' - }; + }, - ChoiceTable.separator = '::'; + other: 'Other', - // ## Dependencies + customInput: 'Please specify.' - ChoiceTable.dependencies = { - JSUS: {} }; + ChoiceTable.separator = '::'; + /** * ## ChoiceTable constructor * @@ -5913,21 +6568,26 @@ * @see ChoiceTable.onclick */ this.listener = function(e) { - var name, value, td, tr; - var i, len, removed; + var value, td, ci; + var i, len, removed, otherSel; e = e || window.event; td = e.target || e.srcElement; // See if it is a clickable choice. - if ('undefined' === typeof that.choicesIds[td.id]) { + ci = that.choicesIds; + if ('undefined' === typeof ci[td.id]) { // It might be a nested element, try the parent. td = td.parentNode; if (!td) return; - if ('undefined' === typeof that.choicesIds[td.id]) { + if ('undefined' === typeof ci[td.id]) { td = td.parentNode; - if (!td || 'undefined' === typeof that.choicesIds[td.id]) { - return; + if (!td) return; + if ('undefined' === typeof ci[td.id]) { + td = td.parentNode; + if (!td || 'undefined' === typeof ci[td.id]) { + return; + } } } } @@ -5959,8 +6619,27 @@ // One more click. that.numberOfClicks++; + removed = that.isChoiceCurrent(value); + len = that.choices.length; + + if (that.customInput) { + // Is "Other" currently selected? + otherSel = value === (len - 1); + + if (otherSel && !removed && + // Fixed Select multiple (not all max choices selected). + ('number' !== typeof that.selectMultiple || + (that.selectMultiple > that.currentChoice.length)) + ) { + that.customInput.show(); + } + else if (!that.selectMultiple || otherSel) { + that.customInput.hide(); + } + } + // Click on an already selected choice. - if (that.isChoiceCurrent(value)) { + if (removed) { that.unsetCurrentChoice(value); J.removeClass(td, 'selected'); @@ -5977,7 +6656,6 @@ else { that.selected = null; } - removed = true; } // Click on a new choice. else { @@ -6020,6 +6698,10 @@ value = parseInt(value, 10); that.onclick.call(that, value, removed, td); } + + that.lastClicked = value; + + if (that.doneOnClick) node.done(); }; /** @@ -6133,6 +6815,15 @@ */ this.rightCell = null; + /** + * ### ChoiceTable.header + * + * Header to be displayed above the table + * + * @experimental + */ + this.header = null; + /** * ### ChoiceTable.errorBox * @@ -6231,6 +6922,28 @@ */ this.currentChoice = null; + /** + * ### ChoiceTable.defaultChoice + * + * Choice/s initially selected when the widget is inited + * + * @see ChoiceTable.selectMultiple + * + * @see ChoiceTable.selected + */ + this.defaultChoice = null; + + /** + * ### ChoiceTable._initDefaultChoice + * + * Flags that default choices still need to be added + * + * @see ChoiceTable.defaultChoice + * + * @api private + */ + this._initDefaultChoice = null; + /** * ### ChoiceTable.selectMultiple * @@ -6350,9 +7063,88 @@ /** * ### ChoiceTable.sameWidthCells * - * If TRUE, cells have same width regardless of content + * If truthy, it forces cells to have same width regardless of content + * + * - If TRUE, it automatically computes the equal size of the cells + * (options `left` and `right` affect computation). + * - If string, it is the value of width for all cells + * + * Only applies in horizontal mode. */ this.sameWidthCells = true; + + /** + * ### ChoiceTable.other + * + * If TRUE, adds an "Other" choice as last choice + * + * Accepted values: + * - true: adds "Other" choice as last choice. + * - 'CustomInput': adds "Other" choice AND a CustomInput widget below + * the choicetable (initially hidden). + * - object: as previous, but it also allows for custom options for the + * custom input + * + * @see ChoiceTable.customInput + */ + this.other = null; + + /** + * ### ChoiceTable.customInput + * + * The customInput widget + * + * @see ChoiceTable.other + */ + this.customInput = null; + + /** + * ### ChoiceTable.lastClicked + * + * The idx of the last selected choice + */ + this.lastClicked = null; + + /** + * ### ChoiceTable.doneOnClick + * + * If TRUE, node.done() will be invoked after the first click + */ + this.doneOnClick = null; + + /** + * ### ChoiceTable.solution + * + * Additional information to be displayed after a selection is confirmed + * + * If no answer is provided and the next method is triggered, the + * solution is displayed only if solutionNoChoice is TRUE + * + * @see ChoiceTable.solutionNoChoice + * @see ChoiceTable.next + */ + this.solution = null; + + /** + * ### ChoiceTable.solutionDisplayed + * + * TRUE, if the solution is currently displayed + */ + this.solutionDisplayed = false; + + /** + * ### ChoiceTable.solutionNoChoice + * + * TRUE, he solution is displayed upon trigger even with no choice + */ + this.solutionNoChoice = false; + + /** + * ### ChoiceTable.solutionDiv + * + * The
element containing the solution + */ + this.solutionDiv = null; } // ## ChoiceTable methods @@ -6397,7 +7189,7 @@ * @param {object} opts Configuration options */ ChoiceTable.prototype.init = function(opts) { - var tmp, that; + var tmp, that, i; that = this; if (!this.id) { @@ -6527,24 +7319,44 @@ } // Set the mainText, if any. - if ('string' === typeof opts.mainText) { - this.mainText = opts.mainText; + tmp = opts.mainText + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp) { + throw new TypeError('ChoiceTable.init: opts.mainText cb ' + + 'must return a string. Found: ' + + tmp); + } } - else if ('undefined' !== typeof opts.mainText) { + if ('string' === typeof tmp) { + this.mainText = tmp; + } + else if ('undefined' !== typeof tmp) { throw new TypeError('ChoiceTable.init: opts.mainText must ' + - 'be string or undefined. Found: ' + - opts.mainText); + 'be function, string or undefined. Found: ' + + tmp); } // Set the hint, if any. - if ('string' === typeof opts.hint || false === opts.hint) { - this.hint = opts.hint; - if (this.requiredChoice) this.hint += ' *'; + tmp = opts.hint; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp && false !== tmp) { + throw new TypeError('ChoiceTable.init: opts.hint cb must ' + + 'return string or false. Found: ' + + tmp); + } } - else if ('undefined' !== typeof opts.hint) { + if ('string' === typeof tmp || false === tmp) { + this.hint = tmp; + if (this.requiredChoice && tmp !== false && this.displayRequired) { + this.hint += ' ' + this.requiredMark; + } + } + else if ('undefined' !== typeof tmp) { throw new TypeError('ChoiceTable.init: opts.hint must ' + 'be a string, false, or undefined. Found: ' + - opts.hint); + tmp); } else { // Returns undefined if there are no constraints. @@ -6584,10 +7396,18 @@ 'separator option. Found: ' + this.separator); } - if ('string' === typeof opts.left || - 'number' === typeof opts.left) { - - this.left = '' + opts.left; + // left. + tmp = opts.left; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp && 'undefined' !== typeof tmp) { + throw new TypeError('ChoiceTable.init: opts.left cb must ' + + 'return string or undefined. Found: ' + + tmp); + } + } + if ('string' === typeof tmp || 'number' === typeof tmp) { + this.left = '' + tmp; } else if (J.isNode(opts.left) || J.isElement(opts.left)) { @@ -6595,19 +7415,24 @@ this.left = opts.left; } else if ('undefined' !== typeof opts.left) { - throw new TypeError('ChoiceTable.init: opts.left must ' + - 'be string, number, an HTML Element or ' + - 'undefined. Found: ' + opts.left); + throw new TypeError('ChoiceTable.init: opts.left must be string, ' + + 'number, function, an HTML Element or ' + + 'undefined. Found: ' + tmp); } - if ('string' === typeof opts.right || - 'number' === typeof opts.right) { - - this.right = '' + opts.right; + tmp = opts.right; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp && 'undefined' !== typeof tmp) { + throw new TypeError('ChoiceTable.init: opts.right cb must ' + + 'return string or undefined. Found: ' + + tmp); + } } - else if (J.isNode(opts.right) || - J.isElement(opts.right)) { - + if ('string' === typeof tmp || 'number' === typeof tmp) { + this.right = '' + tmp; + } + else if (J.isNode(opts.right) || J.isElement(opts.right)) { this.right = opts.right; } else if ('undefined' !== typeof opts.right) { @@ -6669,11 +7494,14 @@ // Add the correct choices. - if ('undefined' !== typeof opts.choicesSetSize) { - if (!J.isInt(opts.choicesSetSize, 0)) { + tmp = opts.choicesSetSize; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + } + if ('undefined' !== typeof tmp) { + if (!J.isInt(tmp, 0)) { throw new Error('ChoiceTable.init: choicesSetSize must be ' + - 'undefined or an integer > 0. Found: ' + - opts.choicesSetSize); + 'undefined or an integer > 0. Found: ' + tmp); } if (this.left || this.right) { @@ -6682,47 +7510,132 @@ 'right options are set.'); } - this.choicesSetSize = opts.choicesSetSize; + this.choicesSetSize = tmp; + } + + // Add other. + if ('undefined' !== typeof opts.sameWidthCells) { + this.sameWidthCells = opts.sameWidthCells; + } + + // Add other. + if ('undefined' !== typeof opts.other) { + this.other = opts.other; } // Add the choices. - if ('undefined' !== typeof opts.choices) { - this.setChoices(opts.choices); + tmp = opts.choices; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if (!J.isArray(tmp) || !tmp.length) { + throw new TypeError('ChoiceTable.init: opts.choices cb must ' + + 'return a non-empty array. Found: ' + tmp); + } + } + if ('undefined' !== typeof tmp) { + this.setChoices(tmp); } // Add the correct choices. - if ('undefined' !== typeof opts.correctChoice) { + tmp = opts.correctChoice; + if ('undefined' !== typeof tmp) { if (this.requiredChoice) { - throw new Error('ChoiceTable.init: cannot specify both ' + - 'opts requiredChoice and correctChoice'); + this.requiredChoice = null; + this.required = null; + node.warn('ChoiceTable.init: requiredChoice and ' + + 'correctChoice are both set; requiredChoice ignored.' + ); + } + if ('function' === typeof tmp) { + tmp = tmp.call(this); + // No checks. } this.setCorrectChoice(opts.correctChoice); } // Add the correct choices. - if ('undefined' !== typeof opts.disabledChoices) { + tmp = opts.disabledChoices; + if ('undefined' !== typeof tmp) { + if ('function' === typeof tmp) { + tmp = tmp.call(this); + } if (!J.isArray(opts.disabledChoices)) { - throw new Error('ChoiceTable.init: disabledChoices must be ' + - 'undefined or array. Found: ' + - opts.disabledChoices); + throw new TypeError('ChoiceTable.init: disabledChoices ' + + 'must be undefined or array. Found: ' + + tmp); } // TODO: check if values of disabled choices are correct? // Do we have the choices now, or can they be added later? - tmp = opts.disabledChoices.length; if (tmp) { (function() { - for (var i = 0; i < tmp; i++) { - that.disableChoice(opts.disabledChoices[i]); + for (i = 0; i < tmp.length; i++) { + that.disableChoice(tmp[i]); } })(); } } - if ('undefined' === typeof opts.sameWidthCells) { - this.sameWidthCells = !!opts.sameWidthCells; + if ('undefined' !== typeof opts.doneOnClick) { + this.doneOnClick = !!opts.doneOnClick; + } + + tmp = opts.solution; + if ('undefined' !== typeof tmp) { + if ('string' !== typeof tmp && 'function' !== typeof tmp) { + throw new TypeError('ChoiceTable.init: solution must be ' + + 'string or undefined. Found: ' + tmp); + } + this.solution = tmp; + } + + tmp = opts.defaultChoice; + if ('undefined' !== typeof tmp) { + this.defaultChoice = tmp; + initDefaultChoice(this); + } + + if (opts.header) { + tmp = opts.header; + // One td will colspan all choices. + if ('string' === typeof tmp) { + tmp = [ tmp ]; + } + else if (!J.isArray(tmp) || + (tmp.length !== 1 && tmp.length !== opts.choices.length)) { + + throw new Error('ChoiceTableGroup.init: header ' + + 'must be string, array (size ' + + opts.choices.length + + '), or undefined. Found: ' + tmp); + } + + this.header = tmp; + } + + }; + + /** + * ### ChoiceTable.clickChoice + * + * Clicks on a choice + * + * @param {string|number} idx The idx of the choice to click on + */ + ChoiceTable.prototype.clickChoice = function(idx) { + if (!this.choicesCells) { + throw new Error('ChoiceTable.clickChoice: choicesCells not ' + + 'initialized.'); + } + if (J.isInt(idx) === false) { + throw new TypeError('ChoiceTable.clickChoice: idx must be ' + + 'integer. Found: ' + idx); } + if (!this.choicesCells[idx]) { + throw new Error('ChoiceTable.clickChoice: idx not found: ' + idx); + } + this.choicesCells[idx].click(); }; /** @@ -6730,10 +7643,13 @@ * * Marks a choice as disabled (will not be clickable) * - * @param {string|number} value The value of the choice to disable` + * @param {string|number} idx The idx of the choice to disable */ - ChoiceTable.prototype.disableChoice = function(value) { - this.disabledChoices[value] = true; + ChoiceTable.prototype.disableChoice = function(idx) { + if (!this.disabledChoices[idx]) { + this.disabledChoices[idx] = true; + J.addClass(this.choicesCells[idx], 'disabled'); + } }; /** @@ -6741,10 +7657,13 @@ * * Enables a choice (will be clickable again if previously disabled) * - * @param {string|number} value The value of the choice to disable` + * @param {string|number} idx The value of the choice to disable */ - ChoiceTable.prototype.enableChoice = function(value) { - this.disabledChoices[value] = null; + ChoiceTable.prototype.enableChoice = function(idx) { + if (this.disabledChoices[idx]) { + this.disabledChoices[idx] = null; + J.removeClass(this.choicesCells[idx], 'disabled'); + } }; /** @@ -6764,7 +7683,7 @@ * @see ChoiceTable.buildTableAndChoices */ ChoiceTable.prototype.setChoices = function(choices) { - var len; + var len, idxOther; if (!J.isArray(choices)) { throw new TypeError('ChoiceTable.setChoices: choices ' + 'must be array'); @@ -6772,6 +7691,11 @@ if (!choices.length) { throw new Error('ChoiceTable.setChoices: choices array is empty'); } + // Check and drop previous "other" choices. + if (this.other) { + idxOther = choices.indexOf(this.getText('other')); + if (idxOther >= 0) choices.splice(idxOther, 1); + } this.choices = choices; len = choices.length; @@ -6779,6 +7703,48 @@ this.order = J.seq(0, len-1); if (this.shuffleChoices) this.order = J.shuffle(this.order); + // Loop through all choices and see if there is any fixed position. + // TODO: we could add validation here. + (function(w) { + var i, c, fixedPos, idxOrder, allFixedPos = [], allFixedLen; + // See if there is any fixed-choice. + for (i = -1 ; ++i < len ; ) { + fixedPos = undefined; + idxOrder = w.order[i]; + c = choices[idxOrder]; + if (J.isArray(c)) { + // Third position after id and text is fixedPos. + fixedPos = c[2]; + } + else if ('object' === typeof choices[i]) { + fixedPos = c.fixedPos; + } + if ('undefined' !== typeof fixedPos) { + allFixedPos.push({ fixed: fixedPos, pos: i, idx: idxOrder}); + } + } + // All fixed position collected, we need to sort them from + // lowest to highest, then we can do the placing. + allFixedLen = allFixedPos.length; + if (allFixedLen) { + if (allFixedLen > 1) { + allFixedPos.sort(function(a, b) {return a.fixed < b.fixed}); + } + for (i = -1 ; ++i < allFixedLen ; ) { + c = allFixedPos[i]; + // Remove from old position and place it in new one. + w.order.splice(c.pos, 1); + w.order.splice(c.fixed, 0, c.idx); + } + } + })(this) + + // Add 'Other' field at the end. + if (this.other) { + this.choices[len] = this.getText('other'); + this.order[len] = len + } + // Build the table and choices at once (faster). if (this.table) this.buildTableAndChoices(); // Or just build choices. @@ -6799,12 +7765,13 @@ * @see ChoiceTable.renderSpecial */ ChoiceTable.prototype.buildChoices = function() { - var i, len; - i = -1, len = this.choices.length; + var len, pos, idx; + pos = -1, len = this.choices.length; // Pre-allocate the choicesCells array. this.choicesCells = new Array(len); - for ( ; ++i < len ; ) { - this.renderChoice(this.choices[this.order[i]], i); + for ( ; ++pos < len ; ) { + idx = this.order[pos]; + this.renderChoice(this.choices[idx], idx, pos); } if (this.left) this.renderSpecial('left', this.left); if (this.right) this.renderSpecial('right', this.right); @@ -6826,10 +7793,33 @@ ChoiceTable.prototype.buildTable = (function() { function makeSet(i, len, H, doSets) { - var tr, counter; + var tr, td, counter, pos; counter = 0; // Start adding tr/s and tds based on the orientation. if (H) { + + if (this.header) { + tr = W.add('tr', this.table); + + // Add empty left header cell, if needed. + if (this.left) W.add('td', tr, { className: 'header' }); + + for ( ; ++i < this.header.length ; ) { + td = W.add('td', tr, { + innerHTML: this.header[i], + className: 'header' + }); + } + + // Only one element, header spans throughout. + if (i === 1) td.setAttribute('colspan', this.choices.length); + + // Add empty right header cell, if needed. + if (this.right) W.add('td', tr, { className: 'header' }); + + i = -1; + } + tr = createTR(this, 'main'); // Add horizontal choices title. if (this.leftCell) tr.appendChild(this.leftCell); @@ -6845,7 +7835,8 @@ } } // Clickable cell. - tr.appendChild(this.choicesCells[i]); + pos = this.order[i]; + tr.appendChild(this.choicesCells[pos]); // Stop if we reached set size (still need to add the right). if (doSets && ++counter >= this.choicesSetSize) break; } @@ -6889,7 +7880,7 @@ * @see ChoiceTable.orientation */ ChoiceTable.prototype.buildTableAndChoices = function() { - var i, len, tr, td, H; + var i, idx, len, tr, td, H; len = this.choices.length; // Pre-allocate the choicesCells array. @@ -6918,7 +7909,8 @@ } } // Clickable cell. - td = this.renderChoice(this.choices[this.order[i]], i); + idx = this.order[i]; + td = this.renderChoice(this.choices[idx], idx, i); tr.appendChild(td); } if (this.right) { @@ -6989,7 +7981,7 @@ * text to display as choice, or an object with properties value and * display. If a renderer function is defined there are no restriction * on the format of choice. - * @param {number} idx The position of the choice within the choice array + * @param {number} idx The position of the choice within the choices array * * @return {HTMLElement} td The newly created cell of the table * @@ -6997,17 +7989,23 @@ * @see ChoiceTable.separator * @see ChoiceTable.choicesCells */ - ChoiceTable.prototype.renderChoice = function(choice, idx) { + ChoiceTable.prototype.renderChoice = function(choice, idx, pos) { var td, shortValue, value, width; td = document.createElement('td'); if (this.tabbable) J.makeTabbable(td); // Forces equal width. if (this.sameWidthCells && this.orientation === 'H') { - width = this.left ? 70 : 100; - if (this.right) width = width - 30; - width = width / (this.choicesSetSize || this.choices.length); - td.style.width = width.toFixed(2) + '%'; + if (this.sameWidthCells === true) { + width = this.left ? 70 : 100; + if (this.right) width = width - 20; + width = width / (this.choicesSetSize || this.choices.length); + width = width.toFixed(2) + '%'; + } + else { + width = this.sameWidthCells; + } + td.style.width = width; } // Use custom renderer. @@ -7026,7 +8024,8 @@ choice = choice.display; } - value = this.shuffleChoices ? this.order[idx] : idx; + // value = this.shuffleChoices ? this.order[idx] : idx; + value = idx; if ('string' === typeof choice || 'number' === typeof choice) { td.innerHTML = choice; @@ -7055,7 +8054,7 @@ } // All fine, updates global variables. - this.choicesValues[value] = idx; + this.choicesValues[value] = pos; this.choicesCells[idx] = td; this.choicesIds[td.id] = td; @@ -7117,7 +8116,7 @@ ChoiceTable.prototype.append = function() { var tmp; // Id must be unique. - if (W.getElementById(this.id)) { + if (W.gid(this.id)) { throw new Error('ChoiceTable.append: id is not ' + 'unique: ' + this.id); } @@ -7157,6 +8156,11 @@ this.errorBox = W.append('div', this.bodyDiv, { className: 'errbox' }); + this.setCustomInput(this.other, this.bodyDiv); + + if (this.solution) { + this.solutionDiv = W.append('div', this.bodyDiv); + } // Creates a free-text textarea, possibly with placeholder text. if (this.freeText) { @@ -7170,6 +8174,33 @@ // Append textarea. this.bodyDiv.appendChild(this.textarea); } + + // Inits default choices, if necessary. + if (this._initDefaultChoice) initDefaultChoice(this); + }; + + /** + * ### ChoiceTable.setCustomInput + * + * Set Custom Input widget. + * + */ + ChoiceTable.prototype.setCustomInput = function(other, root) { + var opts; + if (other === null || 'boolean' === typeof other) return; + opts = { + id: 'other' + this.id, + mainText: this.getText('customInput'), + requiredChoice: this.requiredChoice, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark + }; + // other is the string 'CustomInput' or a conf object. + if ('object' === typeof other) J.mixin(opts, other); + // Force initially hidden. + opts.hidden = true; + this.customInput = node.widgets.append('CustomInput', root, opts); + }; /** @@ -7224,6 +8255,7 @@ // Remove listener to make cells clickable with the keyboard. if (this.tabbable) J.makeClickable(this.table, false); } + if (this.customInput) this.customInput.disable(); this.emit('disabled'); }; @@ -7244,6 +8276,7 @@ this.table.addEventListener('click', this.listener); // Add listener to make cells clickable with the keyboard. if (this.tabbable) J.makeClickable(this.table); + if (this.customInput) this.customInput.enable(); this.emit('enabled'); }; @@ -7268,9 +8301,24 @@ * @see ChoiceTable.attempts * @see ChoiceTable.setCorrectChoice */ - ChoiceTable.prototype.verifyChoice = function(markAttempt) { + ChoiceTable.prototype.verifyChoice = function(markAttempt) { var i, len, j, lenJ, c, clone, found; - var correctChoice; + var correctChoice, ci, ciCorrect; + + // Mark attempt by default. + markAttempt = 'undefined' === typeof markAttempt ? true : markAttempt; + if (markAttempt) this.attempts.push(this.currentChoice); + + // Custom input to check. + ci = this.customInput && !this.customInput.isHidden(); + if (ci) { + ciCorrect = this.customInput.getValues({ + markAttempt: markAttempt + }).isCorrect; + if (ciCorrect === false) return false; + // Set it to null so it is returned correctly, later below. + if ('undefined' === typeof ciCorrect) ciCorrect = null; + } // Check the number of choices. if (this.requiredChoice !== null) { @@ -7278,40 +8326,40 @@ else return this.currentChoice.length >= this.requiredChoice; } - // If no correct choice is set return null. - if ('undefined' === typeof this.correctChoice) return null; - // Mark attempt by default. - markAttempt = 'undefined' === typeof markAttempt ? true : markAttempt; - if (markAttempt) this.attempts.push(this.currentChoice); - if (!this.selectMultiple) { - return this.currentChoice === this.correctChoice; - } - else { - // Make it an array (can be a string). - correctChoice = J.isArray(this.correctChoice) ? - this.correctChoice : [this.correctChoice]; + correctChoice = this.correctChoice; + // If no correct choice is set return null or ciCorrect (true|null). + if (null === correctChoice) return ci ? ciCorrect : null; - len = correctChoice.length; - lenJ = this.currentChoice.length; - // Quick check. - if (len !== lenJ) return false; - // Check every item. - i = -1; - clone = this.currentChoice.slice(0); - for ( ; ++i < len ; ) { - found = false; - c = correctChoice[i]; - j = -1; - for ( ; ++j < lenJ ; ) { - if (clone[j] === c) { - found = true; - break; - } + // Only one choice allowed, ci is correct, + // otherwise we would have returned already. + if (!this.selectMultiple) return this.currentChoice === correctChoice; + + // Multiple selections allowed. + + // Make it an array (can be a string). + if (!J.isArray(correctChoice)) correctChoice = [correctChoice]; + + len = correctChoice.length; + lenJ = this.currentChoice.length; + // Quick check. + if (len !== lenJ) return false; + // Check every item. + i = -1; + clone = this.currentChoice.slice(0); + for ( ; ++i < len ; ) { + found = false; + c = correctChoice[i]; + j = -1; + for ( ; ++j < lenJ ; ) { + if (clone[j] === c) { + found = true; + break; } - if (!found) return false; } - return true; + if (!found) return false; } + return true; + }; /** @@ -7417,18 +8465,26 @@ * * Highlights the choice table * - * @param {string} The style for the table's border. + * @param {string|obj} opts Optional. If string is the 'border' + * option for backward compatibilityThe style for the table's border. * Default '3px solid red' * * @see ChoiceTable.highlighted */ - ChoiceTable.prototype.highlight = function(border) { + ChoiceTable.prototype.highlight = function(opts) { + var border, ci; + opts = opts || {}; + // Backward compatible. + if ('string' === typeof opts) opts = { border: opts }; + border = opts.border; if (border && 'string' !== typeof border) { throw new TypeError('ChoiceTable.highlight: border must be ' + 'string or undefined. Found: ' + border); } if (!this.table || this.highlighted) return; this.table.style.border = border || '3px solid red'; + ci = this.customInput; + if (opts.customInput !== false && ci && !ci.isHidden()) ci.highlight(); this.highlighted = true; this.emit('highlighted', border); }; @@ -7440,9 +8496,15 @@ * * @see ChoiceTable.highlighted */ - ChoiceTable.prototype.unhighlight = function() { + ChoiceTable.prototype.unhighlight = function(opts) { + var ci; + opts = opts || {}; if (!this.table || this.highlighted !== true) return; this.table.style.border = ''; + ci = this.customInput; + if (opts.customInput !== false && ci && !ci.isHidden()) { + ci.unhighlight(); + } this.highlighted = false; this.setError(); this.emit('unhighlighted'); @@ -7476,7 +8538,10 @@ * @see ChoiceTable.reset */ ChoiceTable.prototype.getValues = function(opts) { - var obj, resetOpts, i, len; + var obj, resetOpts, i, len, ci, ciCorrect; + var that; + + that = this; opts = opts || {}; obj = { id: this.id, @@ -7494,20 +8559,21 @@ // Option getValue backward compatible. if (opts.addValue !== false && opts.getValue !== false) { if (!this.selectMultiple) { - obj.value = getValueFromChoice(this.choices[obj.choice]); + obj.value = getValueFromChoice(that,this.choices[obj.choice]); } else { len = obj.choice.length; obj.value = new Array(len); if (len === 1) { obj.value[0] = - getValueFromChoice(this.choices[obj.choice[0]]); + getValueFromChoice(that,this.choices[obj.choice[0]]); } else { i = -1; for ( ; ++i < len ; ) { obj.value[i] = - getValueFromChoice(this.choices[obj.choice[i]]); + getValueFromChoice(that, + this.choices[obj.choice[i]]); } if (opts.sortValue !== false) obj.value.sort(); } @@ -7520,18 +8586,43 @@ if (this.groupOrder === 0 || this.groupOrder) { obj.groupOrder = this.groupOrder; } - if (null !== this.correctChoice || null !== this.requiredChoice) { + + ci = this.customInput; + if (this.required !== false && + (null !== this.correctChoice || null !== this.requiredChoice || + (ci && !ci.isHidden()))) { + obj.isCorrect = this.verifyChoice(opts.markAttempt); obj.attempts = this.attempts; - if (!obj.isCorrect && opts.highlight) this.highlight(); + if (!obj.isCorrect && opts.highlight) this.highlight({ + // If errored, it is already highlighted + customInput: false + }); } + if (this.textarea) obj.freetext = this.textarea.value; + if (obj.isCorrect === false) { - this.setError(this.getText('error', obj.value)); + // If there is an error on CI, we just highlight CI. + // However, there could be an error also on the choice table, + // e.g., not enough options selected. It will be catched + // at next click. + // TODO: change verifyChoice to say where the error is coming from. + if (ci) { + ciCorrect = ci.getValues({ + markAttempt: false + }).isCorrect; + } + if (ci && !ciCorrect && !ci.isHidden()) { + this.unhighlight({ customInput: false }); + } + else { + this.setError(this.getText('error', obj.value)); + } } else if (opts.reset) { - resetOpts = 'object' !== typeof opts.reset ? {} : opts.reset; - this.reset(resetOpts); + resetOpts = 'object' !== typeof opts.reset ? {} : opts.reset; + this.reset(resetOpts); } return obj; }; @@ -7595,6 +8686,7 @@ // Set values, random or pre-set. i = -1; + // Pre-set. if ('undefined' !== typeof options.values) { if (!J.isArray(options.values)) tmp = [ options.values ]; len = tmp.length; @@ -7653,6 +8745,9 @@ // Make a random comment. if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); + if (this.customInput && !this.customInput.isHidden()) { + this.customInput.setValues(); + } }; /** @@ -7693,6 +8788,7 @@ if (this.isHighlighted()) this.unhighlight(); if (options.shuffleChoices) this.shuffle(); + if (this.customInput) this.customInput.reset(); }; /** @@ -7707,8 +8803,15 @@ var parentTR; H = this.orientation === 'H'; - order = J.shuffle(this.order); - i = -1, len = order.length; + len = this.order.length; + if (this.other) { + order = J.shuffle(this.order.slice(0,-1)); + order.push(this.order[len - 1]); + } + else { + order = J.shuffle(this.order); + } + i = -1; choicesValues = {}; choicesCells = new Array(len); @@ -7741,6 +8844,65 @@ this.choicesValues = choicesValues; }; + /** + * ### ChoiceManager.setValues + * + * Sets values for forms in manager as specified by the options + * + * @param {object} options Optional. Options specifying how to set + * the values. If no parameter is specified, random values will + * be set. + */ + ChoiceTable.prototype.next = function() { + var sol; + sol = this.solution; + // No solution or solution already displayed. + if (!sol || this.solutionDisplayed) return false; + // Solution, but no answer provided. + if (sol) { + if (!this.isChoiceDone() && !this.solutionNoChoice) return false; + this.solutionDisplayed = true; + if ('function' === typeof sol) { + sol = this.solution(this.verifyChoice(false), this); + } + this.solutionDiv.innerHTML = sol; + } + this.disable(); + W.adjustFrameHeight(); + node.emit('WIDGET_NEXT', this); + return true; + }; + + ChoiceTable.prototype.prev = function() { + return false; + if (!this.solutionDisplayed) return false; + this.solutionDisplayed = false; + this.solutionDiv.innerHTML = ''; + this.enable(); + W.adjustFrameHeight(); + node.emit('WIDGET_PREV', this); + return true; + }; + + ChoiceTable.prototype.isChoiceDone = function(complete) { + var cho, mul, len, ci; + ci = this.customInput; + cho = this.currentChoice; + mul = this.selectMultiple; + // Selected "Other, Specify" + if (ci && this.isChoiceCurrent(this.choices.length-1)) return false; + // Single choice. + if ((!complete || !mul) && null !== cho) return true; + // Multiple choices. + if (J.isArray(cho)) len = cho.length; + if (mul === true && len === this.choices.length) return true; + if ('number' === typeof mul && len === mul) return true; + // Not done. + return false; + }; + + + // ## Helper methods. /** @@ -7805,6 +8967,7 @@ * The value is either the text displayed or short value specified * by the choice. * + * @param {ChoiceTable} that This instance * @param {mixed} choice * @param {boolean} display TRUE to return the display value instead * one. Default: FALSE. @@ -7815,7 +8978,10 @@ * @see ChoiceTable.getValues * @see ChoiceTable.renderChoice */ - function getValueFromChoice(choice, display) { + function getValueFromChoice(that, choice, display) { + if (choice === that.getText('other') && that.customInput) { + return that.customInput.getValues().value; + } if ('string' === typeof choice || 'number' === typeof choice) { return choice; } @@ -7823,15 +8989,45 @@ if ('object' === typeof choice) { return choice[ display ? 'display' : 'value' ]; } - if (J.isElement(choice) || J.isNode(choice)) return choice.innerHTML; - return null; + if (J.isElement(choice) || J.isNode(choice)) return choice.innerHTML; + return null; + } + + /** + * ### initDefaultChoice + * + * Clicks on the default choices if they exist, or mark it as todo + * + * @param {ChoiceTable} that This instance + * + * @see ChoiceTable._initDefaultChoice + */ + function initDefaultChoice(that) { + var choice; + choice = that.defaultChoice; + // Already appended. + if (that.table) { + if (J.isArray(choice)) { + for (i = 0; i < choice.length; i++) { + that.clickChoice(i); + } + } + else { + that.clickChoice(choice); + } + that._initDefaultChoice = false; + } + else { + // Mark the choice to be inited as soon as possible. + that._initDefaultChoice = true; + } } })(node); /** * # ChoiceTableGroup - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a table that groups together several choice tables widgets @@ -7848,29 +9044,23 @@ // ## Meta-data - ChoiceTableGroup.version = '1.8.0'; + ChoiceTableGroup.version = '1.9.0'; ChoiceTableGroup.description = 'Groups together and manages sets of ' + 'ChoiceTable widgets.'; - ChoiceTableGroup.title = 'Make your choice'; ChoiceTableGroup.className = 'choicetable choicetablegroup'; ChoiceTableGroup.separator = '::'; ChoiceTableGroup.texts = { - autoHint: function(w) { - if (w.requiredChoice) return '*'; - else return false; - }, - error: 'Selection required.' }; // ## Dependencies ChoiceTableGroup.dependencies = { - JSUS: {} + ChoiceTable: {} }; /** @@ -8246,6 +9436,17 @@ * @see ChoiceTable.tabbable */ this.tabbable = null; + + /** + * ### ChoiceTableGroup.valueOnly + * + * If TRUE, `getValues` returns only the field `value` from ChoiceTable + * + * Default FALSE + * + * @see ChoiceTableGroup.getValues + */ + this.valueOnly = null; } // ## ChoiceTableGroup methods @@ -8396,11 +9597,25 @@ 'be a string, false, or undefined. Found: ' + opts.hint); } - else { - // Returns undefined if there are no constraints. - this.hint = this.getText('autoHint'); + + if (this.required && this.hint !== false && + opts.displayRequired !== false) { + + tmp = this.requiredMark; + + if (this.hint) { + if (this.hint.charAt(this.hint.length-1) !== tmp) { + this.hint += ' ' + tmp; + } + } + else { + this.hint = tmp; + } + } + // this.hint = node.widgets.utils.processHints(opts.hint); + // Set the timeFrom, if any. if (opts.timeFrom === false || 'string' === typeof opts.timeFrom) { @@ -8451,6 +9666,8 @@ if (opts.tabbable !== false) this.tabbable = true; + if (opts.valueOnly === true) this.valueOnly = true; + // Separator checked by ChoiceTable. if (opts.separator) this.separator = opts.separator; @@ -8473,16 +9690,21 @@ opts.freeText : !!opts.freeText; if (opts.header) { - if (!J.isArray(opts.header) || - opts.header.length !== opts.choices.length) { + tmp = opts.header; + // One td will colspan all choices. + if ('string' === typeof tmp) { + tmp = [ tmp ]; + } + else if (!J.isArray(tmp) || + (tmp.length !== 1 && tmp.length !== opts.choices.length)) { throw new Error('ChoiceTableGroup.init: header ' + - 'must be an array of length ' + + 'must be string, array (size ' + opts.choices.length + - ' or undefined. Found: ' + opts.header); + '), or undefined. Found: ' + tmp); } - this.header = opts.header; + this.header = tmp; } @@ -8538,7 +9760,7 @@ * @see ChoiceTableGroup.order */ ChoiceTableGroup.prototype.buildTable = function() { - var i, len, tr, H, ct; + var i, len, td, tr, H, ct; var j, lenJ, lenJOld, hasRight, cell; H = this.orientation === 'H'; @@ -8551,11 +9773,13 @@ className: 'header' }); for ( ; ++i < this.header.length ; ) { - W.add('td', tr, { + td = W.add('td', tr, { innerHTML: this.header[i], className: 'header' }); } + // Only one element, header spans throughout. + if (i === 1) td.setAttribute('colspan', this.choices.length); i = -1; } @@ -8894,6 +10118,8 @@ * - reset: If TRUTHY and no item raises an error, * then it resets the state of all items before * returning it. Default: FALSE. + * - valueOnly: If TRUE it returns only the value of each ChoiceTable + * instead of the all object from .getValues(). Experimental. * * @return {object} Object containing the choice and paradata * @@ -8901,10 +10127,11 @@ * @see ChoiceTableGroup.reset */ ChoiceTableGroup.prototype.getValues = function(opts) { - var obj, i, len, tbl, toHighlight, toReset; + var obj, i, len, tbl, toHighlight, toReset, res, valueOnly; obj = { id: this.id, order: this.order, + nClicks: 0, items: {}, isCorrect: true }; @@ -8913,18 +10140,23 @@ // Make sure reset is done only at the end. toReset = opts.reset; opts.reset = false; + valueOnly = opts.valueOnly === true || this.valueOnly; i = -1, len = this.items.length; for ( ; ++i < len ; ) { tbl = this.items[i]; - obj.items[tbl.id] = tbl.getValues(opts); - if (obj.items[tbl.id].choice === null) { + res = tbl.getValues(opts); + obj.items[tbl.id] = valueOnly ? res.value : res; + if (res.choice === null) { obj.missValues = true; - if (tbl.requiredChoice) { + if (this.required || tbl.requiredChoice) { toHighlight = true; obj.isCorrect = false; } } - if (obj.items[tbl.id].isCorrect === false && opts.highlight) { + else { + obj.nClicks += res.nClicks; + } + if (res.isCorrect === false && opts.highlight) { toHighlight = true; } } @@ -9089,7 +10321,6 @@ s.group = that.id; s.groupOrder = i+1; s.orientation = that.orientation; - s.title = false; s.listeners = false; s.separator = that.separator; @@ -9186,7 +10417,7 @@ /** * # Consent - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2024 Stefano Balietti * MIT Licensed * * Displays a consent form with buttons to accept/reject it @@ -9201,10 +10432,9 @@ // ## Meta-data - Consent.version = '0.3.0'; + Consent.version = '0.8.0'; Consent.description = 'Displays a configurable consent form.'; - Consent.title = false; Consent.panel = false; Consent.className = 'consent'; @@ -9236,21 +10466,25 @@ * * Creates a new instance of Consent * - * @param {object} options Optional. Configuration options - * which is forwarded to Consent.init. - * * @see Consent.init */ function Consent() { /** - * ## Consent.consent + * ## Consent.consentTexts * * The object containing the variables to substitute * * Default: node.game.settings.CONSENT */ - this.consent = null; + this.consentTexts = null; + + /** + * ## Consent.agreed + * + * If TRUE, consent has been given + */ + this.agreed = null; /** * ## Consent.showPrint @@ -9260,6 +10494,84 @@ * Default: TRUE */ this.showPrint = null; + + /** + * ## Consent.showAgreeBtns + * + * If TRUE, the agree/disagree buttons are shown + * + * Default: TRUE + */ + this.showAgreeBtns = null; + + /** + * ## Consent.disconnect + * + * If TRUE, client is disconnected upon reject + * + * Default: TRUE + */ + this.disconnect = null; + + /** + * ## Consent.checkboxes + * + * Checkboxes that need to checked to consent + * + * The content of the arrays can be strings, or objects that specify + * additional properties, i.e.: + * + * ```js + * + * { + * label: 'This is the label text', + * required: false, // Default true + * className: 'myclass' // Added to outer div, default: 'form-switch' + * } + * ``` + * + * They can also be functions that either return strings or objects, + * or FALSE, if the checkbox should not be added. + * + */ + this.checkboxes = []; + + /** + * ## Consent.fineprint + * + * Additional text displayed in a small font under the checkboxes + */ + this.fineprint = null; + + /** + * ## Consent.prefix + * + * The prefix to the ids created by the widget + * + * Default: '' + */ + this.prefix = ''; + + /** + * ## Consent.consentId + * + * The id of the HTML element that contains the consent + * + * The widget will be appended here, if found. + * + * Default: `prefix` + 'consent' + */ + this.consentId = 'consent'; + + /** + * ## Consent.doneOnAgree + * + * If TRUE, `node.done` is called upon agreeing to consent form + * + * Default: TRUE + */ + this.doneOnAgree; + } // ## Consent methods. @@ -9272,61 +10584,159 @@ * @param {object} opts Optional. Configuration options. */ Consent.prototype.init = function(opts) { + var that; opts = opts || {}; - this.consent = opts.consent || node.game.settings.CONSENT; + this.consentTexts = opts.consent || node.game.settings.CONSENT; - if (this.consent && 'object' !== typeof this.consent) { - throw new TypeError('Consent: consent must be object or ' + - 'undefined. Found: ' + this.consent); + if (this.consentTexts && 'object' !== typeof this.consentTexts) { + throw new TypeError('Consent.init: consent must be object or ' + + 'undefined. Found: ' + this.consentTexts); } this.showPrint = opts.showPrint === false ? false : true; + + this.showBtns = opts.showAgreeBtns === false ? false : true; + + this.disconnect = opts.disconnect === false ? false : true; + + this.doneOnAgree = opts.doneOnAgree === false ? false : true; + + if (J.isArray(opts.checkboxes)) { + that = this; + opts.checkboxes.forEach(function(item) { + if ('function' === typeof item) { + item = item(); + if (item === false) return; + } + that.checkboxes.push(item); + }); + } + else if (opts.checkboxes) { + throw new TypeError('Consent.init: checkboxes must be array or ' + + 'undefined. Found: ' + this.checkboxes); + } + + _assignStr(this, opts, 'prefix'); + _assignStr(this, opts, 'fineprint'); + _assignStr(this, opts, 'consentId'); + + if ('undefined' === typeof opts.consentId) { + this.consentId = _addPrefix(this, this.consentId); + } }; Consent.prototype.enable = function() { - var a, na; - if (this.notAgreed) return; - a = W.gid('agree'); - if (a) a.disabled = false; - na = W.gid('notAgree'); - if (na) na.disabled = false; + if (this.agreed !== null) return; + _toggleEnable(true); }; Consent.prototype.disable = function() { - var a, na; - if (this.notAgreed) return; - a = W.gid('agree'); - if (a) a.disabled = true; - na = W.gid('notAgree'); - if (na) na.disabled = true; + _toggleEnable(false); }; Consent.prototype.append = function() { - var consent, html; + var that, consent, isRtl, html, btn1, btn2, st1, st2; + + that = this; + // Hide not agreed div. - W.hide('notAgreed'); + W.hide(_addPrefix(this, 'notAgreed')); - consent = W.gid('consent'); + consent = W.gid(this.consentId); + if (!consent) { + node.warn('Consent.append: the page does not contain an ' + + 'element with id "' + this.consentId + + '", it will use widget\'s root'); + + consent = w.bodyDiv; + } html = ''; + + // Checkboxes. + + isRtl = W.isRTL(this.bodyDiv); + + if (this.checkboxes.length || this.fineprint) { + + html += '
'; + + if (this.checkboxes.length) { + html += '
'; + this.checkboxes.forEach(function(c, idx) { + var id, label, btn, className; + id = _getCbxId(that, idx+1); + + className = 'form-check'; + if (isRtl) className += '-reverse'; + + if ('object' === typeof c) { + label = c.label; + className += ' ' + c.className; + } + else { + label = c; + } + + btn = ''; + label = ''; + + html += '
'; + html += '
'; + html += isRtl ? label + btn : btn + label; + html += '
'; + }); + html += '
'; + } + + if (this.fineprint) { + html += '

'; + html += this.fineprint; + html += '

'; + } + + html += '
'; + + } // Print. if (this.showPrint) { html = this.getText('printText'); - html += '

'; + html += '

'; } - // Header for buttons. - html += '' + this.getText('consentTerms') + '
'; + + if (this.showBtns !== false) { + // Header for buttons. + html += '' + this.getText('consentTerms') + '
'; + + // Buttons. + html += ''; + } + consent.innerHTML += html; setTimeout(function() { W.adjustFrameHeight(); }); @@ -9334,7 +10744,7 @@ Consent.prototype.listeners = function() { var that = this; - var consent = this.consent; + var consent = this.consentTexts; node.on('FRAME_LOADED', function() { var a, na, p, id; @@ -9352,14 +10762,20 @@ } // Add listeners on buttons. - a = W.gid('agree'); - na = W.gid('notAgree'); - - if (!a) throw new Error('Consent: agree button not found'); - if (!na) throw new Error('Consent: notAgree button not found'); - - - a.onclick = function() { node.done({ consent: true }); }; + if (!that.showBtns) return; + + a = W.gid(_addPrefix(this, 'agree')); + na = W.gid(_addPrefix(this, 'notAgree')); + + a.onclick = function() { + var consent; + node.emit('CONSENT_ACCEPTING'); + consent = that.getValues({ agreed: true }); + if (!consent.consent) return; + this.agreed = true; + node.emit('CONSENT_ACCEPTED', consent); + if (that.doneOnAgree) node.done(consent); + }; na.onclick = function() { var showIt, confirmed; @@ -9368,7 +10784,7 @@ node.emit('CONSENT_REJECTING'); - that.notAgreed = true; + that.agreed = false; node.set({ consent: false, // Need to send these two because it's not a DONE msg. @@ -9380,16 +10796,22 @@ a.onclick = null; na.onclick = null; - node.socket.disconnect(); - W.hide('consent'); - W.show('notAgreed'); + // Disconnect, if requested. + if (that.disconnect) { + // Destroy disconnectBox (if found) before disconnecting. + if (node.game.discBox) node.game.discBox.destroy(); + node.socket.disconnect(); + } + + W.hide(that.consentId); + W.show(_addPrefix(that, 'notAgreed')); // If a show-consent button is found enable it. - showIt = W.gid('show-consent'); + showIt = W.gid(_addPrefix(that, 'show-consent')); if (showIt) { showIt.onclick = function() { var div, s; - div = W.toggle('consent'); + div = W.toggle(that.consentId); s = div.style.display === '' ? 'hide' : 'show'; this.innerHTML = that.getText('showHideConsent', s); }; @@ -9399,11 +10821,137 @@ }); }; + /** + * ## Consent.getValues + * + * Returns the current selection on Consent + * + * @param {object} opts Configuration object. Options: + * - highlight: if TRUE, missing consents on checkboxes are highlighted. + * Default: TRUE. + * - agreed: TRUE to flag that the user has already clicked on agree + * @returns {object} consent Values of consent. + * + * ```js + * { + * consent: true, // if all consent conditions are fullfilled + * checkboxes: true // if all required checkboxes are checked + * [checkbox_ID1...IDN]: true // one property per checkbox + * } + */ + Consent.prototype.getValues = function(opts) { + var consent, that; + that = this; + consent = { consent: true }; + opts = opts || {}; + if (this.checkboxes.length) { + consent.checkboxes = true; + this.checkboxes.forEach(function(c, idx) { + var cbx, id, req; + id = _getCbxId(that, idx+1); + cbx = W.gid(id); + if (!cbx) { + node.warn('Consent: could not find checkbox ' + id); + } + else { + req = that.checkboxes[idx]; + consent[id] = cbx.checked; + + if ('string' === typeof req || + req.required !== false) { + + if (!cbx.checked) { + // At least one is needed to deny consent. + consent.checkboxes = consent.consent = false; + if (opts.highlight !== false) W.shake(cbx); + } + } + } + }); + } + if (this.agreed !== true && this.showBtns && !opts.agreed) { + consent.consent = false; + } + return consent; + }; + + // ### Helper functions + + + /** ### _toggleEnable + * + * Enables/disables inputs in the widget + * + * @param {boolean} state True or false + */ + function _toggleEnable(state) { + var elem, i; + elem = W.gid('agree'); + if (elem) elem.disabled = state; + elem = W.gid('notAgree'); + if (elem) elem.disabled = state; + if (this.checkboxes && this.checkboxes.length) { + for (i = 0; i < this.checkboxes.length; i++) { + elem = W.gid(_getCbxId(i+1)); + if (elem) elem.disabled = state; + } + } + } + + /** + * ### _addPrefix + * + * Adds a the widget prefix to a string, if one is set. + * + * @param {object} w This widget + * @param {string} str The string to manipulate + * + * @returns {string} The id of the checkbox at a given index + */ + function _addPrefix(w, str) { + return (w.prefix ? (w.prefix + '_') : '') + str; + } + + /** + * ### _getCbxId + * + * Returns a standardized id for a chekbox based on its index. + * + * @param {object} w This widget + * @param {number} idx The id of the checkbox + * + * @returns {string} The id of the checkbox at a given index + */ + function _getCbxId(w, idx) { + return _addPrefix(w, 'consent_checkbox_' + idx); + } + + /** + * ### _assignStr + * + * Checks the value of a field in an object, if string it stores it + * + * @param {object} w This widget + * @param {object} opts The configuration options with the field to check + * @param {string} id The id to assign + */ + function _assignStr(w, opts, id) { + var str; + str = opts[id]; + if ('string' === typeof str) { + w[id] = str; + } + else if (str) { + throw new TypeError('Consent.init: ' + id + 'Id must be ' + + 'string or undefined. Found: ' + str); + } + } + })(node); /** * # ContentBox - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays some content. @@ -9421,14 +10969,9 @@ ContentBox.version = '0.2.0'; ContentBox.description = 'Simply displays some content'; - ContentBox.title = false; ContentBox.panel = false; ContentBox.className = 'contentbox'; - // ## Dependencies - - ContentBox.dependencies = {}; - /** * ## ContentBox constructor * @@ -9507,7 +11050,7 @@ /** * # Controls - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates and manipulates a set of forms @@ -9527,7 +11070,6 @@ Controls.version = '0.5.1'; Controls.description = 'Wraps a collection of user-inputs controls.'; - Controls.title = 'Controls'; Controls.className = 'controls'; /** @@ -9584,7 +11126,7 @@ } Controls.prototype.add = function(root, id, attributes) { - // TODO: replace W.addTextInput + // TODO: replace W.addTextInput //return W.addTextInput(root, id, attributes); }; @@ -9700,7 +11242,7 @@ }; } - if (attributes.label) { + if (attributes.label) { W.add('label', container, { 'for': elem.id, innerHTML: attributes.label @@ -9935,7 +11477,7 @@ for (key in this.features) { if (this.features.hasOwnProperty(key)) { el = W.getElementById(key); - if (el.checked) return el.value; + if (el.checked) return el.value; } } return false; @@ -9945,7 +11487,7 @@ /** * # CustomInput - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a configurable input form with validation @@ -9963,7 +11505,6 @@ CustomInput.version = '0.12.0'; CustomInput.description = 'Creates a configurable input form'; - CustomInput.title = false; CustomInput.panel = false; CustomInput.className = 'custominput'; @@ -10125,7 +11666,8 @@ res = '(Must be before ' + w.params.max + ')'; } } - return w.required ? ((res || '') + ' *') : (res || false); + return w.required && w.displayRequired ? + ((res || '') + ' ' + w.requiredMark) : (res || false); }, numericErr: function(w) { var str, p; @@ -10185,12 +11727,6 @@ emptyErr: 'Cannot be empty' }; - // ## Dependencies - - CustomInput.dependencies = { - JSUS: {} - }; - /** * ## CustomInput constructor * @@ -10413,7 +11949,7 @@ * @param {object} opts Configuration options */ CustomInput.prototype.init = function(opts) { - var tmp, that, e, isText, setValues; + var tmp, val, that, e, isText, setValues; that = this; e = 'CustomInput.init: '; @@ -10480,102 +12016,102 @@ 'or undefined. Found: ' + opts.validation); } - tmp = opts.validation; + val = opts.validation; } - else { - // Add default validations based on type. + // Add default validations based on type. - if (this.type === 'number' || this.type === 'float' || - this.type === 'int' || this.type === 'text') { + if (this.type === 'number' || this.type === 'float' || + this.type === 'int' || this.type === 'text') { - isText = this.type === 'text'; + isText = this.type === 'text'; - // Greater than. - if ('undefined' !== typeof opts.min) { - tmp = J.isNumber(opts.min); - if (false === tmp) { - throw new TypeError(e + 'min must be number or ' + - 'undefined. Found: ' + opts.min); - } - this.params.lower = opts.min; - this.params.leq = true; + // Greater than. + if ('undefined' !== typeof opts.min) { + tmp = J.isNumber(opts.min); + if (false === tmp) { + throw new TypeError(e + 'min must be number or ' + + 'undefined. Found: ' + opts.min); } - // Less than. - if ('undefined' !== typeof opts.max) { - tmp = J.isNumber(opts.max); - if (false === tmp) { - throw new TypeError(e + 'max must be number or ' + - 'undefined. Found: ' + opts.max); - } - this.params.upper = opts.max; - this.params.ueq = true; + this.params.lower = opts.min; + this.params.leq = true; + } + // Less than. + if ('undefined' !== typeof opts.max) { + tmp = J.isNumber(opts.max); + if (false === tmp) { + throw new TypeError(e + 'max must be number or ' + + 'undefined. Found: ' + opts.max); } + this.params.upper = opts.max; + this.params.ueq = true; + } - if (opts.strictlyGreater) this.params.leq = false; - if (opts.strictlyLess) this.params.ueq = false; + if (opts.strictlyGreater) this.params.leq = false; + if (opts.strictlyLess) this.params.ueq = false; - // Checks on both min and max. - if ('undefined' !== typeof this.params.lower && - 'undefined' !== typeof this.params.upper) { + // Checks on both min and max. + if ('undefined' !== typeof this.params.lower && + 'undefined' !== typeof this.params.upper) { - if (this.params.lower > this.params.upper) { - throw new TypeError(e + 'min cannot be greater ' + - 'than max. Found: ' + - opts.min + '> ' + opts.max); + if (this.params.lower > this.params.upper) { + throw new TypeError(e + 'min cannot be greater ' + + 'than max. Found: ' + + opts.min + '> ' + opts.max); + } + // Exact length. + if (this.params.lower === this.params.upper) { + if (!this.params.leq || !this.params.ueq) { + + throw new TypeError(e + 'min cannot be equal to ' + + 'max when strictlyGreater or ' + + 'strictlyLess are set. ' + + 'Found: ' + opts.min); } - // Exact length. - if (this.params.lower === this.params.upper) { - if (!this.params.leq || !this.params.ueq) { - - throw new TypeError(e + 'min cannot be equal to ' + - 'max when strictlyGreater or ' + - 'strictlyLess are set. ' + - 'Found: ' + opts.min); - } - if (this.type === 'int' || this.type === 'text') { - if (J.isFloat(this.params.lower)) { + if (this.type === 'int' || this.type === 'text') { + if (J.isFloat(this.params.lower)) { - throw new TypeError(e + 'min cannot be a ' + - 'floating point number ' + - 'and equal to ' + - 'max, when type ' + - 'is not "float". Found: ' + - opts.min); - } + throw new TypeError(e + 'min cannot be a ' + + 'floating point number ' + + 'and equal to ' + + 'max, when type ' + + 'is not "float". Found: ' + + opts.min); } - // Store this to create better error strings. - this.params.exactly = true; - } - else { - // Store this to create better error strings. - this.params.between = true; } + // Store this to create better error strings. + this.params.exactly = true; + } + else { + // Store this to create better error strings. + this.params.between = true; } + } - // Checks for text only. - if (isText) { + // Checks for text only. + if (isText) { - this.params.noNumbers = opts.noNumbers; + this.params.noNumbers = opts.noNumbers; - if ('undefined' !== typeof this.params.lower) { - if (this.params.lower < 0) { - throw new TypeError(e + 'min cannot be negative ' + - 'when type is "text". Found: ' + - this.params.lower); - } - if (!this.params.leq) this.params.lower++; + if ('undefined' !== typeof this.params.lower) { + if (this.params.lower < 0) { + throw new TypeError(e + 'min cannot be negative ' + + 'when type is "text". Found: ' + + this.params.lower); } - if ('undefined' !== typeof this.params.upper) { - if (this.params.upper < 0) { - throw new TypeError(e + 'max cannot be negative ' + - 'when type is "text". Found: ' + - this.params.upper); - } - if (!this.params.ueq) this.params.upper--; + if (!this.params.leq) this.params.lower++; + } + if ('undefined' !== typeof this.params.upper) { + if (this.params.upper < 0) { + throw new TypeError(e + 'max cannot be negative ' + + 'when type is "text". Found: ' + + this.params.upper); } + if (!this.params.ueq) this.params.upper--; + } - tmp = function(value) { + if (!val) { + val = function(value) { var len, p, out, err; p = that.params; len = value.length; @@ -10589,9 +12125,9 @@ } else { if (('undefined' !== typeof p.lower && - len < p.lower) || - ('undefined' !== typeof p.upper && - len > p.upper)) { + len < p.lower) || + ('undefined' !== typeof p.upper && + len > p.upper)) { err = true; } @@ -10601,18 +12137,20 @@ if (err) out.err = err; return out; }; - - setValues = function() { - var a, b; - a = 'undefined' !== typeof that.params.lower ? - (that.params.lower + 1) : 5; - b = 'undefined' !== typeof that.params.upper ? - that.params.upper : (a + 5); - return J.randomString(J.randomInt(a, b)); - }; } - else { - tmp = (function() { + + setValues = function() { + var a, b; + a = 'undefined' !== typeof that.params.lower ? + (that.params.lower + 1) : 5; + b = 'undefined' !== typeof that.params.upper ? + that.params.upper : (a + 5); + return J.randomString(J.randomInt(a, b)); + }; + } + else { + if (!val) { + val = (function() { var cb; if (that.type === 'float') cb = J.isFloat; else if (that.type === 'int') cb = J.isInt; @@ -10628,95 +12166,97 @@ }; }; })(); - - setValues = function() { - var p, a, b; - p = that.params; - if (that.type === 'float') return J.random(); - a = 0; - if ('undefined' !== typeof p.lower) { - a = p.leq ? (p.lower - 1) : p.lower; - } - if ('undefined' !== typeof p.upper) { - b = p.ueq ? p.upper : (p.upper - 1); - } - else { - b = 100 + a; - } - return J.randomInt(a, b); - }; } - // Preset inputWidth. - if (this.params.upper) { - if (this.params.upper < 10) this.inputWidth = '100px'; - else if (this.params.upper < 20) this.inputWidth = '200px'; - } + setValues = function() { + var p, a, b; + p = that.params; + if (that.type === 'float') return J.random(); + a = 0; + if ('undefined' !== typeof p.lower) { + a = p.leq ? (p.lower - 1) : p.lower; + } + if ('undefined' !== typeof p.upper) { + b = p.ueq ? p.upper : (p.upper - 1); + } + else { + b = 100 + a; + } + return J.randomInt(a, b); + }; + } + // Preset inputWidth. + if (this.params.upper) { + if (this.params.upper < 10) this.inputWidth = '100px'; + else if (this.params.upper < 20) this.inputWidth = '200px'; } - else if (this.type === 'date') { - if ('undefined' !== typeof opts.format) { - // TODO: use regex. - if (opts.format !== 'mm-dd-yy' && - opts.format !== 'dd-mm-yy' && - opts.format !== 'mm-dd-yyyy' && - opts.format !== 'dd-mm-yyyy' && - opts.format !== 'mm.dd.yy' && - opts.format !== 'dd.mm.yy' && - opts.format !== 'mm.dd.yyyy' && - opts.format !== 'dd.mm.yyyy' && - opts.format !== 'mm/dd/yy' && - opts.format !== 'dd/mm/yy' && - opts.format !== 'mm/dd/yyyy' && - opts.format !== 'dd/mm/yyyy') { - - throw new Error(e + 'date format is invalid. Found: ' + - opts.format); - } - this.params.format = opts.format; + + } + else if (this.type === 'date') { + if ('undefined' !== typeof opts.format) { + // TODO: use regex. + if (opts.format !== 'mm-dd-yy' && + opts.format !== 'dd-mm-yy' && + opts.format !== 'mm-dd-yyyy' && + opts.format !== 'dd-mm-yyyy' && + opts.format !== 'mm.dd.yy' && + opts.format !== 'dd.mm.yy' && + opts.format !== 'mm.dd.yyyy' && + opts.format !== 'dd.mm.yyyy' && + opts.format !== 'mm/dd/yy' && + opts.format !== 'dd/mm/yy' && + opts.format !== 'mm/dd/yyyy' && + opts.format !== 'dd/mm/yyyy') { + + throw new Error(e + 'date format is invalid. Found: ' + + opts.format); } - else { - this.params.format = 'mm/dd/yyyy'; + this.params.format = opts.format; + } + else { + this.params.format = 'mm/dd/yyyy'; + } + + this.params.sep = this.params.format.charAt(2); + tmp = this.params.format.split(this.params.sep); + this.params.yearDigits = tmp[2].length; + this.params.dayPos = tmp[0].charAt(0) === 'd' ? 0 : 1; + this.params.monthPos = this.params.dayPos ? 0 : 1; + this.params.dateLen = tmp[2].length + 6; + if (opts.minDate) { + tmp = getParsedDate(opts.minDate, this.params); + if (!tmp) { + throw new Error(e + 'minDate must be a Date object. ' + + 'Found: ' + opts.minDate); } - - this.params.sep = this.params.format.charAt(2); - tmp = this.params.format.split(this.params.sep); - this.params.yearDigits = tmp[2].length; - this.params.dayPos = tmp[0].charAt(0) === 'd' ? 0 : 1; - this.params.monthPos = this.params.dayPos ? 0 : 1; - this.params.dateLen = tmp[2].length + 6; - if (opts.minDate) { - tmp = getParsedDate(opts.minDate, this.params); - if (!tmp) { - throw new Error(e + 'minDate must be a Date object. ' + - 'Found: ' + opts.minDate); - } - this.params.minDate = tmp; + this.params.minDate = tmp; + } + if (opts.maxDate) { + tmp = getParsedDate(opts.maxDate, this.params); + if (!tmp) { + throw new Error(e + 'maxDate must be a Date object. ' + + 'Found: ' + opts.maxDate); } - if (opts.maxDate) { - tmp = getParsedDate(opts.maxDate, this.params); - if (!tmp) { - throw new Error(e + 'maxDate must be a Date object. ' + - 'Found: ' + opts.maxDate); - } - if (this.params.minDate && - this.params.minDate.obj > tmp.obj) { + if (this.params.minDate && + this.params.minDate.obj > tmp.obj) { - throw new Error(e + 'maxDate cannot be prior to ' + - 'minDate. Found: ' + tmp.str + - ' < ' + this.params.minDate.str); - } - this.params.maxDate = tmp; + throw new Error(e + 'maxDate cannot be prior to ' + + 'minDate. Found: ' + tmp.str + + ' < ' + this.params.minDate.str); } + this.params.maxDate = tmp; + } - // Preset inputWidth. - if (this.params.yearDigits === 2) this.inputWidth = '100px'; - else this.inputWidth = '150px'; + // Preset inputWidth. + if (this.params.yearDigits === 2) this.inputWidth = '100px'; + else this.inputWidth = '150px'; - // Preset placeholder. - this.placeholder = this.params.format; + // Preset placeholder. + this.placeholder = this.params.format; - tmp = function(value) { + if (!val) { + val = function(value) { var p, tokens, tmp, res, dayNum, l1, l2; p = that.params; @@ -10763,7 +12303,7 @@ else { // Is it leap year? dayNum = (res.year % 4 === 0 && res.year % 100 !== 0) || - res.year % 400 === 0 ? 29 : 28; + res.year % 400 === 0 ? 29 : 28; } res.month = tmp; // Day. @@ -10789,51 +12329,53 @@ } return res; }; + } - setValues = function() { - var p, minD, maxD, d, day, month, year; - p = that.params; - minD = p.minDate ? p.minDate.obj : new Date('01/01/1900'); - maxD = p.maxDate ? p.maxDate.obj : undefined; - d = J.randomDate(minD, maxD); - day = d.getDate(); - month = (d.getMonth() + 1); - year = d.getFullYear(); - if (p.yearDigits === 2) year = ('' + year).substr(2); - if (p.monthPos === 0) d = month + p.sep + day; - else d = day + p.sep + month; - d += p.sep + year; - return d; - }; + setValues = function() { + var p, minD, maxD, d, day, month, year; + p = that.params; + minD = p.minDate ? p.minDate.obj : new Date('01/01/1900'); + maxD = p.maxDate ? p.maxDate.obj : undefined; + d = J.randomDate(minD, maxD); + day = d.getDate(); + month = (d.getMonth() + 1); + year = d.getFullYear(); + if (p.yearDigits === 2) year = ('' + year).substr(2); + if (p.monthPos === 0) d = month + p.sep + day; + else d = day + p.sep + month; + d += p.sep + year; + return d; + }; + } + else if (this.type === 'us_state') { + if (opts.abbreviation) { + this.params.abbr = true; + this.inputWidth = '100px'; + } + else { + this.inputWidth = '200px'; } - else if (this.type === 'us_state') { - if (opts.abbreviation) { - this.params.abbr = true; - this.inputWidth = '100px'; + if (opts.territories !== false) { + this.terr = true; + if (this.params.abbr) { + tmp = getUsStatesList('usStatesTerrByAbbrLow'); } else { - this.inputWidth = '200px'; + tmp = getUsStatesList('usStatesTerrLow'); } - if (opts.territories !== false) { - this.terr = true; - if (this.params.abbr) { - tmp = getUsStatesList('usStatesTerrByAbbrLow'); - } - else { - tmp = getUsStatesList('usStatesTerrLow'); - } + } + else { + if (this.params.abbr) { + tmp = getUsStatesList('usStatesByAbbrLow'); } else { - if (this.params.abbr) { - tmp = getUsStatesList('usStatesByAbbrLow'); - } - else { - tmp = getUsStatesList('usStatesLow'); - } + tmp = getUsStatesList('usStatesLow'); } - this.params.usStateVal = tmp; + } + this.params.usStateVal = tmp; - tmp = function(value) { + if (!val) { + val = function(value) { var res; res = { value: value }; if (!that.params.usStateVal[value.toLowerCase()]) { @@ -10841,14 +12383,16 @@ } return res; }; + } - setValues = function() { - return J.randomKey(that.params.usStateVal); - }; + setValues = function() { + return J.randomKey(that.params.usStateVal); + }; - } - else if (this.type === 'us_zip') { - tmp = function(value) { + } + else if (this.type === 'us_zip') { + if (val) { + val = function(value) { var res; res = { value: value }; if (!isValidUSZip(value)) { @@ -10856,83 +12400,85 @@ } return res; }; - - setValues = function() { - return Math.floor(Math.random()*90000) + 10000; - }; } - // Lists. + setValues = function() { + return Math.floor(Math.random()*90000) + 10000; + }; + } + + // Lists. - else if (this.type === 'list' || - this.type === 'us_city_state_zip') { + else if (this.type === 'list' || + this.type === 'us_city_state_zip') { - if (opts.listSeparator) { - if ('string' !== typeof opts.listSeparator) { - throw new TypeError(e + 'listSeparator must be ' + - 'string or undefined. Found: ' + - opts.listSeperator); - } - this.params.listSep = opts.listSeparator; - } - else { - this.params.listSep = ','; + if (opts.listSeparator) { + if ('string' !== typeof opts.listSeparator) { + throw new TypeError(e + 'listSeparator must be ' + + 'string or undefined. Found: ' + + opts.listSeperator); } + this.params.listSep = opts.listSeparator; + } + else { + this.params.listSep = ','; + } - if (this.type === 'us_city_state_zip') { - - getUsStatesList('usStatesTerrByAbbr'); - this.params.minItems = this.params.maxItems = 3; - this.params.fixedSize = true; - this.params.itemValidation = function(item, idx) { - if (idx === 2) { - if (!usStatesTerrByAbbr[item.toUpperCase()]) { - return { err: that.getText('usStateAbbrErr') }; - } - } - else if (idx === 3) { - if (!isValidUSZip(item)) { - return { err: that.getText('usZipErr') }; - } - } - }; + if (this.type === 'us_city_state_zip') { - this.placeholder = 'Town' + this.params.listSep + - ' State' + this.params.listSep + ' ZIP'; - } - else { - if ('undefined' !== typeof opts.minItems) { - tmp = J.isInt(opts.minItems, 0); - if (tmp === false) { - throw new TypeError(e + 'minItems must be ' + - 'a positive integer. Found: ' + - opts.minItems); + getUsStatesList('usStatesTerrByAbbr'); + this.params.minItems = this.params.maxItems = 3; + this.params.fixedSize = true; + this.params.itemValidation = function(item, idx) { + if (idx === 2) { + if (!usStatesTerrByAbbr[item.toUpperCase()]) { + return { err: that.getText('usStateAbbrErr') }; } - this.params.minItems = tmp; } - else if (this.required) { - this.params.minItems = 1; - } - if ('undefined' !== typeof opts.maxItems) { - tmp = J.isInt(opts.maxItems, 0); - if (tmp === false) { - throw new TypeError(e + 'maxItems must be ' + - 'a positive integer. Found: ' + - opts.maxItems); + else if (idx === 3) { + if (!isValidUSZip(item)) { + return { err: that.getText('usZipErr') }; } - if (this.params.minItems && - this.params.minItems > tmp) { + } + }; - throw new TypeError(e + 'maxItems must be larger ' + - 'than minItems. Found: ' + - tmp + ' < ' + - this.params.minItems); - } - this.params.maxItems = tmp; + this.placeholder = 'Town' + this.params.listSep + + ' State' + this.params.listSep + ' ZIP'; + } + else { + if ('undefined' !== typeof opts.minItems) { + tmp = J.isInt(opts.minItems, 0); + if (tmp === false) { + throw new TypeError(e + 'minItems must be ' + + 'a positive integer. Found: ' + + opts.minItems); } + this.params.minItems = tmp; } + else if (this.required) { + this.params.minItems = 1; + } + if ('undefined' !== typeof opts.maxItems) { + tmp = J.isInt(opts.maxItems, 0); + if (tmp === false) { + throw new TypeError(e + 'maxItems must be ' + + 'a positive integer. Found: ' + + opts.maxItems); + } + if (this.params.minItems && + this.params.minItems > tmp) { + + throw new TypeError(e + 'maxItems must be larger ' + + 'than minItems. Found: ' + + tmp + ' < ' + + this.params.minItems); + } + this.params.maxItems = tmp; + } + } - tmp = function(value) { + if (!val) { + val = function(value) { var i, len, v, iVal, err; value = value.split(that.params.listSep); len = value.length; @@ -10984,42 +12530,41 @@ } return { value: value }; }; + } - if (this.type === 'us_city_state_zip') { - setValues = function() { - var sep; - sep = that.params.listSep + ' '; - return J.randomString(8) + sep + - J.randomKey(usStatesTerrByAbbr) + sep + - (Math.floor(Math.random()*90000) + 10000); - }; - } - else { - setValues = function(opts) { - var p, minItems, nItems, i, str, sample; - p = that.params; - minItems = p.minItems || 0; - if (opts.availableValues) { - nItems = J.randomInt(minItems, - opts.availableValues.length); - nItems--; - sample = J.sample(0, (nItems-1)); - } - else { - nItems = J.randomInt(minItems, - p.maxItems || (minItems + 5)); - nItems--; - } - str = ''; - for (i = 0; i < nItems; i++) { - if (i !== 0) str += p.listSep + ' '; - if (sample) str += opts.availableValues[sample[i]]; - else str += J.randomString(J.randomInt(3,10)); - } - return str; - }; - } - + if (this.type === 'us_city_state_zip') { + setValues = function() { + var sep; + sep = that.params.listSep + ' '; + return J.randomString(8) + sep + + J.randomKey(usStatesTerrByAbbr) + sep + + (Math.floor(Math.random()*90000) + 10000); + }; + } + else { + setValues = function(opts) { + var p, minItems, nItems, i, str, sample; + p = that.params; + minItems = p.minItems || 0; + if (opts.availableValues) { + nItems = J.randomInt(minItems, + opts.availableValues.length); + nItems--; + sample = J.sample(0, (nItems-1)); + } + else { + nItems = J.randomInt(minItems, + p.maxItems || (minItems + 5)); + nItems--; + } + str = ''; + for (i = 0; i < nItems; i++) { + if (i !== 0) str += p.listSep + ' '; + if (sample) str += opts.availableValues[sample[i]]; + else str += J.randomString(J.randomInt(3,10)); + } + return str; + }; } // US_Town,State, Zip Code @@ -11036,10 +12581,10 @@ if (value.trim() === '') { if (that.required) res.err = that.getText('emptyErr'); } - else if (tmp) { - res = tmp(value); + else if (val) { + res = val.call(this, value); } - if (that.userValidation) that.userValidation(res); + if (that.userValidation) that.userValidation.call(this, res); return res; }; @@ -11153,7 +12698,9 @@ 'undefined. Found: ' + opts.hint); } this.hint = opts.hint; - if (this.required) this.hint += ' *'; + if (this.required && this.displayRequired) { + this.hint += ' ' + this.requiredMark; + } } else { this.hint = this.getText('autoHint'); @@ -11389,7 +12936,6 @@ * * @return {mixed} The value in the input * - * @see CustomInput.verifyChoice * @see CustomInput.reset */ CustomInput.prototype.getValues = function(opts) { @@ -11603,7 +13149,7 @@ /** * # CustomInputGroup - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a table that groups together several custom input widgets @@ -11624,23 +13170,16 @@ CustomInputGroup.description = 'Groups together and manages sets of ' + 'CustomInput widgets.'; - CustomInputGroup.title = false; CustomInputGroup.className = 'custominput custominputgroup'; CustomInputGroup.separator = '::'; CustomInputGroup.texts.autoHint = function(w) { - if (w.requiredChoice) return '*'; + if (w.requiredChoice && w.displayRequired) return w.requiredMark; else return false; }; CustomInputGroup.texts.inputErr = 'One or more errors detected.'; - // ## Dependencies - - CustomInputGroup.dependencies = { - JSUS: {} - }; - /** * ## CustomInputGroup constructor * @@ -11935,6 +13474,8 @@ * - res: the validation result of the single input * - input: the custom input that fired oninput * - widget: a reference to this widget + * + * @see addCustomInput */ this.oninput = null; @@ -12076,7 +13617,7 @@ opts.validation); } - // Set the validation function. + // Set the oninput function. if ('function' === typeof opts.oninput) { this._oninput = opts.oninput; @@ -12103,7 +13644,9 @@ // Set the hint, if any. if ('string' === typeof opts.hint) { this.hint = opts.hint; - if (this.requiredChoice) this.hint += ' *'; + if (this.requiredChoice && this.displayRequired) { + this.hint += ' ' + this.requiredMark; + } } else if ('undefined' !== typeof opts.hint) { throw new TypeError('CustomInputGroup.init: hint must ' + @@ -12657,6 +14200,14 @@ if ('undefined' === typeof s.requiredChoice && that.requiredChoice) { s.requiredChoice = that.requiredChoice; } + + if ('undefined' === typeof s.displayRequired) { + s.displayRequired = that.displayRequired; + } + + if ('undefined' === typeof s.requiredMark) { + s.requiredMark = that.requiredMark; + } if ('undefined' === typeof s.timeFrom) s.timeFrom = that.timeFrom; @@ -12736,7 +14287,7 @@ id: that.id + '_summary', storeRef: false, title: false, - panel: false, + // panel: false, className: 'custominputgroup-summary', disabled: true }, that.sharedOptions); @@ -12810,8 +14361,7 @@ // ## Dependencies D3.dependencies = { - d3: {}, - JSUS: {} + d3: {} }; function D3 (options) { @@ -13178,12 +14728,6 @@ DebugWall.title = 'Debug Wall'; DebugWall.className = 'debugwall'; - // ## Dependencies - - DebugWall.dependencies = { - JSUS: {} - }; - /** * ## DebugWall constructor * @@ -13526,7 +15070,7 @@ /** * # DisconnectBox - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Shows a disconnect button @@ -13544,7 +15088,6 @@ DisconnectBox.version = '0.4.0'; DisconnectBox.description = 'Monitors and handles disconnections'; - DisconnectBox.title = false; DisconnectBox.panel = false; DisconnectBox.className = 'disconnectbox'; @@ -13687,7 +15230,7 @@ /** * # DoneButton - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a button that if pressed emits node.done() @@ -13706,16 +15249,10 @@ DoneButton.description = 'Creates a button that if ' + 'pressed emits node.done().'; - DoneButton.title = false; + DoneButton.panel = false; DoneButton.className = 'donebutton'; DoneButton.texts.done = 'Done'; - // ## Dependencies - - DoneButton.dependencies = { - JSUS: {} - }; - /** * ## DoneButton constructor * @@ -13740,8 +15277,8 @@ this.button = options.button; } else if ('undefined' === typeof options.button) { - this.button = document.createElement('input'); - this.button.type = 'button'; + this.button = document.createElement('button'); + // this.button.type = 'button'; } else { throw new TypeError('DoneButton constructor: options.button must ' + @@ -13751,6 +15288,10 @@ this.button.onclick = function() { if (that.onclick && false === that.onclick()) return; + if (node.game.isWidgetStep()) { + // Widget has a next visualization in the same step. + if (node.widgets.last.next() !== false) return; + } if (node.done()) that.disable(); }; @@ -13823,28 +15364,30 @@ if (tmp) this.button.id = tmp; // Button className. - if ('undefined' === typeof opts.className) { + if ('undefined' === typeof opts.classNameBtn) { tmp = 'btn btn-lg btn-primary'; } - else if (opts.className === false) { + else if (opts.classNameBtn === false) { tmp = ''; } - else if ('string' === typeof opts.className) { - tmp = opts.className; + else if ('string' === typeof opts.classNameBtn) { + tmp = opts.classNameBtn; } - else if (J.isArray(opts.className)) { - tmp = opts.className.join(' '); + else if (J.isArray(opts.classNameBtn)) { + tmp = opts.classNameBtn.join(' '); } else { - throw new TypeError('DoneButton.init: className must ' + + throw new TypeError('DoneButton.init: classNameBtn must ' + 'be string, array, or undefined. Found: ' + - opts.className); + opts.classNameBtn); } this.button.className = tmp; // Button text. - this.button.value = 'string' === typeof opts.text ? - opts.text : this.getText('done'); + // this.button.value = 'string' === typeof opts.text ? + // opts.text : this.getText('done'); + this.button.innerHTML = 'string' === typeof opts.text ? + opts.text : this.getText('done'); this.disableOnDisconnect = 'undefined' === typeof opts.disableOnDisconnect ? @@ -13859,14 +15402,7 @@ 'be number or undefined. Found: ' + tmp); } - tmp = opts.onclick; - if (tmp) { - if ('function' !== typeof tmp) { - throw new TypeError('DoneButton.init: onclick must function ' + - 'or undefined. Found: ' + tmp); - } - this.onclick = tmp; - } + setOnClick(this, opts.onclick); }; DoneButton.prototype.append = function() { @@ -13889,10 +15425,9 @@ // then unlocked by GameWindow, but otherwise it must be // done here. node.on('PLAYING', function() { - var prop, step, delay; + var prop, delay; - step = node.game.getCurrentGameStage(); - prop = node.game.plot.getProperty(step, 'donebutton'); + prop = node.game.getProperty('donebutton'); if (prop === false || (prop && prop.enableOnPlaying === false)) { // It might be disabled already, but we do it again. that.disable(); @@ -13917,8 +15452,17 @@ that.enable(); } } - if ('string' === typeof prop) that.button.value = prop; - else if (prop && prop.text) that.button.value = prop.text; + if ('string' === typeof prop) { + // that.button.value = prop; + that.button.innerHTML = prop; + } + else if (prop) { + // if (prop.text) that.button.value = prop.text; + if (prop.text) that.button.innerHTML = prop.text; + if (prop.onclick) setOnClick(that, prop.onclick, true); + } + + }); if (this.disableOnDisconnect) { @@ -13951,12 +15495,15 @@ var oldText, that; if (duration) { that = this; - oldText = this.button.value; + // oldText = this.button.value; + oldText = this.button.innerHTML; node.timer.setTimeout(function() { - that.button.value = oldText; + // that.button.value = oldText; + that.button.innerHTML = oldText; }, duration); } - this.button.value = text; + // this.button.value = text; + this.button.innerHTML = text; }; /** @@ -13983,18 +15530,50 @@ this.emit('enabled', opts); }; + + // ## Helper functions. + + // Checks and sets the onclick function. + function setOnClick(that, onclick, step) { + var str; + if ('undefined' !== typeof onclick) { + if ('function' !== typeof onclick && onclick !== null) { + str = 'DoneButton.init'; + if (step) str += ' (step property)'; + throw new TypeError(str + ': onclick must be function, null,' + + ' or undefined. Found: ' + onclick); + } + that.onclick = onclick; + } + if (step) { + node.once('REALLY_DONE', function() { + that.onclick = null; + }); + } + } + })(node); +/** + * # DropDown + * Copyright(c) 2023 Stefano Balietti + * MIT Licensed + * + * Creates a customizable dropdown menu + * + * www.nodegame.org + */ (function(node) { node.widgets.register('Dropdown', Dropdown); // Meta-data. - Dropdown.version = '0.1.0'; + Dropdown.version = '0.4.0'; Dropdown.description = 'Creates a configurable dropdown menu.'; Dropdown.texts = { + // Texts here (more info on this later). error: function (w, value) { if (value !== null && w.fixedChoice && @@ -14012,8 +15591,6 @@ } }; - // Title is displayed in the header. - Dropdown.title = false; // Classname is added to the widgets. Dropdown.className = 'dropdown'; @@ -14035,6 +15612,13 @@ */ this.mainText = null; + /** + * ### Dropdown.hint + * + * An additional text with information in lighter font + */ + this.hint = null; + /** * ### Dropdown.labelText * @@ -14043,11 +15627,11 @@ this.labelText = null; /** - * ### Dropdown.placeHolder + * ### Dropdown.placeholder * - * A placeHolder text for the input + * A placeholder text for the input */ - this.placeHolder = null; + this.placeholder = null; /** * ### Dropdown.choices @@ -14066,10 +15650,17 @@ /** * ### Dropdown.menu * - * Holder of the HTML element (datalist or select) + * Holder of the selected value (input or select) */ this.menu = null; + /** + * ### Dropdown.datalist + * + * Holder of the options for the datalist element + */ + this.datalist = null; + /** * ### Dropdown.listener * @@ -14114,9 +15705,7 @@ // Call onchange, if any. if (that.onchange) { - - - that.onchange(that.currentChoice, that); + that.onchange(that.currentChoice, menu, that); } }; @@ -14246,253 +15835,309 @@ } - Dropdown.prototype.init = function (options) { + Dropdown.prototype.init = function (opts) { // Init widget variables, but do not create // HTML elements, they should be created in append. var tmp; if (!this.id) { - throw new TypeError('Dropdown.init: options.id is missing'); + throw new TypeError('Dropdown.init: id is missing'); } - if ('string' === typeof options.mainText) { - this.mainText = options.mainText; + if ('string' === typeof opts.mainText) { + this.mainText = opts.mainText; } - else if ('undefined' !== typeof options.mainText) { - throw new TypeError('Dropdown.init: options.mainText must ' + + else if ('undefined' !== typeof opts.mainText) { + throw new TypeError('Dropdown.init: mainText must ' + 'be string or undefined. Found: ' + - options.mainText); + opts.mainText); } // Set the labelText, if any. - if ('string' === typeof options.labelText) { - this.labelText = options.labelText; + if ('string' === typeof opts.labelText) { + this.labelText = opts.labelText; } - else if ('undefined' !== typeof options.labelText) { - throw new TypeError('Dropdown.init: options.labelText must ' + + else if ('undefined' !== typeof opts.labelText) { + throw new TypeError('Dropdown.init: labelText must ' + 'be string or undefined. Found: ' + - options.labelText); + opts.labelText); } - // Set the placeHolder text, if any. - if ('string' === typeof options.placeHolder) { - this.placeHolder = options.placeHolder; + // Set the placeholder text, if any. + if ('string' === typeof opts.placeholder) { + this.placeholder = opts.placeholder; } - else if ('undefined' !== typeof options.placeHolder) { - throw new TypeError('Dropdown.init: options.placeHolder must ' + + else if ('undefined' !== typeof opts.placeholder) { + throw new TypeError('Dropdown.init: placeholder must ' + 'be string or undefined. Found: ' + - options.placeHolder); + opts.placeholder); } // Add the choices. - if ('undefined' !== typeof options.choices) { - this.choices = options.choices; + if ('undefined' !== typeof opts.choices) { + this.choices = opts.choices; } // Option requiredChoice, if any. - if ('boolean' === typeof options.requiredChoice) { - this.requiredChoice = options.requiredChoice; + if ('boolean' === typeof opts.requiredChoice) { + this.requiredChoice = opts.requiredChoice; } - else if ('undefined' !== typeof options.requiredChoice) { - throw new TypeError('Dropdown.init: options.requiredChoice ' + + else if ('undefined' !== typeof opts.requiredChoice) { + throw new TypeError('Dropdown.init: requiredChoice ' + 'be boolean or undefined. Found: ' + - options.requiredChoice); + opts.requiredChoice); } // Add the correct choices. - if ('undefined' !== typeof options.correctChoice) { + if ('undefined' !== typeof opts.correctChoice) { if (this.requiredChoice) { throw new Error('Dropdown.init: cannot specify both ' + - 'options requiredChoice and correctChoice'); + 'opts requiredChoice and correctChoice'); } - if (J.isArray(options.correctChoice) && - options.correctChoice.length > options.choices.length) { - throw new Error('Dropdown.init: options.correctChoice ' + - 'length cannot exceed options.choices length'); + if (J.isArray(opts.correctChoice) && + opts.correctChoice.length > opts.choices.length) { + throw new Error('Dropdown.init: correctChoice ' + + 'length cannot exceed opts.choices length'); } else { - this.correctChoice = options.correctChoice; + this.correctChoice = opts.correctChoice; } } // Option fixedChoice, if any. - if ('boolean' === typeof options.fixedChoice) { - this.fixedChoice = options.fixedChoice; + if ('boolean' === typeof opts.fixedChoice) { + this.fixedChoice = opts.fixedChoice; } - else if ('undefined' !== typeof options.fixedChoice) { - throw new TypeError('Dropdown.init: options.fixedChoice ' + + else if ('undefined' !== typeof opts.fixedChoice) { + throw new TypeError('Dropdown.init: fixedChoice ' + 'be boolean or undefined. Found: ' + - options.fixedChoice); + opts.fixedChoice); } - - - if ("undefined" === typeof options.tag || - "datalist" === options.tag || - "select" === options.tag) { - this.tag = options.tag; + if ("undefined" === typeof opts.tag) { + this.tag = "datalist"; + } + else if ("datalist" === opts.tag || "select" === opts.tag) { + this.tag = opts.tag; } else { - throw new TypeError('Dropdown.init: options.tag must ' + - 'be "datalist" or "select". Found: ' + - options.tag); + throw new TypeError('Dropdown.init: tag must ' + + 'be "datalist", "select" or undefined. Found: ' + opts.tag); } // Set the main onchange listener, if any. - if ('function' === typeof options.listener) { + if ('function' === typeof opts.listener) { this.listener = function (e) { - options.listener.call(this, e); + opts.listener.call(this, e); }; } - else if ('undefined' !== typeof options.listener) { - throw new TypeError('Dropdown.init: opts.listener must ' + + else if ('undefined' !== typeof opts.listener) { + throw new TypeError('Dropdown.init: listener must ' + 'be function or undefined. Found: ' + - options.listener); + opts.listener); } // Set an additional onchange, if any. - if ('function' === typeof options.onchange) { - this.onchange = options.onchange; + if ('function' === typeof opts.onchange) { + this.onchange = opts.onchange; } - else if ('undefined' !== typeof options.onchange) { - throw new TypeError('Dropdownn.init: opts.onchange must ' + + else if ('undefined' !== typeof opts.onchange) { + throw new TypeError('Dropdownn.init: onchange must ' + 'be function or undefined. Found: ' + - options.onchange); + opts.onchange); } // Set an additional validation, if any. - if ('function' === typeof options.validation) { - this.validation = options.validation; + if ('function' === typeof opts.validation) { + this.validation = opts.validation; } - else if ('undefined' !== typeof options.validation) { - throw new TypeError('Dropdownn.init: opts.validation must ' + + else if ('undefined' !== typeof opts.validation) { + throw new TypeError('Dropdownn.init: validation must ' + 'be function or undefined. Found: ' + - options.validation); + opts.validation); } // Option shuffleChoices, default false. - if ('undefined' === typeof options.shuffleChoices) tmp = false; - else tmp = !!options.shuffleChoices; + if ('undefined' === typeof opts.shuffleChoices) tmp = false; + else tmp = !!opts.shuffleChoices; this.shuffleChoices = tmp; - if (options.width) { - if ('string' !== typeof options.width) { + if (opts.width) { + if ('string' !== typeof opts.width) { throw new TypeError('Dropdownn.init:width must be string or ' + - 'undefined. Found: ' + options.width); + 'undefined. Found: ' + opts.width); } - this.inputWidth = options.width; + this.inputWidth = opts.width; } // Validation Speed - if ('undefined' !== typeof options.validationSpeed) { + if ('undefined' !== typeof opts.validationSpeed) { - tmp = J.isInt(options.valiadtionSpeed, 0, undefined, true); + tmp = J.isInt(opts.valiadtionSpeed, 0, undefined, true); if (tmp === false) { throw new TypeError('Dropdownn.init: validationSpeed must ' + ' a non-negative number or undefined. Found: ' + - options.validationSpeed); + opts.validationSpeed); } this.validationSpeed = tmp; } + // Hint (must be done after requiredChoice) + tmp = opts.hint; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp && false !== tmp) { + throw new TypeError('Dropdown.init: hint cb must ' + + 'return string or false. Found: ' + + tmp); + } + } + if ('string' === typeof tmp || false === tmp) { + this.hint = tmp; + } + else if ('undefined' !== typeof tmp) { + throw new TypeError('Dropdown.init: hint must ' + + 'be a string, false, or undefined. Found: ' + + tmp); + } + if (this.requiredChoice && tmp !== false && + opts.displayRequired !== false) { + + this.hint = tmp ? + (this.hint + ' ' + this.requiredMark) : ' ' + this.requiredMark; + } } // Implements the Widget.append method. Dropdown.prototype.append = function () { - if (W.gid(this.id)) { throw new Error('Dropdown.append: id is not unique: ' + this.id); } - var text = this.text; - var label = this.label; + var mt; - text = W.get('p'); - text.innerHTML = this.mainText; - text.id = 'p'; - this.bodyDiv.appendChild(text); + if (this.mainText) { + mt = W.append('span', this.bodyDiv, { + className: 'dropdown-maintext', + innerHTML: this.mainText + }); + } + + // Hint. + if (this.hint) { + W.append('span', mt || this.bodyDiv, { + className: 'dropdown-hint', + innerHTML: this.hint + }); + } - label = W.get('label'); - label.innerHTML = this.labelText - this.bodyDiv.appendChild(label); + if (this.labelText) { + W.append('label', this.bodyDiv, { + innerHTML: this.labelText + }); + } this.setChoices(this.choices, true); this.errorBox = W.append('div', this.bodyDiv, { - className: 'errbox', id: 'errbox' + className: 'errbox' }); }; Dropdown.prototype.setChoices = function (choices, append) { - var tag, option, order, placeHolder; - var select, datalist, input, create; - var i, len; + var isDatalist, order; + var select; + var i, len, value, name; // TODO validate choices. this.choices = choices; if (!append) return; - create = false; - if (this.menu) this.menu.innerHTML = ''; - else create = true; + isDatalist = this.tag === 'datalist'; - if (create) { - placeHolder = this.placeHolder; - tag = this.tag; - if (tag === "datalist" || "undefined" === typeof tag) { + // Create the structure from scratch or just clear all options. + if (this.menu) { + select = isDatalist ? this.datalist : this.menu; + select.innerHTML = ''; + } + else { + if (isDatalist) { - datalist = W.get('datalist'); - datalist.id = "dropdown"; + this.menu = W.add('input', this.bodyDiv, { + id: this.id, + autocomplete: 'off' + }); - input = W.get('input'); - input.setAttribute('list', datalist.id); - input.id = this.id; - input.autocomplete = "off"; - if (placeHolder) { input.placeholder = placeHolder; } - if (this.inputWidth) input.style.width = this.inputWidth; - this.bodyDiv.appendChild(input); - this.bodyDiv.appendChild(datalist); - this.menu = input; + this.datalist = select = W.add('datalist', this.bodyDiv, { + id: this.id + "_datalist" + }); + this.menu.setAttribute('list', this.datalist.id); } - else if (tag === "select") { + else { select = W.get('select'); select.id = this.id; - if (this.inputWidth) select.style.width = this.inputWidth; - if (placeHolder) { - option = W.get('option'); - option.value = ""; - option.innerHTML = placeHolder; - option.setAttribute("disabled", ""); - option.setAttribute("selected", ""); - option.setAttribute("hidden", ""); - select.appendChild(option); - } this.bodyDiv.appendChild(select); this.menu = select; } } + // Set width. + if (this.inputWidth) this.menu.style.width = this.inputWidth; + + // Adding placeholder. + if (this.placeholder) { + if (isDatalist) { + this.menu.placeholder = this.placeholder; + } + else { + + W.add('option', this.menu, { + value: '', + innerHTML: this.placeholder, + // Makes the placeholder unselectable after first click. + disabled: '', + selected: '', + hidden: '' + }); + } + } + + // Adding all options. len = choices.length; order = J.seq(0, len - 1); if (this.shuffleChoices) order = J.shuffle(order); - for (i = 0; i < len; i++) { - option = W.get('option'); - option.value = choices[order[i]]; - option.innerHTML = choices[order[i]]; - this.menu.appendChild(option); + + // Determining value and name of choice. + value = name = choices[order[i]]; + if ('object' === typeof value) { + if ('undefined' !== typeof value.value) { + name = value.name; + value = value.value; + } + else if (J.isArray(value)) { + name = value[1]; + value = value[0]; + } + } + + // select is a datalist element if tag is "datalist". + W.add('option', select, { + value: value, + innerHTML: name + }); } this.enable(); - } + }; /** * ### Dropdown.verifyChoice @@ -14506,27 +16151,38 @@ * - correctChoice: the choices are compared against correct ones. * - fixedChoice: compares the choice with given choices. * - * @return {boolean|null} TRUE if current choice is correct, - * FALSE if it is not correct, or NULL if no correct choice - * was set + * If a custom validation is set, it will executed with the current + * result of the validation. + * + * @return {object} res The result of the verification and validation. + * The object is of the type: + * ```js + * { + * value: boolean/null // TRUE if current choice is correct, + * // FALSE if it is not correct, + * // or NULL if no correct choice was set. + * } + * ``` + * The custom validation function, if any is set, can add + * information to the return object. * + * @see Dropdown.validation */ Dropdown.prototype.verifyChoice = function () { var that = this; var correct = this.correctChoice; var current = this.currentChoice; + var correctOptions; var res = { value: '' }; if (this.tag === "select" && this.numberOfChanges === 0) { - - current = this.currentChoice = this.menu.value; - + current = this.currentChoice = this.menu.value || null; } if (this.requiredChoice) { - res.value = current !== null; + res.value = current !== null && current !== this.placeholder; } // If no correct choice is set return null. @@ -14538,7 +16194,7 @@ res.value = current === this.choices[correct]; } if (J.isArray(correct)) { - var correctOptions = correct.map(function (x) { + correctOptions = correct.map(function (x) { return that.choices[x]; }); res.value = correctOptions.indexOf(current) >= 0; @@ -14548,19 +16204,13 @@ if (this.choices.indexOf(current) < 0) res.value = false; } - if (this.validation) { - if (undefined === typeof res) { - throw new TypeError('something'); - } - - this.validation(this.currentChoice, res); - } + if (this.validation) this.validation(this.currentChoice, res); return res; }; /** - * ### ChoiceTable.setError + * ### Dropdown.setError * * Set the error msg inside the errorBox * @@ -14570,7 +16220,7 @@ */ Dropdown.prototype.setError = function (err) { // TODO: the errorBox is added only if .append() is called. - // However, ChoiceTableGroup use the table without calling .append(). + // However, DropdownGroup use the table without calling .append(). if (this.errorBox) this.errorBox.innerHTML = err || ''; if (err) this.highlight(); else this.unhighlight(); @@ -14605,7 +16255,6 @@ * @see Dropdown.highlighted */ Dropdown.prototype.unhighlight = function () { - if (this.highlighted !== true) return; this.menu.style.border = ''; this.highlighted = false; @@ -14613,6 +16262,135 @@ this.emit('unhighlighted'); }; + /** + * ### Dropdown.selectChoice + * + * Select a given choice in the datalist or select tag. + * + * @param {string|number} choice. Its value depends on the tag. + * + * - "datalist": a string, if number it is resolved to the name of + * the choice at idx === choice. + * - "select": a number, if string it is resolved to the idx of + * the choice name === choice. Value -1 will unselect all choices. + * + * @return {string|number} idx The resolved name or index + */ + Dropdown.prototype.selectChoice = function (choice) { + // idx is a number if tag is select and a string if tag is datalist. + var idx; + + if (!this.choices || !this.choices.length) return; + if ('undefined' === typeof choice) return; + + idx = choice; + + if (this.tag === 'select') { + if ('string' === typeof choice) { + idx = getIdxOfChoice(this, choice); + if (idx === -1) { + node.warn('Dropdown.selectChoice: choice not found: ' + + choice); + return; + } + } + else if (null === choice || false === choice) { + idx = 0; + } + else if ('number' === typeof choice) { + // 1-based. 0 is for deselecting everything. + idx++; + } + else { + throw new TypeError('Dropdown.selectChoice: invalid choice: ' + + choice); + } + + // Set the choice. + this.menu.selectedIndex = idx; + } + else { + + if ('number' === typeof choice) { + idx = getChoiceOfIdx(this, choice); + if ('undefined' === typeof idx) { + node.warn('Dropdown.selectChoice: choice not found: ' + + choice); + return; + } + } + else if ('string' !== typeof choice) { + throw new TypeError('Dropdown.selectChoice: invalid choice: ' + + choice); + } + + this.menu.value = idx; + } + + // Simulate event. + this.listener({ target: this.menu }); + + return idx; + }; + + /** + * ### Dropdown.setValues + * + * Set the values on the dropdown menu + * + * @param {object} opts Optional. Configuration options. + * + * @see Dropdown.verifyChoice + */ + Dropdown.prototype.setValues = function(opts) { + var choice, correctChoice; + var i, len, j, lenJ; + + if (!this.choices || !this.choices.length) { + throw new Error('Dropdown.setValues: no choices found.'); + } + if ('undefined' === typeof opts) opts = {}; + + // TODO: this code is duplicated from ChoiceTable. + if (opts.correct && this.correctChoice !== null) { + + // Make it an array (can be a string). + correctChoice = J.isArray(this.correctChoice) ? + this.correctChoice : [this.correctChoice]; + + i = -1, len = correctChoice.length; + for ( ; ++i < len ; ) { + choice = parseInt(correctChoice[i], 10); + if (this.shuffleChoices) { + j = -1, lenJ = this.order.length; + for ( ; ++j < lenJ ; ) { + if (this.order[j] === choice) { + choice = j; + break; + } + } + } + + this.selectChoice(choice); + } + return; + } + + // Set values, random or pre-set. + if ('number' === typeof opts || 'string' === typeof opts) { + opts = { values: opts }; + } + else if (opts && 'undefined' === typeof opts.values) { + // Select has index 0 for deselecting + opts = { values: J.randomInt(this.choices.length) -1 }; + // TODO: merge other options if they are used by selectChoice. + } + + // If other options are used (rather than values) change TODO above. + this.selectChoice(opts.values); + + }; + /** * ### Dropdown.getValues * @@ -14625,9 +16403,9 @@ * @see Dropdown.verifyChoice */ Dropdown.prototype.getValues = function (opts) { - var obj; + var obj, verif; opts = opts || {}; - var verif = this.verifyChoice().value; + verif = this.verifyChoice().value; obj = { id: this.id, @@ -14646,6 +16424,7 @@ if (null !== this.correctChoice || null !== this.requiredChoice || null !== this.fixedChoice) { + obj.isCorrect = verif; if (!obj.isCorrect && opts.highlight) this.highlight(); } @@ -14656,7 +16435,18 @@ }; /** - * ### ChoiceTable.listeners + * ### Dropdown.isChoiceDone + * + * Returns TRUE if the choice/s has been done, if requested + * + * @return {boolean} TRUE if the choice is done + */ + Dropdown.prototype.isChoiceDone = function() { + return this.verifyChoice().value !== false; + }; + + /** + * ### Dropdown.listeners * * Implements Widget.listeners * @@ -14676,41 +16466,69 @@ }; /** - * ### ChoiceTable.disable + * ### Dropdown.disable * - * Disables clicking on the table and removes CSS 'clicklable' class + * Disables the dropdown menu */ Dropdown.prototype.disable = function () { if (this.disabled === true) return; this.disabled = true; - if (this.menu) { - this.menu.removeEventListener('change', this.listener); - } + if (this.menu) this.menu.removeEventListener('change', this.listener); this.emit('disabled'); }; /** - * ### ChoiceTable.enable - * - * Enables clicking on the table and adds CSS 'clicklable' class + * ### Dropdown.enable * - * @return {function} cb The event listener function + * Enables the dropdown menu */ Dropdown.prototype.enable = function () { if (this.disabled === false) return; if (!this.menu) { - throw new Error('Dropdown.enable: menu is not defined'); + throw new Error('Dropdown.enable: dropdown menu not found.'); } this.disabled = false; this.menu.addEventListener('change', this.listener); this.emit('enabled'); }; + // ## Helper methods. + + + function getChoiceOfIdx(that, idx) { + return extractChoice(that.choices[idx]); + + } + + function extractChoice(c) { + if ('object' === typeof c) { + if ('undefined' !== typeof c.name) c = c.name; + else c = c[1]; + } + return c; + } + + function getIdxOfChoice(that, choice) { + var i, len, c; + len = that.choices.length; + for (i = 0; i < len; i++) { + c = that.choices[i]; + // c can be string, object, or array. + if ('object' === typeof c) { + if ('undefined' !== typeof c.name) c = c.name; + else c = c[1]; + } + if (c === choice) return i; + } + return -1; + } + + })(node); /** * # EmailForm - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays a form to input email @@ -14728,7 +16546,6 @@ EmailForm.version = '0.13.1'; EmailForm.description = 'Displays a configurable email form.'; - EmailForm.title = false; EmailForm.className = 'emailform'; EmailForm.texts = { @@ -15130,7 +16947,7 @@ /** * # EndScreen - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates an interface to display final earnings, exit code, etc. @@ -15146,11 +16963,10 @@ // ## Add Meta-data - EndScreen.version = '0.7.2'; + EndScreen.version = '0.8.0'; EndScreen.description = 'Game end screen. With end game message, ' + 'email form, and exit code.'; - EndScreen.title = false; EndScreen.className = 'endscreen'; EndScreen.texts = { @@ -15180,11 +16996,11 @@ * * Creates a new instance of EndScreen * - * @param {object} options Configuration options + * @param {object} opts Configuration options * * @see EndScreen.init */ - function EndScreen(options) { + function EndScreen(opts) { /** * ### EndScreen.showEmailForm @@ -15276,86 +17092,101 @@ * * If TRUE, after being appended it sends a 'WIN' message to server * - * Default: FALSE + * Default: TRUE */ - this.askServer = options.askServer || false; + this.askServer = true; + + /** + * ### EndScreen.maxDecimals + * + * The max number of decimals in each number in the win field + * + * Decimals are not enforceed, i.e., if a number has no decimals, + * it will be left as is. + * + * FALSE to allow for any number of decimals. + * + * It only applies to incoming data from server. + * + * Default: 2 + */ + this.maxDec = 2; } - EndScreen.prototype.init = function(options) { + EndScreen.prototype.init = function(opts) { + + if ('undefined' !== typeof opts.askServer) { + this.askServer = !!opts.askServer; + } - if (options.email === false) { + if (opts.email === false) { this.showEmailForm = false; } - else if ('boolean' === typeof options.showEmailForm) { - this.showEmailForm = options.showEmailForm; + else if ('boolean' === typeof opts.showEmailForm) { + this.showEmailForm = opts.showEmailForm; } - else if ('undefined' !== typeof options.showEmailForm) { - throw new TypeError('EndScreen.init: ' + - 'options.showEmailForm ' + - 'must be boolean or undefined. ' + - 'Found: ' + options.showEmailForm); + else if ('undefined' !== typeof opts.showEmailForm) { + throw new TypeError('EndScreen.init: opts.showEmailForm ' + + 'must be boolean or undefined. Found: ' + + opts.showEmailForm); } - if (options.feedback === false) { + if (opts.feedback === false) { this.showFeedbackForm = false; } - else if ('boolean' === typeof options.showFeedbackForm) { - this.showFeedbackForm = options.showFeedbackForm; + else if ('boolean' === typeof opts.showFeedbackForm) { + this.showFeedbackForm = opts.showFeedbackForm; } - else if ('undefined' !== typeof options.showFeedbackForm) { - throw new TypeError('EndScreen.init: ' + - 'options.showFeedbackForm ' + - 'must be boolean or undefined. ' + - 'Found: ' + options.showFeedbackForm); + else if ('undefined' !== typeof opts.showFeedbackForm) { + throw new TypeError('EndScreen.init: opts.showFeedbackForm ' + + 'must be boolean or undefined. Found: ' + + opts.showFeedbackForm); } - if (options.totalWin === false) { + if (opts.totalWin === false) { this.showTotalWin = false; } - else if ('boolean' === typeof options.showTotalWin) { - this.showTotalWin = options.showTotalWin; + else if ('boolean' === typeof opts.showTotalWin) { + this.showTotalWin = opts.showTotalWin; } - else if ('undefined' !== typeof options.showTotalWin) { - throw new TypeError('EndScreen.init: ' + - 'options.showTotalWin ' + - 'must be boolean or undefined. ' + - 'Found: ' + options.showTotalWin); + else if ('undefined' !== typeof opts.showTotalWin) { + throw new TypeError('EndScreen.init: opts.showTotalWin ' + + 'must be boolean or undefined. Found: ' + + opts.showTotalWin); } - if (options.exitCode === false) { - options.showExitCode !== false + if (opts.exitCode === false) { + opts.showExitCode !== false } - else if ('boolean' === typeof options.showExitCode) { - this.showExitCode = options.showExitCode; + else if ('boolean' === typeof opts.showExitCode) { + this.showExitCode = opts.showExitCode; } - else if ('undefined' !== typeof options.showExitCode) { - throw new TypeError('EndScreen.init: ' + - 'options.showExitCode ' + - 'must be boolean or undefined. ' + - 'Found: ' + options.showExitCode); + else if ('undefined' !== typeof opts.showExitCode) { + throw new TypeError('EndScreen.init: opts.showExitCode ' + + 'must be boolean or undefined. Found: ' + + opts.showExitCode); } - if ('string' === typeof options.totalWinCurrency && - options.totalWinCurrency.trim() !== '') { + if ('string' === typeof opts.totalWinCurrency && + opts.totalWinCurrency.trim() !== '') { - this.totalWinCurrency = options.totalWinCurrency; + this.totalWinCurrency = opts.totalWinCurrency; } - else if ('undefined' !== typeof options.totalWinCurrency) { + else if ('undefined' !== typeof opts.totalWinCurrency) { throw new TypeError('EndScreen.init: ' + - 'options.totalWinCurrency must be undefined ' + + 'opts.totalWinCurrency must be undefined ' + 'or a non-empty string. Found: ' + - options.totalWinCurrency); + opts.totalWinCurrency); } - if (options.totalWinCb) { - if ('function' === typeof options.totalWinCb) { - this.totalWinCb = options.totalWinCb; + if (opts.totalWinCb) { + if ('function' === typeof opts.totalWinCb) { + this.totalWinCb = opts.totalWinCb; } else { - throw new TypeError('EndScreen.init: ' + - 'options.totalWinCb ' + - 'must be function or undefined. ' + - 'Found: ' + options.totalWinCb); + throw new TypeError('EndScreen.init: opts.totalWinCb ' + + 'must be function or undefined. Found: ' + + opts.totalWinCb); } } @@ -15374,13 +17205,13 @@ errString: 'Please enter a valid email and retry' }, setMsg: true // Sends a set message for logic's db. - }, options.email)); + }, opts.email)); } if (this.showFeedbackForm) { this.feedback = node.widgets.get('Feedback', J.mixin( { storeRef: false, minChars: 50, setMsg: true }, - options.feedback)); + opts.feedback)); } }; @@ -15402,6 +17233,7 @@ var totalWinElement, totalWinParaElement, totalWinInputElement; var exitCodeElement, exitCodeParaElement, exitCodeInputElement; var exitCodeBtn, exitCodeGroup; + var basePay; var that = this; endScreenElement = document.createElement('div'); @@ -15452,7 +17284,8 @@ exitCodeGroup.className = 'input-group-btn'; exitCodeBtn = document.createElement('button'); - exitCodeBtn.className = 'btn btn-default endscreen-copy-btn'; + exitCodeBtn.className = + 'btn btn-outline-secondary endscreen-copy-btn'; exitCodeBtn.innerHTML = this.getText('copyButton'); exitCodeBtn.type = 'button'; exitCodeBtn.onclick = function() { @@ -15468,6 +17301,13 @@ this.exitCodeInputElement = exitCodeInputElement; } + basePay = node.game.settings.BASE_PAY; + if ('undefined' !== typeof basePay) { + this.updateDisplay({ + basePay: basePay, total: basePay, exitCode: 'N/A' + }); + } + if (this.showEmailForm) { node.widgets.append(this.emailForm, endScreenElement, { title: false, @@ -15503,7 +17343,8 @@ document.execCommand('copy', false); inp.remove(); alert(this.getText('exitCopyMsg')); - } catch (err) { + } + catch (err) { alert(this.getText('exitCopyError')); } }; @@ -15519,7 +17360,7 @@ */ EndScreen.prototype.updateDisplay = function(data) { var preWin, totalWin, totalRaw, exitCode; - var totalHTML, exitCodeHTML, ex, err; + var totalHTML, exitCodeHTML, ex, err, i, len; if (this.totalWinCb) { totalWin = this.totalWinCb(data, this); @@ -15545,37 +17386,39 @@ preWin = ''; if ('undefined' !== typeof data.basePay) { - preWin = data.basePay; - + preWin = enforceDecimals(data.basePay, this.maxDec); } if ('undefined' !== typeof data.bonus && data.showBonus !== false) { if (preWin !== '') preWin += ' + '; - preWin += data.bonus; + preWin += enforceDecimals(data.bonus, this.maxDec); } if (data.partials) { if (!J.isArray(data.partials)) { - node.err('EndScreen error, invalid partials win: ' + - data.partials); + node.err('EndScreen error, partials must be array. ' + + 'Found: ' + data.partials); } else { - // If there is a basePay we already have a preWin. - if (preWin !== '') preWin += ' + '; - preWin += data.partials.join(' + '); + len = data.partials.length; + for (i = 0; i < len; i++) { + preWin += ' + ' + enforceDecimals(data.partials[i], + this.maxDec); + } } } if ('undefined' !== typeof data.totalRaw) { if (preWin) preWin += ' = '; else preWin = ''; - preWin += data.totalRaw; + preWin += enforceDecimals(data.totalRaw, this.maxDec); // Get Exchange Rate. ex = 'undefined' !== typeof data.exchangeRate ? - data.exchangeRate : node.game.settings.EXCHANGE_RATE; + enforceDecimals(data.exchangeRate, this.maxDec) : + node.game.settings.EXCHANGE_RATE; // If we have an exchange rate, check if we have a totalRaw. if ('undefined' !== typeof ex) preWin += '*' + ex; @@ -15591,6 +17434,9 @@ totalWin = this.getText('errTotalWin'); err = true; } + else { + totalWin = enforceDecimals(totalWin, this.maxDec); + } } } @@ -15620,11 +17466,30 @@ } }; + /** + * #### enforceDecimals + * + * @param {number|string} num The number or string to enforce + * @param {number|bool} nDec Number of decimals, or FALSE to not enforce + * @param {boolean} forceNum If TRUE, it forces the return of a number. + * + * @returns The number with at most the specified num of decimals + */ + function enforceDecimals(num, nDec, forceNum) { + var idx; + if (nDec !== false) { + num = '' + num; + idx = num.lastIndexOf('.'); + if (idx > num.length - 3) num = num.substring(0, idx+3); + } + return forceNum ? Number(num) : num; + } + })(node); /** * # Feedback - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Sends a feedback message to the server @@ -15646,7 +17511,6 @@ Feedback.version = '1.6.0'; Feedback.description = 'Displays a configurable feedback form'; - Feedback.title = 'Feedback'; Feedback.className = 'feedback'; Feedback.texts = { @@ -15706,12 +17570,6 @@ colOver = '#a32020'; // #f2dede'; colRemain = '#78b360'; // '#dff0d8'; - // ## Dependencies - - Feedback.dependencies = { - JSUS: {} - }; - /** * ## Feedback constructor * @@ -15888,6 +17746,12 @@ } } + if (this.minWords || this.minChars || this.maxWords || + this.maxChars) { + + this.required = true; + } + /** * ### Feedback.rows * @@ -16189,6 +18053,17 @@ return res; }; + /** + * ### Feedback.isChoiceDone + * + * Returns TRUE if the feedback was filled as requested + * + * @return {boolean} TRUE if the feedback was filled as requested + */ + Feedback.prototype.isChoiceDone = function() { + return this.verifyFeedback(); + }; + /** * ### Feedback.append * @@ -16564,9 +18439,132 @@ })(node); +/** + * # Goto + * Copyright(c) 2023 Stefano Balietti + * MIT Licensed + * + * Creates a simple interface to go to a step in the sequence. + * + * www.nodegame.org + * + * + * TODO: Update Style: + + + + + */ + (function(node) { + + "use strict"; + + node.widgets.register('Goto', Goto); + + // ## Meta-data + + Goto.version = '0.0.1'; + Goto.description = 'Creates a simple interface to move across ' + + 'steps in the sequence.'; + + Goto.panel = false; + Goto.className = 'goto'; + + /** + * ## Goto constructor + * + * Creates a new instance of Goto + * + * @param {object} options Optional. Configuration options. + * + * @see Goto.init + */ + function Goto(options) { + /** + * ### Goto.dropdown + * + * A callback executed after the button is clicked + * + * If it return FALSE, node.done() is not called. + */ + this.dropdown; + } + + Goto.prototype.append = function() { + this.dropdown = node.widgets.append('Dropdown', this.bodyDiv, { + tag: 'select', + choices: getSequence(), + id: 'ng_goto', + placeholder: 'Go to Step', + width: '15rem', + onchange: function(choice, datalist, that) { + node.game.gotoStep(choice); + } + }); + }; + + /** + * ### Goto.disable + * + * Disables the widget + */ + Goto.prototype.disable = function(opts) { + if (this.disabled) return; + this.disabled = true; + this.dropdown.enable(); + this.emit('disabled', opts); + }; + + /** + * ### Goto.enable + * + * Enables the widget + */ + Goto.prototype.enable = function(opts) { + if (!this.disabled) return; + this.disabled = false; + this.dropdown.disable(); + this.emit('enabled', opts); + }; + + + // ## Helper functions. + + function getSequence(seq) { + var i, j, out, value, vvalue, name, ss; + out = []; + seq = seq || node.game.plot.stager.sequence; + for ( i = 0 ; i < seq.length ; i++) { + value = (i+1); + name = seq[i].id; + for ( j = 0 ; j < seq[i].steps.length ; j++) { + ss = seq[i].steps.length === 1; + vvalue = ss ? value : value + '.' + (j+1); + out.push({ + value: vvalue, + name: vvalue + ' ' + + (ss ? name : name + '.' + seq[i].steps[j]) + }); + } + } + return out; + } + +})(node); + /** * # GroupMalleability - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to measure users' perception of group malleability. @@ -16581,7 +18579,7 @@ // ## Meta-data - GroupMalleability.version = '0.1.0'; + GroupMalleability.version = '0.2.0'; GroupMalleability.description = 'Displays an interface to measure ' + 'perception for group malleability.'; @@ -16623,10 +18621,6 @@ 'can work quickly, your first feeling is generally best.' }; - // ## Dependencies - - GroupMalleability.dependencies = {}; - /** * ## GroupMalleability constructor * @@ -16711,6 +18705,11 @@ else if (opts.mainText !== false) { this.mainText = this.getText('mainText'); } + + // Keep reference to pass to ChoiceTableGroup on creation. + this.requiredMark = opts.requiredMark; + this.displayRequired = opts.displayRequired; + }; GroupMalleability.prototype.append = function() { @@ -16724,7 +18723,9 @@ title: false, panel: false, requiredChoice: this.required, - header: this.header + header: this.header, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark }); }; @@ -16758,7 +18759,7 @@ /** * # LanguageSelector - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Manages and displays information about languages available and selected @@ -16775,19 +18776,16 @@ // ## Meta-data - LanguageSelector.version = '0.6.2'; + LanguageSelector.version = '0.6.3'; LanguageSelector.description = 'Display information about the current ' + - 'language and allows to change language.'; - LanguageSelector.title = 'Language'; - LanguageSelector.className = 'languageselector'; + 'language and allows users to change it.'; - LanguageSelector.texts.loading = 'Loading language information...'; + LanguageSelector.title = 'Select Language'; - // ## Dependencies - LanguageSelector.dependencies = { - JSUS: {} - }; + LanguageSelector.className = 'languageselector'; + + LanguageSelector.texts.loading = 'Loading...'; /** * ## LanguageSelector constructor @@ -16939,13 +18937,14 @@ * @see LanguageSelector.setLanguage */ this.onLangCallback = function(msg) { - var language; + var language, label, display, counter; // Clear display. while (that.displayForm.firstChild) { that.displayForm.removeChild(that.displayForm.firstChild); } + counter = 0; // Initialize widget. that.availableLanguages = msg.data; if (that.usingButtons) { @@ -16953,31 +18952,32 @@ // Creates labeled buttons. for (language in msg.data) { if (msg.data.hasOwnProperty(language)) { - that.optionsLabel[language] = W.get('label', { + label = W.get('label', { id: language + 'Label', 'for': language + 'RadioButton' }); - that.optionsDisplay[language] = W.get('input', { + display = W.get('input', { id: language + 'RadioButton', type: 'radio', name: 'languageButton', value: msg.data[language].name }); - that.optionsDisplay[language].onclick = - makeSetLanguageOnClick(language); - - that.optionsLabel[language].appendChild( - that.optionsDisplay[language]); - that.optionsLabel[language].appendChild( - document.createTextNode( - msg.data[language].nativeName)); - W.add('br', that.displayForm); - that.optionsLabel[language].className = - 'unselectedButtonLabel'; - that.displayForm.appendChild( - that.optionsLabel[language]); + display.onclick = makeOnClick(language); + + label.appendChild(display); + + label.appendChild(document.createTextNode( + msg.data[language].nativeName)); + + if (++counter !== 1) W.add('br', that.displayForm); + label.className = 'unselected'; + that.displayForm.appendChild(label); + + that.optionsLabel[language] = label; + that.optionsDisplay[language] = display; + } } } @@ -16985,18 +18985,19 @@ that.displaySelection = W.get('select', 'selectLanguage'); for (language in msg.data) { - that.optionsLabel[language] = + label = document.createTextNode(msg.data[language].nativeName); - that.optionsDisplay[language] = W.get('option', { + display = W.get('option', { id: language + 'Option', value: language }); - that.optionsDisplay[language].appendChild( - that.optionsLabel[language]); - that.displaySelection.appendChild( - that.optionsDisplay[language]); + display.appendChild(label); + that.displaySelection.appendChild(display); + that.optionsLabel[language] = label; + that.optionsDisplay[language] = display } + that.displayForm.appendChild(that.displaySelection); that.displayForm.onchange = function() { that.setLanguage(that.displaySelection.value, @@ -17017,7 +19018,7 @@ that.onLangCallbackExtension = null; } - function makeSetLanguageOnClick(langStr) { + function makeOnClick(langStr) { return function() { that.setLanguage(langStr, that.updatePlayer === 'onselect'); }; @@ -17122,7 +19123,7 @@ this.optionsDisplay[this.currentLanguage].checked = 'unchecked'; this.optionsLabel[this.currentLanguage].className = - 'unselectedButtonLabel'; + 'unselected'; } } @@ -17132,8 +19133,7 @@ if (this.usingButtons) { // Check language button and change className of label. this.optionsDisplay[this.currentLanguage].checked = 'checked'; - this.optionsLabel[this.currentLanguage].className = - 'selectedButtonLabel'; + this.optionsLabel[this.currentLanguage].className = 'selected'; } else { this.displaySelection.value = this.currentLanguage; @@ -17219,12 +19219,6 @@ MoneyTalks.title = 'Earnings'; MoneyTalks.className = 'moneytalks'; - // ## Dependencies - - MoneyTalks.dependencies = { - JSUS: {} - }; - /** * ## MoneyTalks constructor * @@ -17399,7 +19393,7 @@ /** * # MoodGauge - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to query users about mood, emotions and well-being @@ -17414,21 +19408,15 @@ // ## Meta-data - MoodGauge.version = '0.4.0'; + MoodGauge.version = '0.5.0'; MoodGauge.description = 'Displays an interface to measure mood ' + 'and emotions.'; - MoodGauge.title = 'Mood Gauge'; MoodGauge.className = 'moodgauge'; MoodGauge.texts.mainText = 'Thinking about yourself and how you normally' + ' feel, to what extent do you generally feel: '; - // ## Dependencies - MoodGauge.dependencies = { - JSUS: {} - }; - /** * ## MoodGauge constructor * @@ -17620,14 +19608,13 @@ // ## Available methods. // ### I_PANAS_SF - function I_PANAS_SF(options) { - var items, emotions, choices, left, right; + function I_PANAS_SF(opts) { + var items, emotions, choices, left, right, l; var gauge, i, len; - choices = options.choices || - [ '1', '2', '3', '4', '5' ]; + choices = opts.choices || [ '1', '2', '3', '4', '5' ]; - emotions = options.emotions || [ + emotions = opts.emotions || [ 'Upset', 'Hostile', 'Alert', @@ -17639,32 +19626,32 @@ 'Afraid', 'Active' ]; - - left = options.left || 'never'; - - right = options.right || 'always'; - len = emotions.length; + left = opts.left || 'never'; + right = opts.right || 'always'; + items = new Array(len); i = -1; for ( ; ++i < len ; ) { + l = '' + emotions[i] + ': ' + left; items[i] = { id: emotions[i], - left: '' + emotions[i] + ': never', + left: l, right: right, - choices: choices + sameCellWidth: '200px' }; } gauge = node.widgets.get('ChoiceTableGroup', { - id: options.id || 'ipnassf', + id: opts.id || 'ipnassf', items: items, mainText: this.mainText || this.getText('mainText'), - title: false, requiredChoice: true, - storeRef: false + storeRef: false, + header: opts.header, + choices: choices, }); return gauge; @@ -17708,7 +19695,6 @@ // ## Dependencies Requirements.dependencies = { - JSUS: {}, List: {} }; @@ -18354,7 +20340,7 @@ /** * # RiskGauge - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to measure risk preferences with different methods @@ -18371,11 +20357,10 @@ // ## Meta-data - RiskGauge.version = '0.8.0'; + RiskGauge.version = '0.9.0'; RiskGauge.description = 'Displays an interface to ' + 'measure risk preferences with different methods.'; - RiskGauge.title = 'Risk Gauge'; RiskGauge.className = 'riskgauge'; RiskGauge.texts = { @@ -18445,10 +20430,6 @@ // Backward compatibility. RiskGauge.texts.mainText = RiskGauge.texts.holt_laury_mainText; - // ## Dependencies - RiskGauge.dependencies = { - JSUS: {} - }; /** * ## RiskGauge constructor @@ -18573,6 +20554,9 @@ this.on('unhighlighted', function() { if (gauge.unhighlight) gauge.unhighlight(); }); + + this.displayRequired = opts.displayRequired; + this.requiredMark = opts.requiredMark; }; RiskGauge.prototype.append = function() { @@ -18665,7 +20649,9 @@ mainText: this.mainText || this.getText('holt_laury_mainText'), title: false, requiredChoice: true, - storeRef: false + storeRef: false, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark }); return gauge; @@ -18732,6 +20718,10 @@ // Public variables. + // Enables done button on open (only if DoneButton is found under + // node.game.doneButton). + this.enableDoneBtn = opts.enableDoneButton !== false; + // Store locally because they are overwritten. TODO: check if needed. this._highlight = this.highlight; this._unhighlight = this.unhighlight; @@ -18801,6 +20791,15 @@ this.withPrize = 'undefined' === typeof opts.withPrize ? true : !!opts.withPrize; + this.onopen = null; + if (opts.onopen) { + if ('function' !== typeof opts.onopen) { + throw new TypeError('Bomb: onopen must be function or ' + + 'undefined. Found: ' + opts.onopen); + } + this.onopen = opts.onopen; + } + // Bomb box. // Pick bomb box id, if probability permits it, else set to -1. // Resulting id is between 1 and totBoxes. @@ -18861,7 +20860,8 @@ // Main text. W.add('div', that.bodyDiv, { innerHTML: that.mainText || - that.getText('bomb_mainText', probBomb) + that.getText('bomb_mainText', probBomb), + className: 'bomb-maintext' }); // Slider. @@ -18873,12 +20873,11 @@ initialValue: 0, displayValue: false, displayNoChange: false, + displayRequired: that.displayRequired, + requiredMark: that.requiredMark, type: 'flat', required: true, panel: false, - // texts: { - // currentValue: that.getText('sliderValue') - // }, onmove: function(value) { var i, div, c, v; @@ -18907,9 +20906,9 @@ // Update display. W.gid('bomb_numBoxes').innerText = value; - c = that.currency; - v = that.boxValue; if (that.withPrize) { + c = that.currency; + v = that.boxValue; W.gid('bomb_boxValue').innerText = v + c; W.gid('bomb_totalWin').innerText = Number((value * v)).toFixed(2) + c; @@ -18939,7 +20938,7 @@ W.add('p', infoDiv, { innerHTML: that.getText('bomb_boxValue') + ' ' + - this.boxValue + '' + that.boxValue + '' }); W.add('p', infoDiv, { innerHTML: that.getText('bomb_totalWin') + @@ -18950,7 +20949,7 @@ bombResult = W.add('p', infoDiv, { id: 'bomb_result' }); button = W.add('button', that.bodyDiv, { - className: 'btn-danger', + className: 'btn btn-lg btn-danger', innerHTML: that.getText('bomb_openButton'), }); // Initially hidden. @@ -18979,6 +20978,14 @@ cl = 'bomb_' + (isWinner ? 'won' : 'lost'); bombResult.innerHTML = that.getText(cl); bombResult.className += (' ' + cl); + + // Enable done button, if found and disabled. + if (that.enableDoneBtn && node.game.doneButton && + node.game.doneButton.isDisabled()) { + + node.game.doneButton.enable(); + } + if (that.onopen) that.onopen(isWinner, that); }; } }; @@ -19028,7 +21035,7 @@ /** * # SDO - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to measure users' social dominance orientation (S.D.O.) @@ -19043,11 +21050,10 @@ // ## Meta-data - SDO.version = '0.3.0'; + SDO.version = '0.4.0'; SDO.description = 'Displays an interface to measure Social ' + 'Dominance Orientation (S.D.O.).'; - SDO.title = 'SDO'; SDO.className = 'SDO'; @@ -19260,6 +21266,10 @@ } this.mainText = opts.mainText; } + + // Keep reference to pass to ChoiceTableGroup on creation. + this.requiredMark = opts.requiredMark; + this.displayRequired = opts.displayRequired; }; SDO.prototype.append = function() { @@ -19271,7 +21281,9 @@ title: false, panel: false, requiredChoice: this.required, - header: this.header + header: this.header, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark }); }; @@ -19313,7 +21325,7 @@ /** * # Slider - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2024 Stefano Balietti * MIT Licensed * * Creates a configurable slider. @@ -19330,19 +21342,27 @@ // ## Meta-data - Slider.version = '0.4.0'; + Slider.version = '0.7.0'; Slider.description = 'Creates a configurable slider'; - Slider.title = false; Slider.className = 'slider'; - // ## Dependencies - Slider.texts = { currentValue: function(widget, value) { return 'Value: ' + value; }, - noChange: 'No change' + noChange: 'No change', + // TODO: if the knob is hidden, the message is a bit unclear. + error: 'Movement required. If you agree with the current ' + + 'value, move the slider away and then back to this position.', + autoHint: function(w) { + var h = ''; + if (w.knobHiddenFirst) { + h += 'The slider knob will be shown after the first click. '; + } + if (w.required) h += 'Movement required.'; + return h || false; + } }; @@ -19385,6 +21405,12 @@ */ this.initialValue = 50; + /** Slider.step + * + * Legal increments for the slider + */ + this.step = 1; + /** * ### Slider.mainText * @@ -19456,14 +21482,29 @@ */ this.displayNoChange = true; - /** Slider.noChangeSpan + /** Slider.noChangeBtn * * The checkbox form marking the no-change * * @see Slider.displayNoChange * @see Slider.noChangeCheckbox + * @see Slider.noChangeCb */ - this.noChangeSpan = null; + this.noChangeBtn = null; + + /** + * ### Slider.noChangeCb + * + * If a callback executed when the noChangeBtn is clicked + */ + this.noChangeCb = null; + + /** + * ### Slider.errorBox + * + * An HTML element displayed when a validation error occurs + */ + this.errorBox = null; /** Slider.totalMove * @@ -19471,6 +21512,12 @@ */ this.totalMove = 0; + /** Slider.nClicks + * + * Counts onmousedown/touchstart events on the slider + */ + this.nClicks = 0; + /** Slider.volumeSlider * * If TRUE, only the slider to the left of the pointer is colored @@ -19485,6 +21532,18 @@ */ this.hoverColor = '#2076ea'; + /** Slider.left + * + * A text to be displayed at the leftmost position + */ + this.left = null; + + /** Slider.right + * + * A text to be displayed at the righttmost position + */ + this.right = null; + /** Slider.listener * * The main function listening for slider movement @@ -19495,22 +21554,23 @@ * by the no-change checkbox. Note: when the function is invoked * by the browser, noChange is the change event. * + * @param {boolean} init Optional If true, the function is called + * by the init method, and some operations (e.g., updating totalMove) + * are not executed. + * * @see Slider.onmove */ var timeOut = null; - this.listener = function(noChange) { + this.listener = function(noChange, init, sync) { + var _listener; if (!noChange && timeOut) return; if (that.isHighlighted()) that.unhighlight(); - timeOut = setTimeout(function() { - var percent, diffPercent; + _listener = function() { + var percent, diff; percent = (that.slider.value - that.min) * that.scale; - diffPercent = percent - that.currentValue; - that.currentValue = percent; - - // console.log(diffPercent); // console.log(that.slider.value, percent); if (that.type === 'volume') { @@ -19524,24 +21584,38 @@ if (that.displayValue) { that.valueSpan.innerHTML = - that.getText('currentValue', that.slider.value); + that.getText('currentValue', that.slider.value); } if (that.displayNoChange && noChange !== true) { if (that.noChangeCheckbox.checked) { that.noChangeCheckbox.checked = false; - J.removeClass(that.noChangeSpan, 'italic'); + J.removeClass(that.noChangeBtn, 'italic'); } } - that.totalMove += Math.abs(diffPercent); - - if (that.onmove) { - that.onmove.call(that, that.slider.value, diffPercent); + if (!init) { + // Old (currentValue was a percent). + // diffPercent = percent - that.currentValue; + // that.totalMove += Math.abs(diffPercent); + diff = that.slider.value - that.currentValue; + // console.log(diff); + that.totalMove += Math.abs(diff); + if (that.onmove) { + that.onmove.call(that, that.slider.value, diff); + } } + // Update currentValue. + // Change: vefore currentValue was equal to percent. + that.currentValue = that.slider.value; + + timeOut = null; - }, 0); + }; + + if (sync) _listener(); + else timeOut = setTimeout(_listener, 0); } /** Slider.onmove @@ -19563,6 +21637,25 @@ */ this.timeFrom = 'step'; + /** + * ### Slider.knobHiddenFirst + * + * If TRUE, the knob of the slider is hidden before interaction + */ + this.knobHiddenFirst = false; + + + /** + * ### Slider._tmpColor + * + * The original color of the rangeFill container (default black) + * + * that is replaced upon highlighting. + * Need to do js onmouseover because ccs:hover does not work here. + * + */ + this._tmpColor; + } // ## Slider methods @@ -19614,12 +21707,34 @@ this.initialValue = this.currentValue = tmp; } + // Must be before auto-hint. + if ('undefined' !== typeof opts.hideKnob) { + this.knobHiddenFirst = !!opts.hideKnob; + } + + if ('undefined' !== typeof opts.step) { + tmp = J.isInt(opts.step); + if ('number' !== typeof tmp) { + throw new TypeError(e + 'step must be an integer or ' + + 'undefined. Found: ' + opts.step); + } + this.step = tmp; + } + if ('undefined' !== typeof opts.displayValue) { this.displayValue = !!opts.displayValue; } if ('undefined' !== typeof opts.displayNoChange) { this.displayNoChange = !!opts.displayNoChange; } + if ('undefined' !== typeof opts.noChangeCb) { + if ('function' !== typeof opts.noChangeCb) { + throw new TypeError(e + 'noChangeCb must be function or ' + + 'undefined. Found: ' + opts.noChangeCb); + + } + this.noChangeCb = opts.noChangeCb; + } if (opts.type) { if (opts.type !== 'volume' && opts.type !== 'flat') { @@ -19657,13 +21772,13 @@ this.hint = opts.hint; } else { - // TODO: Do we need it? - // this.hint = this.getText('autoHint'); + this.hint = this.getText('autoHint'); } if (this.required && this.hint !== false) { - if (!this.hint) this.hint = 'Movement required'; - this.hint += ' *'; + if (opts.displayRequired !== false) { + this.hint += ' ' + this.requiredMark; + } } if (opts.onmove) { @@ -19700,6 +21815,23 @@ } this.correctValue = opts.correctValue; } + + tmp = opts.left; + if ('undefined' !== typeof tmp) { + if ('string' !== typeof tmp && 'number' !== typeof tmp) { + throw new TypeError(e + 'left must be string, number or ' + + 'undefined. Found: ' + tmp); + } + this.left = '' + tmp; + } + tmp = opts.right; + if ('undefined' !== typeof tmp) { + if ('string' !== typeof tmp && 'number' !== typeof tmp) { + throw new TypeError(e + 'right must be string, number or ' + + 'undefined. Found: ' + tmp); + } + this.right = '' + tmp; + } }; /** @@ -19709,12 +21841,7 @@ * @param {object} opts Configuration options */ Slider.prototype.append = function() { - var container; - - // The original color of the rangeFill container (default black) - // that is replaced upon highlighting. - // Need to do js onmouseover because ccs:hover does not work here. - var tmpColor; + var container, tmp; var that = this; @@ -19737,99 +21864,305 @@ className: 'container-slider' }); + if (this.left) { + tmp = W.add('span', container); + tmp.innerHTML = this.left; + tmp.style.position = 'relative'; + tmp.style.top = '-20px'; + tmp.style.float = 'left'; + } + this.rangeFill = W.add('div', container, { className: 'fill-slider', // id: 'range-fill' }); - this.slider = W.add('input', container, { + tmp = { className: 'volume-slider', - // id: 'range-slider-input', name: 'rangeslider', type: 'range', min: this.min, - max: this.max - }); + max: this.max, + step: this.step, + }; + if (this.knobHiddenFirst) tmp.style = { opacity: 0 }; + this.slider = W.add('input', container, tmp); + + // Count nClicks. + this.slider.onmousedown = function() { + // Important that it is not three equals here. + if (that.knobHiddenFirst && that.isKnobHidden()) { + that.showKnob(); + that.listener(true, false, true); + } + that.nClicks++; + }; + // For mobile. + this.slider.ontouchstart = this.slider.onmousedown; + // TODO: we should use a CSS class. this.slider.onmouseover = function() { - tmpColor = that.rangeFill.style.background || 'black'; + if (that.slider.disabled) return; + that._tmpColor = that.rangeFill.style.background || 'black'; that.rangeFill.style.background = that.hoverColor; }; this.slider.onmouseout = function() { - that.rangeFill.style.background = tmpColor; + if (that.slider.disabled) return; + that.rangeFill.style.background = that._tmpColor; }; if (this.sliderWidth) this.slider.style.width = this.sliderWidth; - if (this.displayValue) { - this.valueSpan = W.add('span', this.bodyDiv, { - className: 'slider-display-value' - }); + if (this.right) { + tmp = W.add('span', container); + tmp.innerHTML = this.right; + tmp.style.position = 'relative'; + tmp.style.top = '-20px'; + tmp.style.float = 'right'; } if (this.displayNoChange) { - this.noChangeSpan = W.add('span', this.bodyDiv, { - className: 'slider-display-nochange', + this.noChangeBtn = W.add('button', this.bodyDiv, { + className: 'btn btn-danger btn-sm slider-display-nochange', innerHTML: this.getText('noChange') + ' ' }); - this.noChangeCheckbox = W.add('input', this.noChangeSpan, { + this.noChangeCheckbox = W.add('input', this.noChangeBtn, { type: 'checkbox' }); - this.noChangeCheckbox.onclick = function() { - if (that.noChangeCheckbox.checked) { - if (that.slider.value === that.initialValue) return; - that.slider.value = that.initialValue; - that.listener(true); - J.addClass(that.noChangeSpan, 'italic'); + + this.noChangeBtn.onclick = function(event) { + var c, isCheckBox; + c = that.noChangeCheckbox; + isCheckBox = event.target && event.target.type === 'checkbox'; + + // Currently no change, pressed to re-activate movements. + if (that.noChange) { + J.removeClass(that.noChangeBtn, 'italic'); + that.noChange = false; + // Click the checkbox (unless already clicked). + c.checked = false; + that.enableSlider(); } + // Activated no-change. else { - J.removeClass(that.noChangeSpan, 'italic'); + J.addClass(that.noChangeBtn, 'italic'); + // Update state. + that.noChange = true; + // Click the checkbox (unless already clicked). + c.checked = true; + that.disableSlider(); } + // Call callback with current status. + if (that.noChangeCb) that.noChangeCb(that, that.noChange); }; } - this.slider.oninput = this.listener; + if (this.displayValue) { + this.valueSpan = W.add('span', this.bodyDiv, { + className: 'slider-display-value' + }); + } + + this.errorBox = W.append('div', this.bodyDiv, { className: 'errbox' }); + this.slider.value = this.initialValue; + this.slider.oninput = this.listener; - this.slider.oninput(); + this.slider.oninput(false, true); }; Slider.prototype.getValues = function(opts) { - var res, value, nochange; + var res, nochange; opts = opts || {}; res = true; if ('undefined' === typeof opts.highlight) opts.highlight = true; - value = this.currentValue; - nochange = this.noChangeCheckbox && this.noChangeCheckbox.checked; - if ((this.required && this.totalMove === 0 && !nochange) || - (null !== this.correctValue && this.correctValue !== value)) { - - if (opts.highlight) this.highlight(); + if (!this.isChoiceDone()) { + if (opts.highlight) { + this.highlight(); + this.setError(this.getText('error')); + } res = false; } + nochange = this.noChangeCheckbox && this.noChangeCheckbox.checked; + return { - value: value, + value: this.currentValue, noChange: !!nochange, initialValue: this.initialValue, totalMove: this.totalMove, + nClicks: this.nClicks, isCorrect: res, time: node.timer.getTimeSince(this.timeFrom) - }; + }; }; Slider.prototype.setValues = function(opts) { - opts = opts || {}; - this.slider.value = opts.value; - this.slider.oninput(); + var value; + if ('undefined' === typeof opts) opts = {}; + else if ('number' === typeof opts) opts = { value: opts }; + + if (opts.correct && this.correctValue !== null) { + value = this.correctValue; + } + else if ('number' !== typeof opts.value) { + value = J.randomInt(0, 101)-1; + + // Check if movement is required and no movement was done and + // the random value is equal to the current value. If so, add 1. + if (this.required && this.totalMove === 0 && + value === this.slider.value) { + + value++; + } + } + else { + value = opts.value; + } + + this.slider.value = value; + this.slider.oninput(false, false, true); + }; + + + + /** + * ### Slider.disableSlider + * + * Disables the slider only + * + * It hides the knob by default + * + * @param {boolean} showKnob If FALSE it does not show knob + */ + Slider.prototype.disableSlider = function (hideKnob) { + W.addClass(this.rangeFill, 'disabled'); + W.addClass(this.slider, 'disabled'); + this._tmpColor = this.rangeFill.style.background || 'black'; + this.rangeFill.style.background = 'grey'; + this.slider.disabled = true; + if (hideKnob !== false) this.hideKnob(); + }; + + /** + * ### Slider.enableSlider + * + * Enables the slider only + * + * It shows the knob by default, unless it has never been clicked and + * the `knobHiddenFirst` is TRUE. + * + * @param {boolean} showKnob If FALSE it does not show knob + */ + Slider.prototype.enableSlider = function (showKnob) { + W.removeClass(this.rangeFill, 'disabled'); + W.removeClass(this.slider, 'disabled'); + this.rangeFill.style.background = this._tmpColor; + this.slider.disabled = false; + if (showKnob !== false) { + if (this.knobHiddenFirst && this.nClicks !== 0) { + this.showKnob(); + } + } + }; + + /** + * ### Slider.hideKnob + * + * Hides the knob + */ + Slider.prototype.hideKnob = function () { + this.slider.style.opacity = 0; + }; + + /** + * ### Slider.showKnob + * + * Hides the knob + */ + Slider.prototype.showKnob = function () { + this.slider.style.opacity = 1; + }; + + /** + * ### Slider.isKnobHidden + * + * Hides the knob + */ + Slider.prototype.isKnobHidden = function () { + // Two equals important. + return this.slider.style.opacity == 0; + }; + + /** + * ### Slider.disable + * + * Disables the widget + */ + Slider.prototype.disable = function () { + if (this.disabled === true) return; + this.disabled = true; + this.disableSlider(); + if (this.noChangeBtn) { + this.noChangeBtn.disabled = true; + this.noChangeCheckbox.disabled = true; + } + this.emit('disabled'); + }; + + /** + * ### Slider.enable + * + * Enables the widget + */ + Slider.prototype.enable = function () { + if (this.disabled === false) return; + this.disabled = false; + this.enableSlider(); + if (this.noChangeBtn) { + this.noChangeBtn.disabled = false; + this.noChangeCheckbox.disabled = false; + } + this.emit('enabled'); + }; + + /** + * ### Slider.setError + * + * Set the error msg inside the errorBox and call highlight + * + * @param {string} The error msg (can contain HTML) + * + * @see Slider.highlight + * @see Slider.errorBox + */ + Slider.prototype.setError = function(err) { + this.errorBox.innerHTML = err || ''; + if (err) this.highlight(); + else this.unhighlight(); + }; + + /** + * ### Slider.isChoiceDone + * + * Returns TRUE if the slider has been moved (if requested) + * + * @return {boolean} TRUE if the choice is done + */ + Slider.prototype.isChoiceDone = function() { + var value, nochange; + value = this.currentValue; + nochange = this.noChangeCheckbox && this.noChangeCheckbox.checked; + return !((this.required && this.totalMove === 0 && !nochange) || + (null !== this.correctValue && this.correctValue !== value)); }; })(node); /** * # SVOGauge - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to measure users' social value orientation (S.V.O.) @@ -19844,16 +22177,15 @@ // ## Meta-data - SVOGauge.version = '0.8.1'; + SVOGauge.version = '0.9.0'; SVOGauge.description = 'Displays an interface to measure social ' + 'value orientation (S.V.O.).'; - SVOGauge.title = 'SVO Gauge'; SVOGauge.className = 'svogauge'; SVOGauge.texts = { mainText: 'You and another randomly selected participant ' + - 'will receive an extra bonus.
' + + 'will receive an extra bonus. ' + 'Choose the preferred bonus amounts (in cents) for you ' + 'and the other participant in each row.
' + 'We will select one row at random ' + @@ -19864,10 +22196,6 @@ left: 'Your Bonus:
Other\'s Bonus:' }; - // ## Dependencies - - SVOGauge.dependencies = {}; - /** * ## SVOGauge constructor * @@ -19994,6 +22322,9 @@ this.on('unhighlighted', function() { gauge.unhighlight(); }); + + this.displayRequired = opts.displayRequired; + this.requiredMark = opts.requiredMark; }; SVOGauge.prototype.append = function() { @@ -20148,7 +22479,9 @@ title: false, renderer: renderer, requiredChoice: this.required, - storeRef: false + storeRef: false, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark }); return gauge; @@ -20158,7 +22491,7 @@ /** * # VisualRound - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Display information about rounds and/or stage in the game @@ -20176,10 +22509,9 @@ // ## Meta-data - VisualRound.version = '0.9.0'; + VisualRound.version = '0.9.1'; VisualRound.description = 'Displays current/total/left round/stage/step. '; - VisualRound.title = false; VisualRound.className = 'visualround'; VisualRound.texts = { @@ -20387,12 +22719,6 @@ this.updateInformation(); - if (!this.options.displayMode && this.options.displayModeNames) { - console.log('***VisualTimer.init: options.displayModeNames is ' + - 'deprecated. Use options.displayMode instead.***'); - this.options.displayMode = this.options.displayModeNames; - } - if (!this.options.displayMode) { this.setDisplayMode([ 'COUNT_UP_ROUNDS_TO_TOTAL_IFNOT1', @@ -21231,7 +23557,7 @@ /** * # VisualStage - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Shows the name of the current, previous and next step. @@ -21252,7 +23578,6 @@ VisualStage.description = 'Displays the name of the current, previous and next step of the game.'; - VisualStage.title = false; VisualStage.className = 'visualstage'; VisualStage.texts = { @@ -21754,21 +24079,21 @@ * - waitBoxOptions: an option object to be passed to `TimerBox` * - mainBoxOptions: an option object to be passed to `TimerBox` * - * @param {object} options Optional. Configuration options + * @param {object} opts Optional. Configuration options * * @see TimerBox * @see GameTimer */ - VisualTimer.prototype.init = function(options) { - var t, gameTimerOptions; + VisualTimer.prototype.init = function(opts) { + var gameTimerOptions; // We keep the check for object, because this widget is often // called by users and the restart methods does not guarantee // an object. - options = options || {}; - if ('object' !== typeof options) { - throw new TypeError('VisualTimer.init: options must be ' + - 'object or undefined. Found: ' + options); + opts = opts || {}; + if ('object' !== typeof opts) { + throw new TypeError('VisualTimer.init: opts must be ' + + 'object or undefined. Found: ' + opts); } // Important! Do not modify directly options, because it might @@ -21777,36 +24102,36 @@ // If gameTimer is not already set, check options, then // try to use node.game.timer, if defined, otherwise crete a new timer. - if ('undefined' !== typeof options.gameTimer) { + if ('undefined' !== typeof opts.gameTimer) { if (this.gameTimer) { - throw new Error('GameTimer.init: options.gameTimer cannot ' + + throw new Error('GameTimer.init: opts.gameTimer cannot ' + 'be set if a gameTimer is already existing: ' + this.name); } - if ('object' !== typeof options.gameTimer) { - throw new TypeError('VisualTimer.init: options.' + + if ('object' !== typeof opts.gameTimer) { + throw new TypeError('VisualTimer.init: opts.' + 'gameTimer must be object or ' + - 'undefined. Found: ' + options.gameTimer); + 'undefined. Found: ' + opts.gameTimer); } - this.gameTimer = options.gameTimer; + this.gameTimer = opts.gameTimer; } else { if (!this.isInitialized) { this.internalTimer = true; this.gameTimer = node.timer.createTimer({ - name: options.name || 'VisualTimer_' + J.randomInt(10000000) + name: opts.name || 'VisualTimer_' + J.randomInt(10000000) }); } } - if (options.hooks) { + if (opts.hooks) { if (!this.internalTimer) { throw new Error('VisualTimer.init: cannot add hooks on ' + 'external gameTimer.'); } - if (!J.isArray(options.hooks)) { - gameTimerOptions.hooks = [ options.hooks ]; + if (!J.isArray(opts.hooks)) { + gameTimerOptions.hooks = [ opts.hooks ]; } } else { @@ -21825,29 +24150,29 @@ // Important! Manual clone must be done after hooks and gameTimer. // Parse milliseconds option. - if ('undefined' !== typeof options.milliseconds) { + if ('undefined' !== typeof opts.milliseconds) { gameTimerOptions.milliseconds = - node.timer.parseInput('milliseconds', options.milliseconds); + node.timer.parseInput('milliseconds', opts.milliseconds); } // Parse update option. - if ('undefined' !== typeof options.update) { + if ('undefined' !== typeof opts.update) { gameTimerOptions.update = - node.timer.parseInput('update', options.update); + node.timer.parseInput('update', opts.update); } else { gameTimerOptions.update = 1000; } // Parse timeup option. - if ('undefined' !== typeof options.timeup) { - gameTimerOptions.timeup = options.timeup; + if ('undefined' !== typeof opts.timeup) { + gameTimerOptions.timeup = opts.timeup; } // Init the gameTimer, regardless of the source (internal vs external). this.gameTimer.init(gameTimerOptions); - t = this.gameTimer; + // var t = this.gameTimer; // TODO: not using session for now. // node.session.register('visualtimer', { @@ -21870,10 +24195,17 @@ this.options = gameTimerOptions; // Must be after this.options is assigned. - if ('undefined' === typeof this.options.stopOnDone) { + if ('undefined' !== typeof opts.stopOnDone) { + this.options.stopOnDone = !!opts.stopOnDone; + } + else if ('undefined' === typeof this.options.stopOnDone) { this.options.stopOnDone = true; } - if ('undefined' === typeof this.options.startOnPlaying) { + + if ('undefined' !== typeof opts.startOnPlaying) { + this.options.startOnPlaying = !!opts.startOnPlaying; + } + else if ('undefined' === typeof this.options.startOnPlaying) { this.options.startOnPlaying = true; } @@ -21885,7 +24217,7 @@ } J.mixout(this.options.mainBoxOptions, - {classNameBody: options.className, hideTitle: true}); + {classNameBody: opts.className, hideTitle: true}); J.mixout(this.options.waitBoxOptions, {title: 'Max. wait timer', classNameTitle: 'waitTimerTitle', @@ -22384,7 +24716,7 @@ /** * # WaitingRoom - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays the number of connected/required players to start a game @@ -22398,7 +24730,7 @@ node.widgets.register('WaitingRoom', WaitingRoom); // ## Meta-data - WaitingRoom.version = '1.3.0'; + WaitingRoom.version = '1.4.0'; WaitingRoom.description = 'Displays a waiting room for clients.'; WaitingRoom.title = 'Waiting Room'; @@ -22538,7 +24870,6 @@ // #### defaultTreatments defaultTreatments: 'Defaults:' - }; /** @@ -22680,49 +25011,71 @@ this.disconnectIfNotSelected = null; /** - * ### WaitingRoom.playWithBotOption + * ### WaitingRoom.userCanDispatch * - * If TRUE, it displays a button to begin the game with bots + * If TRUE, the interface allows to start a new game * * This option is set by the server, local modifications will * not have an effect if server does not allow it * - * @see WaitingRoom.playBotBtn + * @see WaitingRoom.playBtn */ - this.playWithBotOption = null; + this.userCanDispatch = null; /** - * ### WaitingRoom.playBotBtn + * ### WaitingRoom.playBtn * - * Reference to the button to play with bots + * Reference to the button to play a new game * * Will be created if requested by options. * - * @see WaitingRoom.playWithBotOption + * @see WaitingRoom.userCanDispatch */ - this.playBotBtn = null; + this.playBtn = null; /** - * ### WaitingRoom.selectTreatmentOption + * ### WaitingRoom.userCanSelectTreat * * If TRUE, it displays a selector to choose the treatment of the game * * This option is set by the server, local modifications will * not have an effect if server does not allow it */ - this.selectTreatmentOption = null; + this.userCanSelectTreat = null; /** * ### WaitingRoom.treatmentBtn * * Holds the name of selected treatment * - * Only used if `selectTreatmentOption` is enabled + * Only used if `userCanSelectTreat` is enabled * - * @see WaitingRoom.selectTreatmentOption + * @see WaitingRoom.userCanSelectTreat */ this.selectedTreatment = null; + /** + * ### WaitingRoom.addDefaultTreatments + * + * If TRUE, after the user defined treatments, it adds default ones + * + * It has effect only if WaitingRoom.userCanSelectTreat is TRUE. + * + * Default: TRUE + * + * @see WaitingRoom.userCanSelectTreat + */ + this.addDefaultTreatments = null; + + /** + * ### WaitingRoom.treatmentTiles + * + * If TRUE, treatments are displayed in tiles instead of a dropdown + * + * Default: FALSE + */ + this.treatmentTiles = null; + } // ## WaitingRoom methods @@ -22741,13 +25094,14 @@ * - onSuccess: function executed when all tests succeed * - waitTime: max waiting time to execute all tests (in milliseconds) * - startDate: max waiting time to execute all tests (in milliseconds) - * - playWithBotOption: displays button to dispatch players with bots - * - selectTreatmentOption: displays treatment selector + * - userCanDispatch: displays button to dispatch a new game + * - userCanSelectTreat: displays treatment selector * * @param {object} conf Configuration object. */ WaitingRoom.prototype.init = function(conf) { - var that = this; + var t, that; + that = this; if ('object' !== typeof conf) { throw new TypeError('WaitingRoom.init: conf must be object. ' + @@ -22832,143 +25186,52 @@ } - if (conf.playWithBotOption) this.playWithBotOption = true; - else this.playWithBotOption = false; - if (conf.selectTreatmentOption) this.selectTreatmentOption = true; - else this.selectTreatmentOption = false; - - - // Display Exec Mode. - this.displayExecMode(); - - // Button for bots and treatments. - - if (this.playWithBotOption && !document.getElementById('bot_btn')) { - // Closure to create button group. - (function(w) { - var btnGroup = document.createElement('div'); - btnGroup.role = 'group'; - btnGroup['aria-label'] = 'Play Buttons'; - btnGroup.className = 'btn-group'; - - var playBotBtn = document.createElement('input'); - playBotBtn.className = 'btn btn-primary btn-lg'; - playBotBtn.value = w.getText('playBot'); - playBotBtn.id = 'bot_btn'; - playBotBtn.type = 'button'; - playBotBtn.onclick = function() { - w.playBotBtn.value = w.getText('connectingBots'); - w.playBotBtn.disabled = true; - node.say('PLAYWITHBOT', 'SERVER', w.selectedTreatment); - setTimeout(function() { - w.playBotBtn.value = w.getText('playBot'); - w.playBotBtn.disabled = false; - }, 5000); - }; - - btnGroup.appendChild(playBotBtn); - - // Store reference in widget. - w.playBotBtn = playBotBtn; - - if (w.selectTreatmentOption) { - - var btnGroupTreatments = document.createElement('div'); - btnGroupTreatments.role = 'group'; - btnGroupTreatments['aria-label'] = 'Select Treatment'; - btnGroupTreatments.className = 'btn-group'; - - var btnTreatment = document.createElement('button'); - btnTreatment.className = 'btn btn-default btn-lg ' + - 'dropdown-toggle'; - btnTreatment['data-toggle'] = 'dropdown'; - btnTreatment['aria-haspopup'] = 'true'; - btnTreatment['aria-expanded'] = 'false'; - btnTreatment.innerHTML = w.getText('selectTreatment'); - - var span = document.createElement('span'); - span.className = 'caret'; - - btnTreatment.appendChild(span); - - var ul = document.createElement('ul'); - ul.className = 'dropdown-menu'; - ul.style['text-align'] = 'left'; - - var li, a, t, liT1, liT2, liT3; - if (conf.availableTreatments) { - li = document.createElement('li'); - li.innerHTML = w.getText('gameTreatments'); - li.className = 'dropdown-header'; - ul.appendChild(li); - for (t in conf.availableTreatments) { - if (conf.availableTreatments.hasOwnProperty(t)) { - li = document.createElement('li'); - li.id = t; - a = document.createElement('a'); - a.href = '#'; - a.innerHTML = '' + t + ': ' + - conf.availableTreatments[t]; - li.appendChild(a); - if (t === 'treatment_latin_square') liT3 = li; - else if (t === 'treatment_rotate') liT1 = li; - else if (t === 'treatment_random') liT2 = li; - else ul.appendChild(li); - } - } - li = document.createElement('li'); - li.role = 'separator'; - li.className = 'divider'; - ul.appendChild(li); - li = document.createElement('li'); - li.innerHTML = w.getText('defaultTreatments'); - li.className = 'dropdown-header'; - ul.appendChild(li); - ul.appendChild(liT1); - ul.appendChild(liT2); - ul.appendChild(liT3); - } + if (conf.userCanDispatch) this.userCanDispatch = true; + else this.userCanDispatch = false; + if (conf.userCanSelectTreat) this.userCanSelectTreat = true; + else this.userCanSelectTreat = false; + if ('undefined' !== typeof conf.addDefaultTreatments) { + this.addDefaultTreatments = !!conf.addDefaultTreatments; + } + else { + this.addDefaultTreatments = true; + } - btnGroupTreatments.appendChild(btnTreatment); - btnGroupTreatments.appendChild(ul); + // Button to start a new game and select treatments. + if (conf.queryStringTreatVar) { + t = J.getQueryString(conf.queryStringTreatVar); - btnGroup.appendChild(btnGroupTreatments); + if (t) { + if (!conf.availableTreatments[t]) { + alert('Unknown treatment: ' + t); + } + else { + node.say('DISPATCH', 'SERVER', t); + return; + } + } + } - // We are not using bootstrap js files - // and we redo the job manually here. - btnTreatment.onclick = function() { - // When '' is hidden by bootstrap class. - if (ul.style.display === '') { - ul.style.display = 'block'; - } - else { - ul.style.display = ''; - } - }; + if (conf.treatmentTileCb) { + this.treatmentTileCb = conf.treatmentTileCb; + } - ul.onclick = function(eventData) { - var t; - t = eventData.target; - // When '' is hidden by bootstrap class. - ul.style.display = ''; - t = t.parentNode.id; - // Clicked on description? - if (!t) t = eventData.target.parentNode.parentNode.id; - // Nothing relevant clicked (e.g., header). - if (!t) return; - btnTreatment.innerHTML = t + ' '; - btnTreatment.appendChild(span); - w.selectedTreatment = t; - }; + if ('undefined' !== typeof conf.treatmentTiles) { + this.treatmentTiles = conf.treatmentTiles; + } - // Store Reference in widget. - w.treatmentBtn = btnTreatment; - } - // Append button group. - w.bodyDiv.appendChild(document.createElement('br')); - w.bodyDiv.appendChild(btnGroup); + // Display Exec Mode. + this.displayExecMode(); - })(this); + // Displays treatments / play btn. + if (this.userCanDispatch) { + if (this.userCanSelectTreat) { + this.treatmentTiles ? buildTreatTiles(this, conf) : + buildTreatDropdown(this, conf) + } + else { + addPlayBtn(this); + } } // Handle destroy. @@ -23030,6 +25293,8 @@ * * Displays the state of the waiting room on screen * + * @param {object} update Object containing info about the waiting room + * * @see WaitingRoom.updateState */ WaitingRoom.prototype.updateState = function(update) { @@ -23054,6 +25319,12 @@ */ WaitingRoom.prototype.updateDisplay = function() { var numberOfGameSlots, numberOfGames; + + if (!this.execModeDiv) { + node.warn('WaitingRoom: cannot update display, inteface not ready'); + return; + } + if (this.connected > this.poolSize) { numberOfGames = Math.floor(this.connected / this.groupSize); if ('undefined' !== typeof this.nGames) { @@ -23220,11 +25491,6 @@ // Write about disconnection in page. that.bodyDiv.innerHTML = that.getText('disconnect'); - - // Enough to not display it in case of page refresh. - // setTimeout(function() { - // alert('Disconnection from server detected!'); - // }, 200); }); node.on.data('ROOM_CLOSED', function() { @@ -23232,17 +25498,22 @@ }); }; + /** + * ### WaitingRoom.stopTimer + * + * If found, it stops the timer + */ WaitingRoom.prototype.stopTimer = function() { if (this.timer) { - node.info('waiting room: STOPPING TIMER'); - this.timer.destroy(); + node.info('waiting room: PAUSING TIMER'); + this.timer.stop(); } }; /** * ### WaitingRoom.disconnect * - * Disconnects the playr, stops the timer, and displays a msg + * Disconnects the player, stops the timer, and displays a msg * * @param {string|function} msg. Optional. A disconnect message. If set, * replaces the current value for future calls. @@ -23255,6 +25526,11 @@ this.stopTimer(); }; + /** + * ### WaitingRoom.alertPlayer + * + * Plays a sound and blinks the title of the tab to alert the player + */ WaitingRoom.prototype.alertPlayer = function() { var clearBlink, onFrame; var blink, sound; @@ -23296,4 +25572,244 @@ } }; + // ### Helper functions. + + function addPlayBtn(w) { + var btnGroup, playBtn; + + // Already added. + btnGroup = document.getElementById('play_btn_group'); + if (btnGroup) return btnGroup; + + // Add button to start game. + btnGroup = document.createElement('div'); + btnGroup.id = 'play_btn_group'; + btnGroup.role = 'group'; + btnGroup['aria-label'] = 'Play Buttons'; + btnGroup.className = 'btn-group'; + + playBtn = document.createElement('input'); + playBtn.className = 'btn btn-primary btn-lg'; + playBtn.value = w.getText('playBot'); + playBtn.id = 'play_btn'; + playBtn.type = 'button'; + playBtn.onclick = function() { + w.playBtn.value = w.getText('connectingBots'); + w.playBtn.disabled = true; + node.say('DISPATCH', 'SERVER', w.selectedTreatment); + setTimeout(function() { + w.playBtn.value = w.getText('playBot'); + w.playBtn.disabled = false; + }, 5000); + }; + + btnGroup.appendChild(playBtn); + + // Store reference in widget. + w.playBtn = playBtn; + + // Append button group. + w.bodyDiv.appendChild(document.createElement('br')); + w.bodyDiv.appendChild(btnGroup); + + return btnGroup; + } + + function buildTreatDropdown(w, conf) { + + var btnGroup; + btnGroup = addPlayBtn(w); + + var btnGroupTreatments = document.createElement('div'); + btnGroupTreatments.role = 'group'; + btnGroupTreatments['aria-label'] = 'Select Treatment'; + btnGroupTreatments.className = 'btn-group'; + + var btnTreatment = document.createElement('button'); + btnTreatment.className = 'btn btn-default btn-lg ' + + 'dropdown-toggle'; + btnTreatment['data-toggle'] = 'dropdown'; + btnTreatment['aria-haspopup'] = 'true'; + btnTreatment['aria-expanded'] = 'false'; + btnTreatment.innerHTML = w.getText('selectTreatment'); + + var span = document.createElement('span'); + span.className = 'caret'; + + btnTreatment.appendChild(span); + + var ul = document.createElement('ul'); + ul.className = 'dropdown-menu'; + ul.style['text-align'] = 'left'; + + var li, a, t, liT1, liT2, liT3, liT4; + if (conf.availableTreatments) { + li = document.createElement('li'); + li.innerHTML = w.getText('gameTreatments'); + li.className = 'dropdown-header'; + ul.appendChild(li); + for (t in conf.availableTreatments) { + if (conf.availableTreatments.hasOwnProperty(t)) { + li = document.createElement('li'); + li.id = t; + a = document.createElement('a'); + a.href = '#'; + a.innerHTML = '' + t + ': ' + + conf.availableTreatments[t]; + li.appendChild(a); + if (t === 'treatment_latin_square') liT3 = li; + else if (t === 'treatment_rotate') liT1 = li; + else if (t === 'treatment_random') liT2 = li; + else if (t === 'treatment_weighted_random') liT4 = li; + else ul.appendChild(li); + } + } + + if (w.addDefaultTreatments !== false) { + li = document.createElement('li'); + li.role = 'separator'; + li.className = 'divider'; + ul.appendChild(li); + li = document.createElement('li'); + li.innerHTML = w.getText('defaultTreatments'); + li.className = 'dropdown-header'; + ul.appendChild(li); + ul.appendChild(liT1); + ul.appendChild(liT2); + ul.appendChild(liT3); + ul.appendChild(liT4); + } + } + + btnGroupTreatments.appendChild(btnTreatment); + btnGroupTreatments.appendChild(ul); + + btnGroup.appendChild(btnGroupTreatments); + + // We are not using bootstrap js files + // and we redo the job manually here. + btnTreatment.onclick = function() { + // When '' is hidden by bootstrap class. + if (ul.style.display === '') { + ul.style.display = 'block'; + } + else { + ul.style.display = ''; + } + }; + + ul.onclick = function(eventData) { + var t; + t = eventData.target; + // When '' is hidden by bootstrap class. + ul.style.display = ''; + t = t.parentNode.id; + // Clicked on description? + if (!t) t = eventData.target.parentNode.parentNode.id; + // Nothing relevant clicked (e.g., header). + if (!t) return; + btnTreatment.innerHTML = t + ' '; + btnTreatment.appendChild(span); + w.selectedTreatment = t; + }; + + // Store Reference in widget. + w.treatmentBtn = btnTreatment; + } + + function buildTreatTiles(w, conf) { + var div, a, t, T, display, counter; + var divT1, divT2, divT3, divT4; + var flexBox; + + flexBox = W.add('div', w.bodyDiv); + flexBox.style.display = 'flex'; + flexBox.style['flex-wrap'] = 'wrap'; + flexBox.style['column-gap'] = '20px'; + flexBox.style['justify-content'] = 'space-between'; + flexBox.style['margin'] = '50px 100px 30px 150px'; + flexBox.style['text-align'] = 'center'; + + // border: 1px solid #CCC; + // border-radius: 10px; + // box-shadow: 2px 2px 10px; + // FONT-WEIGHT: 200; + // padding: 10px; + + // --- CAN - SOC waitroom modification --- // + + flexBox.className = 'waitroom-listContainer'; + + // -------------- // + + + counter = 0; + if (conf.availableTreatments) { + for (t in conf.availableTreatments) { + if (conf.availableTreatments.hasOwnProperty(t)) { + div = document.createElement('div'); + div.id = t; + div.style.flex = '200px'; + div.style['margin-top'] = '10px'; + div.className = 'treatment waitroom-list'; + // div.style.display = 'flex'; + + a = document.createElement('span'); + // a.className = + // 'btn btn-default btn-large round btn-icon'; + // a.href = '#'; + if (w.treatmentTileCb) { + display = w.treatmentTileCb(t, + conf.availableTreatments[t], ++counter, w); + } + else { + T = t; + if (t.length > 16) { + T = '' + + t.substr(0, 13) + '...'; + } + display = '' + T + '
' + + '' + + conf.availableTreatments[t] + ''; + } + a.innerHTML = display; + + div.appendChild(a); + + div.onclick = function() { + var t; + t = this.id; + // Clicked on description? + // btnTreatment.innerHTML = t + ' '; + w.selectedTreatment = t; + node.say('DISPATCH', 'SERVER', + w.selectedTreatment); + }; + + t = t.substring(10); + if (t === 'latin_square') divT3 = div; + else if (t === 'rotate') divT1 = div; + else if (t === 'random') divT2 = div; + else if (t === 'weighted_random') divT4 = div; + else flexBox.appendChild(div); + + } + } + + // Hack to fit nicely the treatments. + // div = document.createElement('div'); + // div.style.flex = '200px'; + // div.style['margin-top'] = '10px'; + // div.className = 'waitroom-list'; + // flexBox.appendChild(div); + + if (w.addDefaultTreatments !== false) { + flexBox.appendChild(divT1); + flexBox.appendChild(divT2); + flexBox.appendChild(divT3); + flexBox.appendChild(divT4); + } + } + } + })(node); diff --git a/build/nodegame-widgets.min.js b/build/nodegame-widgets.min.js index 1e00fda..dfe2eb7 100644 --- a/build/nodegame-widgets.min.js +++ b/build/nodegame-widgets.min.js @@ -1,6 +1,6 @@ /** * # Widget - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Prototype of a widget class @@ -12,4 +12,4 @@ * @see Widgets.get * @see Widgets.append */ -(function(e){"use strict";function r(){}function i(e,t,n,r,i){var s;if(!e.constructor[n].hasOwnProperty(t))throw new Error(r+": name not found: "+t);s="undefined"!=typeof e[n][t]?e[n][t]:e.constructor[n][t];if("function"==typeof s){s=s(e,i);if("string"!=typeof s&&s!==!1)throw new TypeError(r+': cb "'+t+'" did not '+"return neither string or false. Found: "+s)}return s}function s(e,n,r,i,s,o){var u,a,f;s||(s=e.constructor[n]),"undefined"==typeof o&&(o={}),u={};if(t.isArray(s)){a=-1,f=s.length;for(;++a1&&(u=f.docked[f.docked.length-2],a=o(u.panelDiv.style.right),a+=u.panelDiv.offsetWidth),a+=r,n.panelDiv.style.right=a+"px",l=0,a+=n.panelDiv.offsetWidth+s;while(f.docked.length>1&&a>e.innerWidth&&l1&&(n+=''+(e.senderToNameMap[t.id]||t.id)+": "),n+=t.msg+"",n},quit:function(e,t){return(e.senderToNameMap[t.id]||t.id)+" left the chat"},noMoreParticipants:function(){return"No active participant left. Chat disabled."},collapse:function(e,t){return(e.senderToNameMap[t.id]||t.id)+" "+(t.collapsed?"mini":"maxi")+"mized the chat"},textareaPlaceholder:function(e){return e.useSubmitEnter?"Type something and press enter to send":"Type something"},submitButton:"Send",isTyping:"is typing..."},n.version="1.5.0",n.description="Offers a uni-/bi-directional communication interface between players, or between players and the server.",n.title="Chat",n.className="chat",n.panel=!1,n.dependencies={JSUS:{}},n.prototype.init=function(n){var r,i,s,o,u;n=n||{},u=this,this.receiverOnly=!!n.receiverOnly,r=n.preprocessMsg;if("function"==typeof r)this.preprocessMsg=r;else if(r)throw new TypeError("Chat.init: preprocessMsg must be function or undefined. Found: "+r);r=n.chatEvent;if(r){if("string"!=typeof r)throw new TypeError("Chat.init: chatEvent must be a non-empty string or undefined. Found: "+r);this.chatEvent=n.chatEvent}else this.chatEvent="CHAT";this.storeMsgs=!!n.storeMsgs,this.storeMsgs&&(this.db||(this.db=new t)),this.useSubmitButton="undefined"==typeof n.useSubmitButton?J.isMobileAgent():!!n.useSubmitButton,this.useSubmitEnter="undefined"==typeof n.useSubmitEnter?!0:!!n.useSubmitEnter,r=n.participants;if(!J.isArray(r)||!r.length)throw new TypeError("Chat.init: participants must be a non-empty array. Found: "+r);this.recipientsIds=new Array(r.length),this.recipientsIdsQuitted=[],this.recipientToSenderMap={},this.recipientToNameMap={},this.senderToNameMap={},this.senderToRecipientMap={};for(i=0;i"+this.title+""),this.stats.unread++)),!0)},n.prototype.disable=function(){this.submitButton&&(this.submitButton.disabled=!0),this.textarea.disabled=!0,this.disabled=!0},n.prototype.enable=function(){this.submitButton&&(this.submitButton.disabled=!1),this.textarea.disabled=!1,this.disabled=!1},n.prototype.getValues=function(){var e;return e={participants:this.participants,totSent:this.stats.sent,totReceived:this.stats.received,totUnread:this.stats.unread,initialMsg:this.initialMsg},this.db&&(e.msgs=this.db.fetch()),e},n.prototype.sendMsg=function(t){var n,r,i;if(this.isDisabled()){e.warn("Chat is disable, msg not sent.");return}if("object"==typeof t){if("undefined"!=typeof t.msg&&"object"==typeof t.msg)throw new TypeError("Chat.sendMsg: opts.msg cannot be object. Found: "+t.msg)}else if("undefined"==typeof t)t={msg:this.readTextarea()};else{if("string"!=typeof t&&"number"!=typeof t)throw new TypeError("Chat.sendMsg: opts must be string, number, object, or undefined. Found: "+t);t={msg:t}}t.msg=this.renderMsg(t,"outgoing");if(t.msg===""){e.warn("Chat: message has no text, not sent.");return}r=t.recipients||this.recipientsIds;if(r.length===0){e.warn("Chat: empty recipient list, message not sent.");return}n=r.length===1?r[0]:r,e.say(this.chatEvent,n,t),t.silent||(i=this,this.writeMsg("outgoing",t),i.textarea&&setTimeout(function(){i.textarea.value=""})),this.amTypingTimeout&&(clearTimeout(this.amTypingTimeout),this.amTypingTimeout=null)}}(node),function(e){"use strict";function n(e){var t=this;this.options=null,this.table=null,this.sc=null,this.fp=null,this.canvas=null,this.changes=[],this.onChange=null,this.onChangeCb=function(e,n){"undefined"==typeof n&&(n=!1),e||(t.sc?e=t.sc.getValues():e=i.random()),t.draw(e,n)},this.timeFrom="step",this.features=null}function r(e,t){this.canvas=new W.Canvas(e),this.scaleX=e.width/n.width,this.scaleY=e.height/n.heigth,this.face=null}function i(e,t){var n;if("undefined"==typeof e)for(n in i.defaults)i.defaults.hasOwnProperty(n)&&(n==="color"?this.color="red":n==="lineWidth"?this.lineWidth=1:n==="scaleX"?this.scaleX=1:n==="scaleY"?this.scaleY=1:this[n]=i.defaults[n].min+Math.random()*i.defaults[n].range);else{if("object"!=typeof e)throw new TypeError("FaceVector constructor: faceVector must be object or undefined.");this.scaleX=e.scaleX||1,this.scaleY=e.scaleY||1,this.color=e.color||"green",this.lineWidth=e.lineWidth||1,t=t||i.defaults;for(n in t)t.hasOwnProperty(n)&&(e.hasOwnProperty(n)?this[n]=e[n]:this[n]=t?t[n]:i.defaults[n].value)}}var t=W.Table;e.widgets.register("ChernoffFaces",n),n.version="0.6.2",n.description="Display parametric data in the form of a Chernoff Face.",n.title="ChernoffFaces",n.className="chernofffaces",n.dependencies={JSUS:{},Table:{},Canvas:{},SliderControls:{}},n.FaceVector=i,n.FacePainter=r,n.width=100,n.height=100,n.onChange="CF_CHANGE",n.prototype.init=function(t){this.options=t,t.features?this.features=new i(t.features):this.features||(this.features=i.random()),this.fp&&this.fp.draw(this.features),t.onChange===!1||t.onChange===null?this.onChange&&(e.off(this.onChange,this.onChangeCb),this.onChange=null):(this.onChange="undefined"==typeof t.onChange?n.onChange:t.onChange,e.on(this.onChange,this.onChangeCb))},n.prototype.getCanvas=function(){return this.canvas},n.prototype.buildHTML=function(){var n,r,s,o;if(this.table)return;o=this.options,s={},this.id&&(s.id=this.id),"string"==typeof o.className?s.className=o.className:o.className!==!1&&(s.className="cf_table"),this.table=new t(s),this.canvas||this.buildCanvas();if("undefined"==typeof o.controls||o.controls)r=J.mergeOnKey(i.defaults,this.features,"value"),n={id:"cf_controls",features:r,onChange:this.onChange,submit:"Send"},"object"==typeof o.controls?this.sc=o.controls:this.sc=e.widgets.get("SliderControls",n);this.sc?this.table.addRow([{content:this.sc,id:this.id+"_td_controls"},{content:this.canvas,id:this.id+"_td_cf"}]):this.table.add({content:this.canvas,id:this.id+"_td_cf"}),this.table.parse()},n.prototype.buildCanvas=function(){var e;this.canvas||(e=this.options,e.canvas||(e.canvas={},"undefined"!=typeof e.height&&(e.canvas.height=e.height),"undefined"!=typeof e.width&&(e.canvas.width=e.width)),this.canvas=W.get("canvas",e.canvas),this.canvas.id="ChernoffFaces_canvas",this.fp=new r(this.canvas),this.fp.draw(this.features))},n.prototype.append=function(){this.table||this.buildHTML(),this.bodyDiv.appendChild(this.table.table)},n.prototype.draw=function(t,n){var r;if("object"!=typeof t)throw new TypeError("ChernoffFaces.draw: features must be object.");this.options.trackChanges&&("string"==typeof this.timeFrom?r=e.timer.getTimeSince(this.timeFrom):r=Date.now?Date.now():(new Date).getTime(),this.changes.push({time:r,change:t})),this.features=t instanceof i?t:new i(t,this.features),this.fp.redraw(this.features),this.sc&&n!==!1&&(this.sc.init({features:J.mergeOnKey(i.defaults,t,"value")}),this.sc.refresh())},n.prototype.getValues=function(e){return e&&e.changes?{changes:this.changes,cf:this.features}:this.fp.face},n.prototype.randomize=function(){var e;return e=i.random(),this.fp.redraw(e),this.sc&&(this.sc.init({features:J.mergeOnValue(i.defaults,e),onChange:this.onChange}),this.sc.refresh()),!0},r.prototype.draw=function(e,t,n){if(!e)return;this.face=e,this.fit2Canvas(e),this.canvas.scale(e.scaleX,e.scaleY),t=t||this.canvas.centerX,n=n||this.canvas.centerY,this.drawHead(e,t,n),this.drawEyes(e,t,n),this.drawPupils(e,t,n),this.drawEyebrow(e,t,n),this.drawNose(e,t,n),this.drawMouth(e,t,n)},r.prototype.redraw=function(e,t,n){this.canvas.clear(),this.draw(e,t,n)},r.prototype.scale=function(e,t){this.canvas.scale(this.scaleX,this.scaleY)},r.prototype.fit2Canvas=function(e){var t;if(!this.canvas){console.log("No canvas found");return}this.canvas.width>this.canvas.height?t=this.canvas.width/e.head_radius*e.head_scale_x:t=this.canvas.height/e.head_radius*e.head_scale_y,e.scaleX=t/2,e.scaleY=t/2},r.prototype.drawHead=function(e,t,n){var r=e.head_radius;this.canvas.drawOval({x:t,y:n,radius:r,scale_x:e.head_scale_x,scale_y:e.head_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyes=function(e,t,n){var i=r.computeFaceOffset(e,e.eye_height,n),s=e.eye_spacing,o=e.eye_radius;this.canvas.drawOval({x:t-s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawPupils=function(e,t,n){var i=e.pupil_radius,s=e.eye_spacing,o=r.computeFaceOffset(e,e.eye_height,n);this.canvas.drawOval({x:t-s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyebrow=function(e,t,n){var i=r.computeEyebrowOffset(e,n),s=e.eyebrow_spacing,o=e.eyebrow_length,u=e.eyebrow_angle;this.canvas.drawLine({x:t-s,y:i,length:o,angle:u,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawLine({x:t+s,y:i,length:0-o,angle:-u,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawNose=function(e,t,n){var i=r.computeFaceOffset(e,e.nose_height,n),s=t+e.nose_width/2,o=i+e.nose_length,u=s-e.nose_width,a=o;this.canvas.ctx.lineWidth=e.lineWidth,this.canvas.ctx.strokeStyle=e.color,this.canvas.ctx.save(),this.canvas.ctx.beginPath(),this.canvas.ctx.moveTo(t,i),this.canvas.ctx.lineTo(s,o),this.canvas.ctx.lineTo(u,a),this.canvas.ctx.stroke(),this.canvas.ctx.restore()},r.prototype.drawMouth=function(e,t,n){var i=r.computeFaceOffset(e,e.mouth_height,n),s=t-e.mouth_width/2,o=t+e.mouth_width/2,u=i-e.mouth_top_y,a=i+e.mouth_bottom_y;this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,u,o,i),this.canvas.ctx.stroke(),this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,a,o,i),this.canvas.ctx.stroke()},r.computeFaceOffset=function(e,t,n){n=n||0;var r=n-e.head_radius+e.head_radius*2*t;return r},r.computeEyebrowOffset=function(e,t){t=t||0;var n=2;return r.computeFaceOffset(e,e.eye_height,t)-n-e.eyebrow_eyedistance},i.defaults={head_radius:{min:10,max:100,step:.01,value:30,label:"Face radius"},head_scale_x:{min:.2,max:2,step:.01,value:.5,label:"Scale head horizontally"},head_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale head vertically"},eye_height:{min:.1,max:.9,step:.01,value:.4,label:"Eye height"},eye_radius:{min:2,max:30,step:.01,value:5,label:"Eye radius"},eye_spacing:{min:0,max:50,step:.01,value:10,label:"Eye spacing"},eye_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale eyes horizontally"},eye_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale eyes vertically"},pupil_radius:{min:1,max:9,step:.01,value:1,label:"Pupil radius"},pupil_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale pupils horizontally"},pupil_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale pupils vertically"},eyebrow_length:{min:1,max:30,step:.01,value:10,label:"Eyebrow length"},eyebrow_eyedistance:{min:.3,max:10,step:.01,value:3,label:"Eyebrow from eye"},eyebrow_angle:{min:-2,max:2,step:.01,value:-0.5,label:"Eyebrow angle"},eyebrow_spacing:{min:0,max:20,step:.01,value:5,label:"Eyebrow spacing"},nose_height:{min:.4,max:1,step:.01,value:.4,label:"Nose height"},nose_length:{min:.2,max:30,step:.01,value:15,label:"Nose length"},nose_width:{min:0,max:30,step:.01,value:10,label:"Nose width"},mouth_height:{min:.2,max:2,step:.01,value:.75,label:"Mouth height"},mouth_width:{min:2,max:100,step:.01,value:20,label:"Mouth width"},mouth_top_y:{min:-10,max:30,step:.01,value:-2,label:"Upper lip"},mouth_bottom_y:{min:-10,max:30,step:.01,value:20,label:"Lower lip"},scaleX:{min:0,max:20,step:.01,value:.2,label:"Scale X"},scaleY:{min:0,max:20,step:.01,value:.2,label:"Scale Y"},color:{min:0,max:20,step:.01,value:.2,label:"color"},lineWidth:{min:0,max:20,step:.01,value:.2,label:"lineWidth"}},function(e){var t;for(t in e)e.hasOwnProperty(t)&&(e[t].range=e[t].max-e[t].min)}(i.defaults),i.random=function(){return console.log("*** FaceVector.random is deprecated. Use new FaceVector() instead."),new i}}(node),function(e){"use strict";function n(n){this.options=n,this.id=n.id,this.table=new t({id:"cf_table"}),this.root=n.root||document.createElement("div"),this.root.id=this.id,this.sc=e.widgets.get("Controls.Slider"),this.fp=null,this.canvas=null,this.dims=null,this.change="CF_CHANGE";var r=this;this.changeFunc=function(){r.draw(r.sc.getAllValues())},this.features=null,this.controls=null}function r(e,t){this.canvas=new W.Canvas(e),this.scaleX=e.width/n.defaults.canvas.width,this.scaleY=e.height/n.defaults.canvas.heigth}function i(e){e=e||{},this.scaleX=e.scaleX||1,this.scaleY=e.scaleY||1,this.color=e.color||"green",this.lineWidth=e.lineWidth||1;for(var t in i.defaults)i.defaults.hasOwnProperty(t)&&(e.hasOwnProperty(t)?this[t]=e[t]:this[t]=i.defaults[t].value)}var t=W.Table;e.widgets.register("ChernoffFacesSimple",n),n.defaults={},n.defaults.id="ChernoffFaces",n.defaults.canvas={},n.defaults.canvas.width=100,n.defaults.canvas.heigth=100,n.version="0.4",n.description="Display parametric data in the form of a Chernoff Face.",n.dependencies={JSUS:{},Table:{},Canvas:{},"Controls.Slider":{}},n.FaceVector=i,n.FacePainter=r,n.prototype.init=function(t){this.id=t.id||this.id;var s=this.id+"_";this.features=t.features||this.features||i.random(),this.controls="undefined"!=typeof t.controls?t.controls:!0;var o=t.idCanvas?t.idCanvas:s+"canvas";this.dims={width:t.width?t.width:n.defaults.canvas.width,height:t.height?t.height:n.defaults.canvas.heigth},this.canvas=W.getCanvas(o,this.dims),this.fp=new r(this.canvas),this.fp.draw(new i(this.features));var u={id:"cf_controls",features:J.mergeOnKey(i.defaults,this.features,"value"),change:this.change,fieldset:{id:this.id+"_controls_fieldest",legend:this.controls.legend||"Controls"},submit:"Send"};this.sc=e.widgets.get("Controls.Slider",u),this.controls&&this.table.add(this.sc),"undefined"==typeof t.change?e.on(this.change,this.changeFunc):(t.change?e.on(t.change,this.changeFunc):e.removeListener(this.change,this.changeFunc),this.change=t.change),this.table.add(this.canvas),this.table.parse(),this.root.appendChild(this.table.table)},n.prototype.getRoot=function(){return this.root},n.prototype.getCanvas=function(){return this.canvas},n.prototype.append=function(e){return e.appendChild(this.root),this.table.parse(),this.root},n.prototype.listeners=function(){},n.prototype.draw=function(e){if(!e)return;var t=new i(e);this.fp.redraw(t),this.sc.init({features:J.mergeOnKey(i.defaults,e,"value")}),this.sc.refresh()},n.prototype.getAllValues=function(){return this.fp.face},n.prototype.randomize=function(){var e=i.random();this.fp.redraw(e);var t={features:J.mergeOnKey(i.defaults,e,"value"),change:this.change};return this.sc.init(t),this.sc.refresh(),!0},r.prototype.draw=function(e,t,n){if(!e)return;this.face=e,this.fit2Canvas(e),this.canvas.scale(e.scaleX,e.scaleY),t=t||this.canvas.centerX,n=n||this.canvas.centerY,this.drawHead(e,t,n),this.drawEyes(e,t,n),this.drawPupils(e,t,n),this.drawEyebrow(e,t,n),this.drawNose(e,t,n),this.drawMouth(e,t,n)},r.prototype.redraw=function(e,t,n){this.canvas.clear(),this.draw(e,t,n)},r.prototype.scale=function(e,t){this.canvas.scale(this.scaleX,this.scaleY)},r.prototype.fit2Canvas=function(e){var t;if(!this.canvas){console.log("No canvas found");return}this.canvas.width>this.canvas.height?t=this.canvas.width/e.head_radius*e.head_scale_x:t=this.canvas.height/e.head_radius*e.head_scale_y,e.scaleX=t/2,e.scaleY=t/2},r.prototype.drawHead=function(e,t,n){var r=e.head_radius;this.canvas.drawOval({x:t,y:n,radius:r,scale_x:e.head_scale_x,scale_y:e.head_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyes=function(e,t,n){var i=r.computeFaceOffset(e,e.eye_height,n),s=e.eye_spacing,o=e.eye_radius;this.canvas.drawOval({x:t-s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawPupils=function(e,t,n){var i=e.pupil_radius,s=e.eye_spacing,o=r.computeFaceOffset(e,e.eye_height,n);this.canvas.drawOval({x:t-s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyebrow=function(e,t,n){var i=r.computeEyebrowOffset(e,n),s=e.eyebrow_spacing,o=e.eyebrow_length,u=e.eyebrow_angle;this.canvas.drawLine({x:t-s,y:i,length:o,angle:u,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawLine({x:t+s,y:i,length:0-o,angle:-u,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawNose=function(e,t,n){var i=r.computeFaceOffset(e,e.nose_height,n),s=t+e.nose_width/2,o=i+e.nose_length,u=s-e.nose_width,a=o;this.canvas.ctx.lineWidth=e.lineWidth,this.canvas.ctx.strokeStyle=e.color,this.canvas.ctx.save(),this.canvas.ctx.beginPath(),this.canvas.ctx.moveTo(t,i),this.canvas.ctx.lineTo(s,o),this.canvas.ctx.lineTo(u,a),this.canvas.ctx.stroke(),this.canvas.ctx.restore()},r.prototype.drawMouth=function(e,t,n){var i=r.computeFaceOffset(e,e.mouth_height,n),s=t-e.mouth_width/2,o=t+e.mouth_width/2,u=i-e.mouth_top_y,a=i+e.mouth_bottom_y;this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,u,o,i),this.canvas.ctx.stroke(),this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,a,o,i),this.canvas.ctx.stroke()},r.computeFaceOffset=function(e,t,n){n=n||0;var r=n-e.head_radius+e.head_radius*2*t;return r},r.computeEyebrowOffset=function(e,t){t=t||0;var n=2;return r.computeFaceOffset(e,e.eye_height,t)-n-e.eyebrow_eyedistance},i.defaults={head_radius:{min:10,max:100,step:.01,value:30,label:"Face radius"},head_scale_x:{min:.2,max:2,step:.01,value:.5,label:"Scale head horizontally"},head_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale head vertically"},eye_height:{min:.1,max:.9,step:.01,value:.4,label:"Eye height"},eye_radius:{min:2,max:30,step:.01,value:5,label:"Eye radius"},eye_spacing:{min:0,max:50,step:.01,value:10,label:"Eye spacing"},eye_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale eyes horizontally"},eye_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale eyes vertically"},pupil_radius:{min:1,max:9,step:.01,value:1,label:"Pupil radius"},pupil_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale pupils horizontally"},pupil_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale pupils vertically"},eyebrow_length:{min:1,max:30,step:.01,value:10,label:"Eyebrow length"},eyebrow_eyedistance:{min:.3,max:10,step:.01,value:3,label:"Eyebrow from eye"},eyebrow_angle:{min:-2,max:2,step:.01,value:-0.5,label:"Eyebrow angle"},eyebrow_spacing:{min:0,max:20,step:.01,value:5,label:"Eyebrow spacing"},nose_height:{min:.4,max:1,step:.01,value:.4,label:"Nose height"},nose_length:{min:.2,max:30,step:.01,value:15,label:"Nose length"},nose_width:{min:0,max:30,step:.01,value:10,label:"Nose width"},mouth_height:{min:.2,max:2,step:.01,value:.75,label:"Mouth height"},mouth_width:{min:2,max:100,step:.01,value:20,label:"Mouth width"},mouth_top_y:{min:-10,max:30,step:.01,value:-2,label:"Upper lip"},mouth_bottom_y:{min:-10,max:30,step:.01,value:20,label:"Lower lip"}},i.random=function(){var e={};for(var t in i.defaults)i.defaults.hasOwnProperty(t)&&(J.inArray(t,["color","lineWidth","scaleX","scaleY"])||(e[t]=i.defaults[t].min+Math.random()*i.defaults[t].max));return e.scaleX=1,e.scaleY=1,e.color="green",e.lineWidth=1,new i(e)},i.prototype.shuffle=function(){for(var e in this)this.hasOwnProperty(e)&&i.defaults.hasOwnProperty(e)&&e!=="color"&&(this[e]=i.defaults[e].min+Math.random()*i.defaults[e].max)},i.prototype.distance=function(e){return i.distance(this,e)},i.distance=function(e,t){var n=0,r;for(var i in e)e.hasOwnProperty(i)&&(r=e[i]-t[i],n+=r*r);return Math.sqrt(n)},i.prototype.toString=function(){var e="Face: ";for(var t in this)this.hasOwnProperty(t)&&(e+=t+" "+this[t]);return e}}(node),function(e){"use strict";function t(){this.dl=null,this.mainText=null,this.spanMainText=null,this.forms=null,this.formsById=null,this.order=null,this.shuffleForms=null,this.group=null,this.groupOrder=null,this.formsOptions={title:!1,frame:!1,storeRef:!1},this.simplify=null,this.freeText=null,this.textarea=null,this.required=null}e.widgets.register("ChoiceManager",t),t.version="1.4.1",t.description="Groups together and manages a set of survey forms (e.g., ChoiceTable).",t.title=!1,t.className="choicemanager",t.dependencies={},t.prototype.init=function(e){var t;"undefined"==typeof e.shuffleForms?t=!1:t=!!e.shuffleForms,this.shuffleForms=t;if("string"==typeof e.group||"number"==typeof e.group)this.group=e.group;else if("undefined"!=typeof e.group)throw new TypeError("ChoiceManager.init: options.group must be string, number or undefined. Found: "+e.group);if("number"==typeof e.groupOrder)this.groupOrder=e.groupOrder;else if("undefined"!=typeof e.group)throw new TypeError("ChoiceManager.init: options.groupOrder must be number or undefined. Found: "+e.groupOrder);if("string"==typeof e.mainText)this.mainText=e.mainText;else if("undefined"!=typeof e.mainText)throw new TypeError("ChoiceManager.init: options.mainText must be string or undefined. Found: "+e.mainText);if("undefined"!=typeof e.formsOptions){if("object"!=typeof e.formsOptions)throw new TypeError("ChoiceManager.init: options.formsOptions must be object or undefined. Found: "+e.formsOptions);if(e.formsOptions.hasOwnProperty("name"))throw new Error("ChoiceManager.init: options.formsOptions cannot contain property name. Found: "+e.formsOptions);this.formsOptions=J.mixin(this.formsOptions,e.formsOptions)}this.freeText="string"==typeof e.freeText?e.freeText:!!e.freeText,"undefined"!=typeof e.required&&(this.required=!!e.required),this.simplify=!!e.simplify,"undefined"!=typeof e.forms&&this.setForms(e.forms)},t.prototype.setForms=function(t){var n,r,i,s,o,u;if("function"==typeof t){o=t.call(e.game);if(!J.isArray(o))throw new TypeError("ChoiceManager.setForms: forms is a callback, but did not returned an array. Found: "+o)}else{if(!J.isArray(t))throw new TypeError("ChoiceManager.setForms: forms must be array or function. Found: "+t);o=t}s=o.length;if(!s)throw new Error("ChoiceManager.setForms: forms is an empty array.");r={},t=new Array(s),i=-1;for(;++i 1. Found: "+n)}this.selectMultiple=n,n&&(this.selected=[],this.currentChoice=[]);if("number"==typeof e.requiredChoice){if(!J.isInt(e.requiredChoice,0))throw new Error("ChoiceTable.init: if number, requiredChoice must a positive integer. Found: "+e.requiredChoice);if("number"==typeof this.selectMultiple&&e.requiredChoice>this.selectMultiple)throw new Error("ChoiceTable.init: requiredChoice cannot be larger than selectMultiple. Found: "+e.requiredChoice+" > "+this.selectMultiple);this.requiredChoice=e.requiredChoice}else if("boolean"==typeof e.requiredChoice)this.requiredChoice=e.requiredChoice?1:null;else if("undefined"!=typeof e.requiredChoice)throw new TypeError("ChoiceTable.init: opts.requiredChoice be number, boolean or undefined. Found: "+e.requiredChoice);"undefined"!=typeof e.oneTimeClick&&(this.oneTimeClick=!!e.oneTimeClick);if("string"==typeof e.group||"number"==typeof e.group)this.group=e.group;else if("undefined"!=typeof e.group)throw new TypeError("ChoiceTable.init: opts.group must be string, number or undefined. Found: "+e.group);if("number"==typeof e.groupOrder)this.groupOrder=e.groupOrder;else if("undefined"!=typeof e.groupOrder)throw new TypeError("ChoiceTable.init: opts.groupOrder must be number or undefined. Found: "+e.groupOrder);if("function"==typeof e.listener)this.listener=function(t){e.listener.call(this,t)};else if("undefined"!=typeof e.listener)throw new TypeError("ChoiceTable.init: opts.listener must be function or undefined. Found: "+e.listener);if("function"==typeof e.onclick)this.onclick=e.onclick;else if("undefined"!=typeof e.onclick)throw new TypeError("ChoiceTable.init: opts.onclick must be function or undefined. Found: "+e.onclick);if("string"==typeof e.mainText)this.mainText=e.mainText;else if("undefined"!=typeof e.mainText)throw new TypeError("ChoiceTable.init: opts.mainText must be string or undefined. Found: "+e.mainText);if("string"==typeof e.hint||!1===e.hint)this.hint=e.hint,this.requiredChoice&&(this.hint+=" *");else{if("undefined"!=typeof e.hint)throw new TypeError("ChoiceTable.init: opts.hint must be a string, false, or undefined. Found: "+e.hint);this.hint=this.getText("autoHint")}if(e.timeFrom===!1||"string"==typeof e.timeFrom)this.timeFrom=e.timeFrom;else if("undefined"!=typeof e.timeFrom)throw new TypeError("ChoiceTable.init: opts.timeFrom must be string, false, or undefined. Found: "+e.timeFrom);if("string"==typeof e.separator)this.separator=e.separator;else if("undefined"!=typeof e.separator)throw new TypeError("ChoiceTable.init: opts.separator must be string, or undefined. Found: "+e.separator);n=this.id+this.separator.substring(0,this.separator.length-1);if(this.id.indexOf(this.separator)!==-1||n.indexOf(this.separator)!==-1)throw new Error("ChoiceTable.init: separator cannot be included in the id or in the concatenation (id + separator). Please specify the right separator option. Found: "+this.separator);if("string"==typeof e.left||"number"==typeof e.left)this.left=""+e.left;else if(J.isNode(e.left)||J.isElement(e.left))this.left=e.left;else if("undefined"!=typeof e.left)throw new TypeError("ChoiceTable.init: opts.left must be string, number, an HTML Element or undefined. Found: "+e.left);if("string"==typeof e.right||"number"==typeof e.right)this.right=""+e.right;else if(J.isNode(e.right)||J.isElement(e.right))this.right=e.right;else if("undefined"!=typeof e.right)throw new TypeError("ChoiceTable.init: opts.right must be string, number, an HTML Element or undefined. Found: "+e.right);if("undefined"==typeof e.className)this.className=t.className;else if(e.className===!1)this.className=!1;else if("string"==typeof e.className)this.className=t.className+" "+e.className;else{if(!J.isArray(e.className))throw new TypeError("ChoiceTable.init: opts.className must be string, array, or undefined. Found: "+e.className);this.className=[t.className].concat(e.className)}e.tabbable!==!1&&(this.tabbable=!0);if("function"==typeof e.renderer)this.renderer=e.renderer;else if("undefined"!=typeof e.renderer)throw new TypeError("ChoiceTable.init: opts.renderer must be function or undefined. Found: "+e.renderer);if("object"==typeof e.table)this.table=e.table;else if("undefined"!=typeof e.table&&!1!==e.table)throw new TypeError("ChoiceTable.init: opts.table must be object, false or undefined. Found: "+e.table);this.table=e.table,this.freeText="string"==typeof e.freeText?e.freeText:!!e.freeText;if("undefined"!=typeof e.choicesSetSize){if(!J.isInt(e.choicesSetSize,0))throw new Error("ChoiceTable.init: choicesSetSize must be undefined or an integer > 0. Found: "+e.choicesSetSize);if(this.left||this.right)throw new Error("ChoiceTable.init: choicesSetSize option cannot be specified when either left or right options are set.");this.choicesSetSize=e.choicesSetSize}"undefined"!=typeof e.choices&&this.setChoices(e.choices);if("undefined"!=typeof e.correctChoice){if(this.requiredChoice)throw new Error("ChoiceTable.init: cannot specify both opts requiredChoice and correctChoice");this.setCorrectChoice(e.correctChoice)}if("undefined"!=typeof e.disabledChoices){if(!J.isArray(e.disabledChoices))throw new Error("ChoiceTable.init: disabledChoices must be undefined or array. Found: "+e.disabledChoices);n=e.disabledChoices.length,n&&function(){for(var t=0;t=this.choicesSetSize)break}this.rightCell&&(i||(o=r(this,"right")),o.appendChild(this.rightCell)),t!==n&&e.call(this,t,n,i,s)}return function(){var t,n,r;if(!this.choicesCells)throw new Error("ChoiceTable.buildTable: choices not set, cannot build table. Id: "+this.id);n=this.orientation==="H",t=this.choicesCells.length,r="number"==typeof this.choicesSetSize,e.call(this,-1,t,n,r),this.enable()}}(),t.prototype.buildTableAndChoices=function(){var e,t,n,i,s;t=this.choices.length,this.choicesCells=new Array(t),e=-1,s=this.orientation==="H",s&&(n=r(this,"main"),this.left&&(i=this.renderSpecial("left",this.left),n.appendChild(i)));for(;++e=this.requiredChoice:this.currentChoice!==null;if("undefined"==typeof this.correctChoice)return null;e="undefined"==typeof e?!0:e,e&&this.attempts.push(this.currentChoice);if(!this.selectMultiple)return this.currentChoice===this.correctChoice;a=J.isArray(this.correctChoice)?this.correctChoice:[this.correctChoice],n=a.length,i=this.currentChoice.length;if(n!==i)return!1;t=-1,o=this.currentChoice.slice(0);for(;++tr)throw new Error("ChoiceTable.setValues: values array cannot be larger than max allowed set: "+s+" > "+r);r=e.values}for(;++i

If you need a copy of this consent form, you may print a copy of this page for your records.

",printBtn:"Print this page",consentTerms:"Do you understand and consent to these terms?",agree:"Yes, I agree",notAgree:"No, I do not agree",showHideConsent:function(e,t){return(t==="hide"?"Hide":"Show")+" Consent Form"}},t.prototype.init=function(t){t=t||{},this.consent=t.consent||e.game.settings.CONSENT;if(this.consent&&"object"!=typeof this.consent)throw new TypeError("Consent: consent must be object or undefined. Found: "+this.consent);this.showPrint=t.showPrint===!1?!1:!0},t.prototype.enable=function(){var e,t;if(this.notAgreed)return;e=W.gid("agree"),e&&(e.disabled=!1),t=W.gid("notAgree"),t&&(t.disabled=!1)},t.prototype.disable=function(){var e,t;if(this.notAgreed)return;e=W.gid("agree"),e&&(e.disabled=!0),t=W.gid("notAgree"),t&&(t.disabled=!0)},t.prototype.append=function(){var e,t;W.hide("notAgreed"),e=W.gid("consent"),t="",this.showPrint&&(t=this.getText("printText"),t+='

'),t+=""+this.getText("consentTerms")+"
",t+='
",e.innerHTML+=t,setTimeout(function(){W.adjustFrameHeight()})},t.prototype.listeners=function(){var t=this,n=this.consent;e.on("FRAME_LOADED",function(){var r,i,s,o;if(n)for(s in n)n.hasOwnProperty(s)&&(o=s.toLowerCase(),o=o.replace(new RegExp("_","g"),"-"),W.setInnerHTML(o,n[s]));r=W.gid("agree"),i=W.gid("notAgree");if(!r)throw new Error("Consent: agree button not found");if(!i)throw new Error("Consent: notAgree button not found");r.onclick=function(){e.done({consent:!0})},i.onclick=function(){var n,s;s=confirm(t.getText("areYouSure"));if(!s)return;e.emit("CONSENT_REJECTING"),t.notAgreed=!0,e.set({consent:!1,time:e.timer.getTimeSince("step"),timeup:!1}),r.disabled=!0,i.disabled=!0,r.onclick=null,i.onclick=null,e.socket.disconnect(),W.hide("consent"),W.show("notAgreed"),n=W.gid("show-consent"),n&&(n.onclick=function(){var e,n;e=W.toggle("consent"),n=e.style.display===""?"hide":"show",this.innerHTML=t.getText("showHideConsent",n)}),e.emit("CONSENT_REJECTED")}})}}(node),function(e){"use strict";function t(){this.mainText=null,this.content=null,this.hint=null}e.widgets.register("ContentBox",t),t.version="0.2.0",t.description="Simply displays some content",t.title=!1,t.panel=!1,t.className="contentbox",t.dependencies={},t.prototype.init=function(e){if("string"==typeof e.mainText)this.mainText=e.mainText;else if("undefined"!=typeof e.mainText)throw new TypeError("ContentBox.init: mainText must be string or undefined. Found: "+e.mainText);if("string"==typeof e.content)this.content=e.content;else if("undefined"!=typeof e.content)throw new TypeError("ContentBox.init: content must be string or undefined. Found: "+e.content);if("string"==typeof e.hint)this.hint=e.hint;else if("undefined"!=typeof e.hint)throw new TypeError("ContentBox.init: hint must be string or undefined. Found: "+e.hint)},t.prototype.append=function(){this.mainText&&W.append("span",this.bodyDiv,{className:"contentbox-maintext",innerHTML:this.mainText}),this.content&&W.append("div",this.bodyDiv,{className:"contentbox-content",innerHTML:this.content}),this.hint&&W.append("span",this.bodyDiv,{className:"contentbox-hint",innerHTML:this.hint})}}(node),function(e){"use strict";function t(e){this.options=e,this.listRoot=null,this.submit=null,this.changeEvent="Controls_change",this.hasChanged=!1}function n(e){t.call(this,e)}function r(e){t.call(this,e)}function i(e){t.call(this,e),this.groupName="undefined"!=typeof e.name?e.name:W.generateUniqueId(),this.radioElem=null}e.widgets.register("Controls",t),t.version="0.5.1",t.description="Wraps a collection of user-inputs controls.",t.title="Controls",t.className="controls",t.prototype.add=function(e,t,n){},t.prototype.getItem=function(e,t){},t.prototype.init=function(e){this.hasChanged=!1,"undefined"!=typeof e.change&&(e.change?this.changeEvent=e.change:this.changeEvent=!1),this.list=new W.List(e),this.listRoot=this.list.getRoot(),e.features&&(this.features=e.features,this.populate())},t.prototype.append=function(){var t=this,n="submit_Controls";this.list.parse(),this.bodyDiv.appendChild(this.listRoot),this.options.submit&&(this.options.submit.id&&(n=this.options.submit.id,this.option.submit=this.option.submit.name),this.submit=W.add("button",this.bodyDiv,J.merge(this.options.attributes,{id:n,innerHTML:this.options.submit})),this.submit.onclick=function(){t.options.change&&e.emit(t.options.change)})},t.prototype.parse=function(){return this.list.parse()},t.prototype.populate=function(){var t,n,r,i,s,o=this;for(t in this.features)this.features.hasOwnProperty(t)&&(r=this.features[t],n=t,r.id&&(n=r.id,delete r.id),i=document.createElement("div"),s=this.add(i,n,r),this.changeEvent&&(s.onchange=function(){e.emit(o.changeEvent)}),r.label&&W.add("label",i,{"for":s.id,innerHTML:r.label}),this.list.addDT(i))},t.prototype.listeners=function(){var t=this;e.on(this.changeEvent,function(){t.hasChanged=!0})},t.prototype.refresh=function(){var e,t;for(e in this.features)this.features.hasOwnProperty(e)&&(t=W.getElementById(e),t&&(t.value=this.features[e].value));return!0},t.prototype.getValues=function(){var e,t,n;e={};for(n in this.features)this.features.hasOwnProperty(n)&&(t=W.getElementById(n),t&&(e[n]=Number(t.value)));return e},t.prototype.highlight=function(e){return W.highlight(this.listRoot,e)},n.prototype.__proto__=t.prototype,n.prototype.constructor=n,n.version="0.2.2",n.description="Collection of Sliders.",n.title="Slider Controls",n.className="slidercontrols",n.dependencies={Controls:{}},e.widgets.register("SliderControls",n),n.prototype.add=function(e,t,n){return n=n||{},n.id=t,n.type="range",W.add("input",e,n)},n.prototype.getItem=function(e,t){return t=t||{},t.id=e,W.get("input",t)},r.prototype.__proto__=t.prototype,r.prototype.constructor=r,r.version="0.14",r.description="Collection of jQuery Sliders.",r.title="jQuery Slider Controls",r.className="jqueryslidercontrols",r.dependencies={jQuery:{},Controls:{}},e.widgets.register("jQuerySliderControls",r),r.prototype.add=function(e,t,n){var r=jQuery("
",{id:t}).slider(),i=r.appendTo(e);return i[0]},r.prototype.getItem=function(e,t){var n=jQuery("
",{id:e}).slider();return n},i.prototype.__proto__=t.prototype,i.prototype.constructor=i,i.version="0.1.2",i.description="Collection of Radio Controls.",i.title="Radio Controls",i.className="radiocontrols",i.dependencies={Controls:{}},e.widgets.register("RadioControls",i),i.prototype.populate=function(){var t,n,r,i,s;s=this,this.radioElem||(this.radioElem=document.createElement("radio"),this.radioElem.group=this.name||"radioGroup",this.radioElem.group=this.className||"radioGroup",this.bodyDiv.appendChild(this.radioElem));for(t in this.features)this.features.hasOwnProperty(t)&&(r=this.features[t],n=t,r.id&&(n=r.id,delete r.id),i=this.add(this.radioElem,n,r),this.changeEvent&&(i.onchange=function(){e.emit(s.changeEvent)}),this.list.addDT(i))},i.prototype.add=function(e,t,n){var r;return"undefined"==typeof n.name&&(n.name=this.groupName),n.id=t,n.type="radio",r=W.add("input",e,n),r.appendChild(document.createTextNode(n.label)),r},i.prototype.getItem=function(e,t){return t=t||{},"undefined"==typeof t.name&&(t.name=this.groupName),t.id=e,t.type="radio",W.get("input",t)},i.prototype.getValues=function(){var e,t;for(e in this.features)if(this.features.hasOwnProperty(e)){t=W.getElementById(e);if(t.checked)return t.value}return!1}}(node),function(e){"use strict";function d(){this.input=null,this.placeholder=null,this.inputWidth=null,this.type=null,this.preprocess=null,this.validation=null,this.userValidation=null,this.validationSpeed=500,this.postprocess=null,this.oninput=null,this.params={},this.errorBox=null,this.mainText=null,this.hint=null,this.requiredChoice=null,this.required=null,this.timeBegin=null,this.timeEnd=null,this.checkbox=null,this.checkboxText=null,this.checkboxCb=null,this.orientation=null}function v(e,t){var n,r;if("string"==typeof e){e=e==="today"?new Date:new Date(e),r=e.getDate();if(!r)return!1}try{n={day:r||e.getDate(),month:e.getMonth()+1,year:e.getFullYear(),obj:e}}catch(i){return!1}return n.str=(t.dayPos?n.day+t.sep+n.month:n.month+t.sep+n.day)+t.sep,n.str+=t.yearDigits===2?n.year.substring(3,4):n.year,n}function m(e){switch(e){case"usStatesTerrByAbbrLow":return p||(m("usStatesTerrLow"),p=J.reverseObj(o,g)),p;case"usStatesTerrByAbbr":return u||(m("usStatesTerr"),u=J.reverseObj(o)),u;case"usTerrByAbbrLow":return l||(l=J.reverseObj(r,g)),l;case"usTerrByAbbr":return i||(i=J.reverseObj(r)),i;case"usStatesByAbbrLow":return c||(c=J.reverseObj(n,g)),c;case"usStatesByAbbr":return s||(s=J.reverseObj(n)),s;case"usStatesTerrLow":return h||(a||(a=y(n)),f||(f=y(r)),h=J.merge(a,f)),h;case"usStatesTerr":return o||(o=J.merge(n,r)),o;case"usStatesLow":return a||(a=y(n)),a;case"usStates":return n;case"usTerrLow":return f||(f=y(r)),f;case"usTerr":return r;default:throw new Error("getUsStatesList: unknown request: "+e)}}function g(e,t){return[e.toLowerCase(),t]}function y(e){var t,n;n={};for(t in e)e.hasOwnProperty(t)&&(n[t.toLowerCase()]=e[t]);return n}function b(e){return e.length===5&&J.isInt(e,0)}e.widgets.register("CustomInput",d),d.version="0.12.0",d.description="Creates a configurable input form",d.title=!1,d.panel=!1,d.className="custominput",d.types={text:!0,number:!0,"float":!0,"int":!0,date:!0,list:!0,us_city_state_zip:!0,us_state:!0,us_zip:!0};var t={",":"comma"," ":"space",".":"dot"},n={Alabama:"AL",Alaska:"AK",Arizona:"AZ",Arkansas:"AR",California:"CA",Colorado:"CO",Connecticut:"CT",Delaware:"DE",Florida:"FL",Georgia:"GA",Hawaii:"HI",Idaho:"ID",Illinois:"IL",Indiana:"IN",Iowa:"IA",Kansas:"KS",Kentucky:"KY",Louisiana:"LA",Maine:"ME",Maryland:"MD",Massachusetts:"MA",Michigan:"MI",Minnesota:"MN",Mississippi:"MS",Missouri:"MO",Montana:"MT",Nebraska:"NE",Nevada:"NV","New Hampshire":"NH","New Jersey":"NJ","New Mexico":"NM","New York":"NY","North Carolina":"NC","North Dakota":"ND",Ohio:"OH",Oklahoma:"OK",Oregon:"OR",Pennsylvania:"PA","Rhode Island":"RI","South Carolina":"SC","South Dakota":"SD",Tennessee:"TN",Texas:"TX",Utah:"UT",Vermont:"VT",Virginia:"VA",Washington:"WA","West Virginia":"WV",Wisconsin:"WI",Wyoming:"WY"},r={"American Samoa":"AS","District of Columbia":"DC","Federated States of Micronesia":"FM",Guam:"GU","Marshall Islands":"MH","Northern Mariana Islands":"MP",Palau:"PW","Puerto Rico":"PR","Virgin Islands":"VI"},i,s,o,u,a,f,l,c,h,p;d.texts={listErr:"Check that there are no empty items; do not end with the separator",listSizeErr:function(e,t){return e.params.fixedSize?e.params.minItems+" items required":t==="min"?"Too few items. Min: "+e.params.minItems:"Too many items. Max: "+e.params.maxItems},usStateAbbrErr:"Not a valid state abbreviation (must be 2 characters)",usStateErr:"Not a valid state (full name required)",usZipErr:"Not a valid ZIP code (must be 5 digits)",autoHint:function(e){var n,r;if(e.type==="list")r=t[e.params.listSep]||e.params.listSep,n="(if more than one, separate with "+r+")";else if(e.type==="us_state")n=e.params.abbr?"(Use 2-letter abbreviation)":"(Type the full name of the state)";else if(e.type==="us_zip")n="(Use 5-digit ZIP code)";else if(e.type==="us_city_state_zip")r=e.params.listSep,n="(Format: Town"+r+" State"+r+" ZIP code)";else if(e.type==="date")e.params.minDate&&e.params.maxDate?n="(Must be between "+e.params.minDate.str+" and "+e.params.maxDate.str+")":e.params.minDate?n="(Must be after "+e.params.minDate.str+")":e.params.maxDate?n="(Must be before "+e.params.maxDate.str+")":n="(Format: "+e.params.format+")";else if(e.type==="number"||e.type==="int"||e.type==="float")e.params.min&&e.params.max?n="(Must be between "+e.params.min+" and "+e.params.max+")":e.params.min?n="(Must be after "+e.params.min+")":e.params.max&&(n="(Must be before "+e.params.max+")");return e.required?(n||"")+" *":n||!1},numericErr:function(e){var t,n;return n=e.params,n.exactly?"Must enter "+n.lower:(t="Must be ",e.type==="float"?t+="a floating point number":e.type==="int"&&(t+="an integer"),n.between?(t+=" "+(n.leq?"≥ ":"<")+n.lower,t+=" and ",t+=(n.ueq?"≤ ":"> ")+n.upper):"undefined"!=typeof n.lower?t+=" "+(n.leq?"≥ ":"< ")+n.lower:"undefined"!=typeof n.upper&&(t+=" "+(n.ueq?"≤ ":"> ")+n.upper),t)},textErr:function(e,t){var n,r;return t==="num"?"Cannot contain numbers":(r=e.params,n="Must be ",r.exactly?n+="exactly "+(r.lower+1):r.between?n+="between "+r.lower+" and "+r.upper:"undefined"!=typeof r.lower?n+=" more than "+(r.lower-1):"undefined"!=typeof r.upper&&(n+=" less than "+(r.upper+1)),n+=" characters long",r.between&&(n+=" (extremes included)"),n+=". Current length: "+t,n)},dateErr:function(e,t){return t==="invalid"?"Date is invalid":t==="min"?"Date must be after "+e.params.minDate.str:t==="max"?"Date must be before "+e.params.maxDate.str:"Must follow format "+e.params.format},emptyErr:"Cannot be empty"},d.dependencies={JSUS:{}},d.prototype.init=function(e){var t,n,r,i,s;n=this,r="CustomInput.init: ";if("undefined"==typeof e.orientation)t="V";else{if("string"!=typeof e.orientation)throw new TypeError("CustomInput.init: orientation must be string, or undefined. Found: "+e.orientation);t=e.orientation.toLowerCase().trim();if(t==="h")t="H";else{if(t!=="v")throw new Error("CustomInput.init: unknown orientation: "+t);t="V"}}this.orientation=t,"undefined"!=typeof e.required&&(this.required=this.requiredChoice=!!e.required);if("undefined"!=typeof e.requiredChoice){if(!!this.required!=!!e.requiredChoice)throw new TypeError("CustomInput.init: required and requiredChoice are incompatible. Option requiredChoice will be deprecated.");this.required=this.requiredChoice=!!e.requiredChoice}"undefined"==typeof this.required&&(this.required=this.requiredChoice=!1);if(e.userValidation){if("function"!=typeof e.userValidation)throw new TypeError("CustomInput.init: userValidation must be function or undefined. Found: "+e.userValidation);this.userValidation=e.userValidation}if(e.type){if(!d.types[e.type])throw new Error(r+"type not supported: "+e.type);this.type=e.type}else this.type="text";if(e.validation){if("function"!=typeof e.validation)throw new TypeError(r+"validation must be function "+"or undefined. Found: "+e.validation);t=e.validation}else if(this.type==="number"||this.type==="float"||this.type==="int"||this.type==="text"){i=this.type==="text";if("undefined"!=typeof e.min){t=J.isNumber(e.min);if(!1===t)throw new TypeError(r+"min must be number or "+"undefined. Found: "+e.min);this.params.lower=e.min,this.params.leq=!0}if("undefined"!=typeof e.max){t=J.isNumber(e.max);if(!1===t)throw new TypeError(r+"max must be number or "+"undefined. Found: "+e.max);this.params.upper=e.max,this.params.ueq=!0}e.strictlyGreater&&(this.params.leq=!1),e.strictlyLess&&(this.params.ueq=!1);if("undefined"!=typeof this.params.lower&&"undefined"!=typeof this.params.upper){if(this.params.lower>this.params.upper)throw new TypeError(r+"min cannot be greater "+"than max. Found: "+e.min+"> "+e.max);if(this.params.lower===this.params.upper){if(!this.params.leq||!this.params.ueq)throw new TypeError(r+"min cannot be equal to "+"max when strictlyGreater or "+"strictlyLess are set. "+"Found: "+e.min);if(this.type==="int"||this.type==="text")if(J.isFloat(this.params.lower))throw new TypeError(r+"min cannot be a "+"floating point number "+"and equal to "+"max, when type "+'is not "float". Found: '+e.min);this.params.exactly=!0}else this.params.between=!0}if(i){this.params.noNumbers=e.noNumbers;if("undefined"!=typeof this.params.lower){if(this.params.lower<0)throw new TypeError(r+"min cannot be negative "+'when type is "text". Found: '+this.params.lower);this.params.leq||this.params.lower++}if("undefined"!=typeof this.params.upper){if(this.params.upper<0)throw new TypeError(r+"max cannot be negative "+'when type is "text". Found: '+this.params.upper);this.params.ueq||this.params.upper--}t=function(e){var t,r,i,s;r=n.params,t=e.length,i={value:e};if(r.noNumbers&&/\d/.test(e))s=n.getText("textErr","num");else{if(r.exactly)s=t!==r.lower;else if("undefined"!=typeof r.lower&&tr.upper)s=!0;s&&(s=n.getText("textErr",t))}return s&&(i.err=s),i},s=function(){var e,t;return e="undefined"!=typeof n.params.lower?n.params.lower+1:5,t="undefined"!=typeof n.params.upper?n.params.upper:e+5,J.randomString(J.randomInt(e,t))}}else t=function(){var e;return n.type==="float"?e=J.isFloat:n.type==="int"?e=J.isInt:e=J.isNumber,function(t){var r,i;return i=n.params,r=e(t,i.lower,i.upper,i.leq,i.ueq),r!==!1?{value:r}:{value:t,err:n.getText("numericErr")}}}(),s=function(){var e,t,r;return e=n.params,n.type==="float"?J.random():(t=0,"undefined"!=typeof e.lower&&(t=e.leq?e.lower-1:e.lower),"undefined"!=typeof e.upper?r=e.ueq?e.upper:e.upper-1:r=100+t,J.randomInt(t,r))};this.params.upper&&(this.params.upper<10?this.inputWidth="100px":this.params.upper<20&&(this.inputWidth="200px"))}else if(this.type==="date"){if("undefined"!=typeof e.format){if(e.format!=="mm-dd-yy"&&e.format!=="dd-mm-yy"&&e.format!=="mm-dd-yyyy"&&e.format!=="dd-mm-yyyy"&&e.format!=="mm.dd.yy"&&e.format!=="dd.mm.yy"&&e.format!=="mm.dd.yyyy"&&e.format!=="dd.mm.yyyy"&&e.format!=="mm/dd/yy"&&e.format!=="dd/mm/yy"&&e.format!=="mm/dd/yyyy"&&e.format!=="dd/mm/yyyy")throw new Error(r+"date format is invalid. Found: "+e.format);this.params.format=e.format}else this.params.format="mm/dd/yyyy";this.params.sep=this.params.format.charAt(2),t=this.params.format.split(this.params.sep),this.params.yearDigits=t[2].length,this.params.dayPos=t[0].charAt(0)==="d"?0:1,this.params.monthPos=this.params.dayPos?0:1,this.params.dateLen=t[2].length+6;if(e.minDate){t=v(e.minDate,this.params);if(!t)throw new Error(r+"minDate must be a Date object. "+"Found: "+e.minDate);this.params.minDate=t}if(e.maxDate){t=v(e.maxDate,this.params);if(!t)throw new Error(r+"maxDate must be a Date object. "+"Found: "+e.maxDate);if(this.params.minDate&&this.params.minDate.obj>t.obj)throw new Error(r+"maxDate cannot be prior to "+"minDate. Found: "+t.str+" < "+this.params.minDate.str);this.params.maxDate=t}this.params.yearDigits===2?this.inputWidth="100px":this.inputWidth="150px",this.placeholder=this.params.format,t=function(e){var t,r,i,s,o,u,a;t=n.params,r=e.split(t.sep);if(r.length!==3)return{err:n.getText("dateErr")};if(r[2].length!==t.yearDigits)return{err:n.getText("dateErr")};s={},t.yearDigits===2?(u=-1,a=100):(u=-1,a=1e4),i=J.isInt(r[2],u,a),i!==!1?s.year=i:s.err=!0,i=J.isInt(r[t.monthPos],1,12,1,1),i?s.month=i:s.err=!0,i===1||i===3||i===5||i===7||i===8||i===10||i===12?o=31:i!==2?o=30:o=s.year%4===0&&s.year%100!==0||s.year%400===0?29:28,s.month=i,i=J.isInt(r[t.dayPos],1,o,1,1),i?s.day=i:s.err=!0;if(s.err)s.err=n.getText("dateErr","invalid");else if(t.minDate||t.maxDate)i=new Date(e),t.minDate.obj&&t.minDate.obj>i?s.err=n.getText("dateErr","min"):t.maxDate.obj&&t.maxDate.objt)throw new TypeError(r+"maxItems must be larger "+"than minItems. Found: "+t+" < "+this.params.minItems);this.params.maxItems=t}}t=function(e){var t,r,i,s,o;e=e.split(n.params.listSep),r=e.length;if(!r)return e;s=n.params.itemValidation,t=0,i=e[0].trim();if(!i)return{err:n.getText("listErr")};if(s){o=s(i,1);if(o)return o}e[t++]=i;if(r>1){i=e[1].trim();if(!i)return{err:n.getText("listErr")};if(s){o=s(i,t+1);if(o)return o}e[t++]=i}if(r>2){i=e[2].trim();if(!i)return{err:n.getText("listErr")};if(s){o=s(i,t+1);if(o)return o}e[t++]=i}if(r>3)for(;tn.params.maxItems?{err:n.getText("listSizeErr","max")}:{value:e}},this.type==="us_city_state_zip"?s=function(){var e;return e=n.params.listSep+" ",J.randomString(8)+e+J.randomKey(u)+e+(Math.floor(Math.random()*9e4)+1e4)}:s=function(e){var t,r,i,s,o,u;t=n.params,r=t.minItems||0,e.availableValues?(i=J.randomInt(r,e.availableValues.length),i--,u=J.sample(0,i-1)):(i=J.randomInt(r,t.maxItems||r+5),i--),o="";for(s=0;sthis.params.dateLen&&(e.value=e.value.substring(0,this.params.dateLen))}:(this.type==="list"||this.type==="us_city_state_zip")&&this.params.listSep.trim()!==""&&(this.preprocess=function(e){var t,r;r=e.value.length,t=n.params.listSep,r>1&&r===e.selectionStart&&e.value.charAt(r-1)===t&&e.value.charAt(r-2)!==t&&(e.value+=" ")}));if(e.postprocess){if("function"!=typeof e.postprocess)throw new TypeError(r+"postprocess must be function or "+"undefined. Found: "+e.postprocess);this.postprocess=e.postprocess}if(e.oninput){if("function"!=typeof e.oninput)throw new TypeError(r+"oninput must be function or "+"undefined. Found: "+e.oninput);this.oninput=e.oninput}if("undefined"!=typeof e.validationSpeed){t=J.isInt(e.valiadtionSpeed,0,undefined,!0);if(t===!1)throw new TypeError(r+"validationSpeed must a non-negative "+"number or undefined. Found: "+e.validationSpeed);this.validationSpeed=t}if(e.mainText){if("string"!=typeof e.mainText)throw new TypeError(r+"mainText must be string or "+"undefined. Found: "+e.mainText);this.mainText=e.mainText}if("undefined"!=typeof e.hint){if(!1!==e.hint&&"string"!=typeof e.hint)throw new TypeError(r+"hint must be a string, false, or "+"undefined. Found: "+e.hint);this.hint=e.hint,this.required&&(this.hint+=" *")}else this.hint=this.getText("autoHint");if(e.placeholder){if("string"!=typeof e.placeholder)throw new TypeError(r+"placeholder must be string or "+"undefined. Found: "+e.placeholder);this.placeholder=e.placeholder}if(e.width){if("string"!=typeof e.width)throw new TypeError(r+"width must be string or "+"undefined. Found: "+e.width);this.inputWidth=e.width}if(e.checkboxText){if("string"!=typeof e.checkboxText)throw new TypeError(r+"checkboxText must be string or "+"undefined. Found: "+e.checkboxText);this.checkboxText=e.checkboxText}if(e.checkboxCb){if(!this.checkboxText)throw new TypeError(r+"checkboxCb cannot be defined "+"if checkboxText is not defined");if("function"!=typeof e.checkboxCb)throw new TypeError(r+"checkboxCb must be function or "+"undefined. Found: "+e.checkboxCb);this.checkboxCb=e.checkboxCb}},d.prototype.append=function(){var t,n;t=this,this.mainText&&(this.spanMainText=W.append("span",this.bodyDiv,{className:"custominput-maintext",innerHTML:this.mainText})),this.hint&&W.append("span",this.spanMainText||this.bodyDiv,{className:"custominput-hint",innerHTML:this.hint}),this.input=W.append("input",this.bodyDiv),this.placeholder&&(this.input.placeholder=this.placeholder),this.inputWidth&&(this.input.style.width=this.inputWidth),this.errorBox=W.append("div",this.bodyDiv,{className:"errbox"}),this.input.oninput=function(){t.timeBegin?t.timeEnd=e.timer.getTimeSince("step"):t.timeEnd=t.timeBegin=e.timer.getTimeSince("step"),n&&clearTimeout(n),t.isHighlighted()&&t.unhighlight(),t.preprocess&&t.preprocess(t.input),n=setTimeout(function(){var e;t.validation&&(e=t.validation(t.input.value),e.err&&t.setError(e.err)),t.oninput&&t.oninput(e,t)},t.validationSpeed)},this.input.onclick=function(){t.isHighlighted()&&t.unhighlight()},this.checkboxText&&(this.checkbox=W.append("input",this.bodyDiv,{type:"checkbox",className:"custominput-checkbox"}),W.append("span",this.bodyDiv,{className:"custominput-checkbox-text",innerHTML:this.checkboxText}),this.checkboxCb&&J.addEvent(this.checkbox,"change",function(){t.checkboxCb(t.checkbox.checked,t)}))},d.prototype.setError=function(e){this.errorBox.innerHTML=e,this.highlight()},d.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("CustomInput.highlight: border must be string or undefined. Found: "+e);if(!this.input||this.highlighted)return;this.input.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},d.prototype.unhighlight=function(){if(!this.input||this.highlighted!==!0)return;this.input.style.border="",this.highlighted=!1,this.errorBox.innerHTML="",this.emit("unhighlighted")},d.prototype.disable=function(e){if(this.disabled)return;if(!this.isAppended())return;this.disabled=!0,this.input.disabled=!0,this.checkbox&&(!e||e.checkbox!==!1)&&(this.checkbox.disable=!0),this.emit("disabled")},d.prototype.enable=function(e){if(this.disabled!==!0)return;if(!this.isAppended())return;this.disabled=!1,this.input.disabled=!1,this.checkbox&&(!e||e.checkbox!==!1)&&(this.checkbox.disable=!1),this.emit("enabled")},d.prototype.reset=function(){this.input&&(this.input.value=""),this.isHighlighted()&&this.unhighlight(),this.timeBegin=this.timeEnd=null},d.prototype.getValues=function(e){var t,n;return e=e||{},t=this.input.value,e.valuesOnly?t:("undefined"==typeof e.markAttempt&&(e.markAttempt=!0),"undefined"==typeof e.highlight&&(e.highlight=!0),t=this.validation?this.validation(t):{value:t},n=!t.err,t.timeBegin=this.timeBegin,t.timeEnd=this.timeEnd,this.postprocess&&(t.value=this.postprocess(t.value,n)),n?(e.markAttempt&&(t.isCorrect=!0),e.reset&&this.reset()):(e.highlight&&this.setError(t.err),e.markAttempt&&(t.isCorrect=!1)),this.checkbox&&(t.checked=this.checkbox.checked),t.id=this.id,t)},d.prototype.setValues=function(e){var t,n;e=e||{};if("undefined"!=typeof e.value)t=e.value;else if("undefined"!=typeof e.values)t=e.values;else if(e.availableValues){n=e.availableValues;if(!J.isArray(n)||!n.length)throw new TypeError("CustomInput.setValues: availableValues must be a non-empty array or undefined. Found: "+n);if(this.type==="list"){if(n.lengththis.n&&(this.path.transition().duration(500).ease("linear").attr("transform","translate("+t(-1)+")"),this.data.shift())}}(node),function(e){"use strict";function n(){this.table=null,this.interval=null,this.intervalTime=1e3}var t=W.Table;e.widgets.register("DebugInfo",n),n.version="0.6.2",n.description="Display basic info a client's status.",n.title="Debug Info",n.className="debuginfo",n.dependencies={Table:{}},n.prototype.init=function(t){var n;"number"==typeof t.intervalTime&&(this.intervalTime=t.intervalTime),n=this,this.on("destroyed",function(){clearInterval(n.interval),n.interval=null,e.silly("DebugInfo destroyed.")})},n.prototype.append=function(){var e;this.table=new t,this.bodyDiv.appendChild(this.table.table),this.updateAll(),e=this,this.interval=setInterval(function(){e.updateAll()},this.intervalTime)},n.prototype.updateAll=function(){var t,n,r,i,s,o,u,a,f,l,c,h;if(!this.bodyDiv){e.err("DebugInfo.updateAll: bodyDiv not found.");return}h="-",r=h,n=h,t=e.game.getCurrentGameStage(),t&&(c=e.game.plot.getStep(t),r=c?c.id:"-",n=t.toString()),s=J.getKeyByValue(e.constants.stageLevels,e.game.getStageLevel()),o=J.getKeyByValue(e.constants.stateLevels,e.game.getStateLevel()),u=J.getKeyByValue(e.constants.windowLevels,W.getStateLevel()),i=e.player?e.player.id:h,a=e.errorManager.lastErr||h,l=e.game.settings&&e.game.settings.treatmentName?e.game.settings.treatmentName:h,f=e.socket.connected?"yes":"no",this.table.clear(!0),this.table.addRow(["Treatment: ",l]),this.table.addRow(["Connected: ",f]),this.table.addRow(["Player Id: ",i]),this.table.addRow(["Stage No: ",n]),this.table.addRow(["Stage Id: ",r]),this.table.addRow(["Stage Lvl: ",s]),this.table.addRow(["State Lvl: ",o]),this.table.addRow(["Players : ",e.game.pl.size()]),this.table.addRow(["Win Lvl: ",u]),this.table.addRow(["Win Loads: ",W.areLoading]),this.table.addRow(["Last Err: ",a]),this.table.parse()}}(node),function(e){"use strict";function t(){this.buttonsDiv=null,this.hiddenTypes={},this.counterIn=0,this.counterOut=0,this.counterLog=0,this.wall=null,this.wallDiv=null,this.origMsgInCb=null,this.origMsgOutCb=null,this.origLogCb=null}e.widgets.register("DebugWall",t),t.version="1.1.0",t.description="Intercepts incoming and outgoing messages, and logs and prints them numbered and timestamped. Warning! Modifies core functions, therefore its usage in production is not recommended.",t.title="Debug Wall",t.className="debugwall",t.dependencies={JSUS:{}},t.prototype.init=function(t){var n;n=this,t.msgIn!==!1&&(this.origMsgInCb=e.socket.onMessage,e.socket.onMessage=function(t){n.write("in",n.makeTextIn(t)),n.origMsgInCb.call(e.socket,t)}),t.msgOut!==!1&&(this.origMsgOutCb=e.socket.send,e.socket.send=function(t){n.write("out",n.makeTextOut(t)),n.origMsgOutCb.call(e.socket,t)}),t.log!==!1&&(this.origLogCb=e.log,e.log=function(t,r,i){n.write(r||"info",n.makeTextLog(t,r,i)),n.origLogCb.call(e,t,r,i)});if(t.hiddenTypes){if("object"!=typeof t.hiddenTypes)throw new TypeError("DebugWall.init: hiddenTypes must be object. Found: "+t.hiddenTypes);this.hiddenTypes=t.hiddenTypes}this.on("destroyed",function(){n.origLogCb&&(e.log=n.origLogCb),n.origMsgOutCb&&(e.socket.send=n.origMsgOutCb),n.origMsgInCb&&(e.socket.onMessage=n.origMsgInCb)})},t.prototype.append=function(){var e,t,n,r,i,s;this.buttonsDiv=W.add("div",this.bodyDiv,{className:"wallbuttonsdiv"}),i=W.add("div",this.buttonsDiv,{className:"btn-group",role:"group","aria-label":"Toggle visibility of messages on wall"}),W.add("input",i,{id:"debug-wall-incoming",className:"btn-check",autocomplete:"off",checked:!0,type:"checkbox"}),e=W.add("label",i,{className:"btn btn-outline-primary","for":"debug-wall-incoming",innerHTML:"Incoming"}),W.add("input",i,{id:"debug-wall-outgoing",className:"btn-check",autocomplete:"off",checked:!0,type:"checkbox"}),t=W.add("label",i,{className:"btn btn-outline-primary","for":"debug-wall-outgoing",innerHTML:"Outgoing"}),W.add("input",i,{id:"debug-wall-log",className:"btn-check",autocomplete:"off",checked:!0,type:"checkbox"}),n=W.add("label",i,{className:"btn btn-outline-primary","for":"debug-wall-log",innerHTML:"Log"}),r=this,W.add("button",this.buttonsDiv,{className:"btn btn-outline-danger me-2",innerHTML:"Clear"}).onclick=function(){r.clear()},this.buttonsDiv.appendChild(i),s=function(e){var t,n,i,s;s="wall_"+e,t=r.wall.getElementsByClassName(s);if(!t||!t.length)return;i=t[0].style.display===""?"none":"";for(n=0;na?(r=W.add("span",l,{className:u+"_click",innerHTML:n.substr(0,a)}),s=W.add("span",r,{className:u+"_extra",innerHTML:n.substr(a,n.length),id:"wall_"+t+"_"+o,style:{display:"none"}}),i=W.add("span",r,{className:u+"_dots",innerHTML:" ...",id:"wall_"+t+"_"+o}),r.onclick=function(){i.style.display==="none"?(i.style.display="",s.style.display="none"):(i.style.display="none",s.style.display="")}):r=W.add("span",l,{innerHTML:n}),this.wallDiv.scrollTop=this.wallDiv.scrollHeight):e.warn("Wall not appended, cannot write.")},t.prototype.makeTextIn=function(e){var t,n;return n=new Date(e.created),t=n.getHours()+":"+n.getMinutes()+":"+n.getSeconds()+":"+n.getMilliseconds(),t+=" | "+e.to+" | "+e.target+" | "+e.action+" | "+e.text+" | "+e.data,t},t.prototype.makeTextOut=function(e){var t;return t=e.from+" | "+e.target+" | "+e.action+" | "+e.text+" | "+e.data,t},t.prototype.makeTextLog=function(e){return e}}(node),function(e){"use strict";function t(){this.showStatus=null,this.showDiscBtn=null,this.statusSpan=null,this.disconnectBtn=null,this.userDiscFlag=null,this.ee=null,this.disconnectCb=null,this.connectCb=null}e.widgets.register("DisconnectBox",t),t.version="0.4.0",t.description="Monitors and handles disconnections",t.title=!1,t.panel=!1,t.className="disconnectbox",t.texts={leave:"Leave Task",left:"You Left",disconnected:"Disconnected!",connected:"Connected"},t.dependencies={},t.prototype.init=function(e){if(e.connectCb){if("function"!=typeof e.connectCb)throw new TypeError("DisconnectBox.init: connectCb must be function or undefined. Found: "+e.connectCb);this.connectCb=e.connectCb}if(e.disconnectCb){if("function"!=typeof e.disconnectCb)throw new TypeError("DisconnectBox.init: disconnectCb must be function or undefined. Found: "+e.disconnectCb);this.disconnectCb=e.disconnectCb}this.showDiscBtn=!!e.showDiscBtn,this.showStatus=!!e.showStatus},t.prototype.append=function(){var t,n;t=this,n=e.socket.isConnected(),this.showStatus&&(this.statusSpan=W.add("span",this.bodyDiv),this.updateStatus(n?"connected":"disconnected")),this.showDiscBtn&&(this.disconnectBtn=W.add("button",this.bodyDiv,{innerHTML:this.getText(n?"leave":"left"),className:"btn",style:{"margin-left":"10px"}}),n||(this.disconnectBtn.disabled=!0),this.disconnectBtn.onclick=function(){t.disconnectBtn.disabled=!0,t.userDiscFlag=!0,e.socket.disconnect()})},t.prototype.updateStatus=function(t){if(!this.statusSpan){e.warn("DisconnectBox.updateStatus: display disabled.");return}this.statusSpan.innerHTML=this.getText(t),this.statusSpan.className=t==="disconnected"?"text-danger":""},t.prototype.listeners=function(){var t;t=this,this.ee=e.getCurrentEventEmitter(),this.ee.on("SOCKET_DISCONNECT",function(){t.statusSpan&&t.updateStatus("disconnected"),t.disconnectBtn&&(t.disconnectBtn.disabled=!0,t.disconnectBtn.innerHTML=t.getText("left")),t.disconnectCb&&t.disconnectCb(t.userDiscFlag)}),this.ee.on("SOCKET_CONNECT",function(){t.statusSpan&&t.updateStatus("connected"),t.disconnectBtn&&(t.disconnectBtn.disabled=!1,t.disconnectBtn.innerHTML=t.getText("leave")),t.connectCb&&t.disconnectCb(),t.userDiscFlag=!1})}}(node),function(e){"use strict";function t(t){var n;n=this;if("object"==typeof t.button)this.button=t.button;else{if("undefined"!=typeof t.button)throw new TypeError("DoneButton constructor: options.button must be object or undefined. Found: "+t.button);this.button=document.createElement("input"),this.button.type="button"}this.button.onclick=function(){if(n.onclick&&!1===n.onclick())return;e.done()&&n.disable()},this.onclick=null,this.disableOnDisconnect=null,this.delayOnPlaying=800}e.widgets.register("DoneButton",t),t.version="1.1.0",t.description="Creates a button that if pressed emits node.done().",t.title=!1,t.className="donebutton",t.texts.done="Done",t.dependencies={JSUS:{}},t.prototype.init=function(e){var n;e=e||{};if("undefined"==typeof e.id)n=t.className;else if("string"==typeof e.id)n=e.id;else{if(!1!==e.id)throw new TypeError("DoneButton.init: id must be string, false, or undefined. Found: "+e.id);n=!1}n&&(this.button.id=n);if("undefined"==typeof e.className)n="btn btn-lg btn-primary";else if(e.className===!1)n="";else if("string"==typeof e.className)n=e.className;else{if(!J.isArray(e.className))throw new TypeError("DoneButton.init: className must be string, array, or undefined. Found: "+e.className);n=e.className.join(" ")}this.button.className=n,this.button.value="string"==typeof e.text?e.text:this.getText("done"),this.disableOnDisconnect="undefined"==typeof e.disableOnDisconnect?!0:!!e.disableOnDisconnect,n=e.delayOnPlaying;if("number"==typeof n)this.delayOnPlaying=n;else if("undefined"!=typeof n)throw new TypeError("DoneButton.init: delayOnPlaying must be number or undefined. Found: "+n);n=e.onclick;if(n){if("function"!=typeof n)throw new TypeError("DoneButton.init: onclick must function or undefined. Found: "+n);this.onclick=n}},t.prototype.append=function(){e.game.isReady()||(this.disabled=!0,this.button.disabled=!0),this.bodyDiv.appendChild(this.button)},t.prototype.listeners=function(){var t,n;t=this,e.on("PLAYING",function(){var r,i,s;i=e.game.getCurrentGameStage(),r=e.game.plot.getProperty(i,"donebutton"),r===!1||r&&r.enableOnPlaying===!1?t.disable():(r&&r.hasOwnProperty&&r.hasOwnProperty("delayOnPlaying")?s=r.delayOnPlaying:s=t.delayOnPlaying,s?setTimeout(function(){n||t.enable()},s):t.enable()),"string"==typeof r?t.button.value=r:r&&r.text&&(t.button.value=r.text)}),this.disableOnDisconnect&&(e.on("SOCKET_DISCONNECT",function(){t.isDisabled()||(t.disable(),n=!0)}),e.on("SOCKET_CONNECT",function(){n&&(t.isDisabled()&&t.enable(),n=!1)}))},t.prototype.updateText=function(t,n){var r,i;n&&(i=this,r=this.button.value,e.timer.setTimeout(function(){i.button.value=r},n)),this.button.value=t},t.prototype.disable=function(e){if(this.disabled)return;this.disabled=!0,this.button.disabled=!0,this.emit("disabled",e)},t.prototype.enable=function(e){if(!this.disabled)return;this.disabled=!1,this.button.disabled=!1,this.emit("enabled",e)}}(node),function(e){function t(){var t;t=this,this.id=null,this.mainText=null,this.labelText=null,this.placeHolder=null,this.choices=null,this.tag=null,this.menu=null,this.listener=function(n){var r,i;n=n||window.event,r=n.target||n.srcElement,t.currentChoice=r.value,t.currentChoice.length===0&&(t.currentChoice=null),"string"==typeof t.timeFrom?t.timeCurrentChoice=e.timer.getTimeSince(t.timeFrom):t.timeCurrentChoice=Date.now?Date.now():(new Date).getTime(),t.numberOfChanges++,t.isHighlighted()&&t.unhighlight(),i&&clearTimeout(i),i=setTimeout(function(){t.verifyChoice(),t.verifyChoice().err&&t.setError(t.verifyChoice().err)},t.validationSpeed),t.onchange&&t.onchange(t.currentChoice,t)},this.onchange=null,this.timeCurrentChoice=null,this.timeFrom="step",this.numberOfChanges=0,this.currentChoice=null,this.shuffleChoices=null,this.order=null,this.errorBox=null,this.correctChoice=null,this.requiredChoice=null,this.fixedChoice=null,this.inputWidth=null,this.validation=null,this.validationSpeed=500}e.widgets.register("Dropdown",t),t.version="0.1.0",t.description="Creates a configurable dropdown menu.",t.texts={error:function(e,t){return t!==null&&e.fixedChoice&&e.choices.indexOf(t)<0?"No custom values allowed.":t!==null&&e.correctChoice!==null?"Not correct, try again.":t!==null&&e.verifyChoice().err?e.verifyChoice().err:"Answer required."}},t.title=!1,t.className="dropdown",t.prototype.init=function(e){var t;if(!this.id)throw new TypeError("Dropdown.init: options.id is missing");if("string"==typeof e.mainText)this.mainText=e.mainText;else if("undefined"!=typeof e.mainText)throw new TypeError("Dropdown.init: options.mainText must be string or undefined. Found: "+e.mainText);if("string"==typeof e.labelText)this.labelText=e.labelText;else if("undefined"!=typeof e.labelText)throw new TypeError("Dropdown.init: options.labelText must be string or undefined. Found: "+e.labelText);if("string"==typeof e.placeHolder)this.placeHolder=e.placeHolder;else if("undefined"!=typeof e.placeHolder)throw new TypeError("Dropdown.init: options.placeHolder must be string or undefined. Found: "+e.placeHolder);"undefined"!=typeof e.choices&&(this.choices=e.choices);if("boolean"==typeof e.requiredChoice)this.requiredChoice=e.requiredChoice;else if("undefined"!=typeof e.requiredChoice)throw new TypeError("Dropdown.init: options.requiredChoice be boolean or undefined. Found: "+e.requiredChoice);if("undefined"!=typeof e.correctChoice){if(this.requiredChoice)throw new Error("Dropdown.init: cannot specify both options requiredChoice and correctChoice");if(J.isArray(e.correctChoice)&&e.correctChoice.length>e.choices.length)throw new Error("Dropdown.init: options.correctChoice length cannot exceed options.choices length");this.correctChoice=e.correctChoice}if("boolean"==typeof e.fixedChoice)this.fixedChoice=e.fixedChoice;else if("undefined"!=typeof e.fixedChoice)throw new TypeError("Dropdown.init: options.fixedChoice be boolean or undefined. Found: "+e.fixedChoice);if("undefined"!=typeof e.tag&&"datalist"!==e.tag&&"select"!==e.tag)throw new TypeError('Dropdown.init: options.tag must be "datalist" or "select". Found: '+e.tag);this.tag=e.tag;if("function"==typeof e.listener)this.listener=function(t){e.listener.call(this,t)};else if("undefined"!=typeof e.listener)throw new TypeError("Dropdown.init: opts.listener must be function or undefined. Found: "+e.listener);if("function"==typeof e.onchange)this.onchange=e.onchange;else if("undefined"!=typeof e.onchange)throw new TypeError("Dropdownn.init: opts.onchange must be function or undefined. Found: "+e.onchange);if("function"==typeof e.validation)this.validation=e.validation;else if("undefined"!=typeof e.validation)throw new TypeError("Dropdownn.init: opts.validation must be function or undefined. Found: "+e.validation);"undefined"==typeof e.shuffleChoices?t=!1:t=!!e.shuffleChoices,this.shuffleChoices=t;if(e.width){if("string"!=typeof e.width)throw new TypeError("Dropdownn.init:width must be string or undefined. Found: "+e.width);this.inputWidth=e.width}if("undefined"!=typeof e.validationSpeed){t=J.isInt(e.valiadtionSpeed,0,undefined,!0);if(t===!1)throw new TypeError("Dropdownn.init: validationSpeed must a non-negative number or undefined. Found: "+e.validationSpeed);this.validationSpeed=t}},t.prototype.append=function(){if(W.gid(this.id))throw new Error("Dropdown.append: id is not unique: "+this.id);var e=this.text,t=this.label;e=W.get("p"),e.innerHTML=this.mainText,e.id="p",this.bodyDiv.appendChild(e),t=W.get("label"),t.innerHTML=this.labelText,this.bodyDiv.appendChild(t),this.setChoices(this.choices,!0),this.errorBox=W.append("div",this.bodyDiv,{className:"errbox",id:"errbox"})},t.prototype.setChoices=function(e,t){var n,r,i,s,o,u,a,f,l,c;this.choices=e;if(!t)return;f=!1,this.menu?this.menu.innerHTML="":f=!0,f&&(s=this.placeHolder,n=this.tag,n==="datalist"||"undefined"==typeof n?(u=W.get("datalist"),u.id="dropdown",a=W.get("input"),a.setAttribute("list",u.id),a.id=this.id,a.autocomplete="off",s&&(a.placeholder=s),this.inputWidth&&(a.style.width=this.inputWidth),this.bodyDiv.appendChild(a),this.bodyDiv.appendChild(u),this.menu=a):n==="select"&&(o=W.get("select"),o.id=this.id,this.inputWidth&&(o.style.width=this.inputWidth),s&&(r=W.get("option"),r.value="",r.innerHTML=s,r.setAttribute("disabled",""),r.setAttribute("selected",""),r.setAttribute("hidden",""),o.appendChild(r)),this.bodyDiv.appendChild(o),this.menu=o)),c=e.length,i=J.seq(0,c-1),this.shuffleChoices&&(i=J.shuffle(i));for(l=0;l=0}this.fixedChoice&&this.choices.indexOf(n)<0&&(r.value=!1);if(this.validation){if(undefined===typeof r)throw new TypeError("something");this.validation(this.currentChoice,r)}return r},t.prototype.setError=function(e){this.errorBox&&(this.errorBox.innerHTML=e||""),e?this.highlight():this.unhighlight()},t.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("Dropdown.highlight: border must be string or undefined. Found: "+e);if(this.highlighted)return;this.menu.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},t.prototype.unhighlight=function(){if(this.highlighted!==!0)return;this.menu.style.border="",this.highlighted=!1,this.setError(),this.emit("unhighlighted")},t.prototype.getValues=function(e){var t;e=e||{};var n=this.verifyChoice().value;t={id:this.id,choice:this.fixedChoice?this.choices.indexOf(this.currentChoice):this.currentChoice,time:this.timeCurrentChoice,nChanges:this.numberOfChanges},"undefined"==typeof e.highlight&&(e.highlight=!0),this.shuffleChoices&&(t.order=this.order),e.addValue!==!1&&e.getValue!==!1&&(t.value=this.currentChoice);if(null!==this.correctChoice||null!==this.requiredChoice||null!==this.fixedChoice)t.isCorrect=n,!t.isCorrect&&e.highlight&&this.highlight();return t.isCorrect===!1&&this.setError(this.getText("error",t.value)),t},t.prototype.listeners=function(){var t=this;e.on("INPUT_DISABLE",function(){t.disable()}),e.on("INPUT_ENABLE",function(){t.enable()})},t.prototype.disable=function(){if(this.disabled===!0)return;this.disabled=!0,this.menu&&this.menu.removeEventListener("change",this.listener),this.emit("disabled")},t.prototype.enable=function(){if(this.disabled===!1)return;if(!this.menu)throw new Error("Dropdown.enable: menu is not defined");this.disabled=!1,this.menu.addEventListener("change",this.listener),this.emit("enabled")}}(node),function(e){"use strict";function t(e){if(!e.onsubmit)this.onsubmit={emailOnly:!0,send:!0,updateUI:!0};else{if("object"!=typeof e.onsubmit)throw new TypeError("EmailForm constructor: opts.onsubmit must be object or undefined. Found: "+e.onsubmit);this.onsubmit=e.onsubmit}this._email=e.email||null,this.attempts=[],this.timeInput=null,this.formElement=null,this.inputElement=null,this.buttonElement=null,this.setMsg=!!e.setMsg||!1,this.showSubmitBtn="undefined"==typeof e.showSubmitBtn?!0:!!e.showSubmitBtn}function n(){return this.inputElement?this.inputElement.value:this._email}e.widgets.register("EmailForm",t),t.version="0.13.1",t.description="Displays a configurable email form.",t.title=!1,t.className="emailform",t.texts={label:"Enter your email:",errString:"Not a valid email address, please correct it and submit it again.",sent:"Sent!"},t.prototype.createForm=function(){var e,t,n,r,i;return e=this,t=document.createElement("form"),t.className="emailform-form",n=document.createElement("label"),n.innerHTML=this.getText("label"),r=document.createElement("input"),r.setAttribute("type","text"),r.setAttribute("placeholder","Email"),r.className="emailform-input form-control",t.appendChild(n),t.appendChild(r),this.formElement=t,this.inputElement=r,this.showSubmitBtn&&(i=document.createElement("input"),i.setAttribute("type","submit"),i.setAttribute("value","Submit email"),i.className="btn btn-lg btn-primary emailform-submit",t.appendChild(i),J.addEvent(t,"submit",function(t){t.preventDefault(),e.getValues(e.onsubmit)},!0),J.addEvent(t,"input",function(){e.timeInput||(e.timeInput=J.now()),e.isHighlighted()&&e.unhighlight()},!0),this.buttonElement=i),this._email&&(this.formElement.value=this._email),this._email=null,t},t.prototype.verifyInput=function(e,t){var r,i;return r=n.call(this),i=J.isEmail(r),i&&t?(this.inputElement&&(this.inputElement.disabled=!0),this.buttonElement&&(this.buttonElement.disabled=!0,this.buttonElement.value=this.getText("sent"))):(t&&this.buttonElement&&(this.buttonElement.value=this.getText("errString")),("undefined"==typeof e||e)&&this.attempts.push(r)),i},t.prototype.append=function(){this.createForm(),this.bodyDiv.appendChild(this.formElement)},t.prototype.setValues=function(e){var t;e=e||{},e.email?t=e.email:t=J.randomEmail(),this.inputElement?this.inputElement.value=t:this._email=t,this.timeInput=J.now()},t.prototype.getValues=function(e){var t,r;return e=e||{},"undefined"!=typeof e.say&&(console.log("***EmailForm.getValues: option say is deprecated, use send.***"),e.send=e.say),"undefined"!=typeof e.sayAnyway&&(console.log("***EmailForm.getValues: option sayAnyway is deprecated, use sendAnyway.***"),e.sendAnyway=e.sayAnyway),"undefined"==typeof e.markAttempt&&(e.markAttempt=!0),"undefined"==typeof e.highlight&&(e.highlight=!0),t=n.call(this),e.verify!==!1&&(r=this.verifyInput(e.markAttempt,e.updateUI)),e.emailOnly||(t={time:this.timeInput,email:t,attempts:this.attempts},e.markAttempt&&(t.isCorrect=r)),r===!1&&((e.updateUI||e.highlight)&&this.highlight(),this.timeInput=null),(e.send&&r||e.sendAnyway)&&this.sendValues({values:t}),e.reset&&this.reset(),t},t.prototype.sendValues=function(t){var n;return t=t||{emailOnly:!0},n=t.values||this.getValues(t),this.setMsg?("string"==typeof n&&(n={email:n}),e.set(n,t.to||"SERVER")):e.say("email",t.to||"SERVER",n),n},t.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("EmailForm.highlight: border must be string or undefined. Found: "+e);if(!this.inputElement||this.highlighted===!0)return;this.inputElement.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},t.prototype.unhighlight=function(){if(!this.inputElement||this.highlighted!==!0)return;this.inputElement.style.border="",this.highlighted=!1,this.emit("unhighlighted")},t.prototype.reset=function(){this.attempts=[],this.timeInput=null,this._email=null,this.inputElement&&(this.inputElement.value=""),this.isHighlighted()&&this.unhighlight()}}(node),function(e){"use strict";function t(e){this.showEmailForm=!0,this.showFeedbackForm=!0,this.showTotalWin=!0,this.showExitCode=!0,this.totalWinCurrency="USD",this.totalWinCb=null,this.emailForm=null,this.feedback=null,this.endScreenHTML=null,this.askServer=e.askServer||!1}e.widgets.register("EndScreen",t),t.version="0.7.2",t.description="Game end screen. With end game message, email form, and exit code.",t.title=!1,t.className="endscreen",t.texts={headerMessage:"Thank you for participating!",message:"You have now completed this task and your data has been saved. Please go back to the Amazon Mechanical Turk web site and submit the HIT.",totalWin:"Your total win:",exitCode:"Your exit code:",errTotalWin:"Error: invalid total win.",errExitCode:"Error: invalid exit code.",copyButton:"Copy",exitCopyMsg:"Exit code copied to clipboard.",exitCopyError:"Failed to copy exit code. Please copy it manually."},t.dependencies={Feedback:{},EmailForm:{}},t.prototype.init=function(t){if(t.email===!1)this.showEmailForm=!1;else if("boolean"==typeof t.showEmailForm)this.showEmailForm=t.showEmailForm;else if("undefined"!=typeof t.showEmailForm)throw new TypeError("EndScreen.init: options.showEmailForm must be boolean or undefined. Found: "+t.showEmailForm);if(t.feedback===!1)this.showFeedbackForm=!1;else if("boolean"==typeof t.showFeedbackForm)this.showFeedbackForm=t.showFeedbackForm;else if("undefined"!=typeof t.showFeedbackForm)throw new TypeError("EndScreen.init: options.showFeedbackForm must be boolean or undefined. Found: "+t.showFeedbackForm);if(t.totalWin===!1)this.showTotalWin=!1;else if("boolean"==typeof t.showTotalWin)this.showTotalWin=t.showTotalWin;else if("undefined"!=typeof t.showTotalWin)throw new TypeError("EndScreen.init: options.showTotalWin must be boolean or undefined. Found: "+t.showTotalWin);if(t.exitCode===!1)t.showExitCode!==!1;else if("boolean"==typeof t.showExitCode)this.showExitCode=t.showExitCode;else if("undefined"!=typeof t.showExitCode)throw new TypeError("EndScreen.init: options.showExitCode must be boolean or undefined. Found: "+t.showExitCode);if("string"==typeof t.totalWinCurrency&&t.totalWinCurrency.trim()!=="")this.totalWinCurrency=t.totalWinCurrency;else if("undefined"!=typeof t.totalWinCurrency)throw new TypeError("EndScreen.init: options.totalWinCurrency must be undefined or a non-empty string. Found: "+t.totalWinCurrency);if(t.totalWinCb){if("function"!=typeof t.totalWinCb)throw new TypeError("EndScreen.init: options.totalWinCb must be function or undefined. Found: "+t.totalWinCb);this.totalWinCb=t.totalWinCb}this.showEmailForm&&!this.emailForm&&(this.emailForm=e.widgets.get("EmailForm",J.mixin({onsubmit:{send:!0,emailOnly:!0,updateUI:!0},storeRef:!1,texts:{label:"If you would like to be contacted for future studies, please enter your email (optional):",errString:"Please enter a valid email and retry"},setMsg:!0},t.email))),this.showFeedbackForm&&(this.feedback=e.widgets.get("Feedback",J.mixin({storeRef:!1,minChars:50,setMsg:!0},t.feedback)))},t.prototype.append=function(){this.endScreenHTML=this.makeEndScreen(),this.bodyDiv.appendChild(this.endScreenHTML),this.askServer&&setTimeout(function(){e.say("WIN")})},t.prototype.makeEndScreen=function(){var t,n,r,i,s,o,u,a,f,l,c,h=this;return t=document.createElement("div"),t.className="endscreen",n=document.createElement("h1"),n.innerHTML=this.getText("headerMessage"),t.appendChild(n),r=document.createElement("p"),r.innerHTML=this.getText("message"),t.appendChild(r),this.showTotalWin&&(i=document.createElement("div"),s=document.createElement("p"),s.innerHTML=""+this.getText("totalWin")+"",o=document.createElement("input"),o.className="endscreen-total form-control",o.setAttribute("disabled","true"),s.appendChild(o),i.appendChild(s),t.appendChild(i),this.totalWinInputElement=o),this.showExitCode&&(u=document.createElement("div"),u.className="input-group",a=document.createElement("span"),a.innerHTML=""+this.getText("exitCode")+"",f=document.createElement("input"),f.id="exit_code",f.className="endscreen-exit-code form-control",f.setAttribute("disabled","true"),c=document.createElement("span"),c.className="input-group-btn",l=document.createElement("button"),l.className="btn btn-default endscreen-copy-btn",l.innerHTML=this.getText("copyButton"),l.type="button",l.onclick=function(){h.copy(f.value)},c.appendChild(l),t.appendChild(a),u.appendChild(c),u.appendChild(f),t.appendChild(u),this.exitCodeInputElement=f),this.showEmailForm&&e.widgets.append(this.emailForm,t,{title:!1,panel:!1}),this.showFeedbackForm&&e.widgets.append(this.feedback,t,{title:!1,panel:!1}),t},t.prototype.listeners=function(){var t;t=this,e.on.data("WIN",function(e){t.updateDisplay(e.data)})},t.prototype.copy=function(e){var t=document.createElement("input");try{document.body.appendChild(t),t.value=e,t.select(),document.execCommand("copy",!1),t.remove(),alert(this.getText("exitCopyMsg"))}catch(n){alert(this.getText("exitCopyError"))}},t.prototype.updateDisplay=function(t){var n,r,i,s,o,u,a,f;if(this.totalWinCb)r=this.totalWinCb(t,this);else{if("undefined"==typeof t.total&&"undefined"==typeof t.totalRaw)throw new Error("EndScreen.updateDisplay: data.total and data.totalRaw cannot be both undefined.");"undefined"!=typeof t.total&&(r=J.isNumber(t.total),r===!1&&(e.err("EndScreen.updateDisplay: invalid data.total: "+t.total),r=this.getText("errTotalWin"),f=!0)),n="","undefined"!=typeof t.basePay&&(n=t.basePay),"undefined"!=typeof t.bonus&&t.showBonus!==!1&&(n!==""&&(n+=" + "),n+=t.bonus),t.partials&&(J.isArray(t.partials)?(n!==""&&(n+=" + "),n+=t.partials.join(" + ")):e.err("EndScreen error, invalid partials win: "+t.partials)),"undefined"!=typeof t.totalRaw&&(n?n+=" = ":n="",n+=t.totalRaw,a="undefined"!=typeof t.exchangeRate?t.exchangeRate:e.game.settings.EXCHANGE_RATE,"undefined"!=typeof a&&(n+="*"+a),"undefined"==typeof r&&(i=J.isNumber(t.totalRaw,0),r=parseFloat(a*i).toFixed(2),r=J.isNumber(r,0),r===!1&&(e.err("EndScreen.updateDisplay: invalid : totalWin calculation from totalRaw."),r=this.getText("errTotalWin"),f=!0))),f||(r!==n&n!==""&&(r=n+" = "+r),r+=" "+this.totalWinCurrency)}s=t.exit,"string"!=typeof s&&(e.err("EndScreen error, invalid exit code: "+s),s=this.getText("errExitCode")),o=this.totalWinInputElement,u=this.exitCodeInputElement,o&&this.showTotalWin&&(o.value=r),u&&this.showExitCode&&(u.value=s)}}(node),function(e){"use strict";function i(e){var t;"undefined"!=typeof e.maxLength&&(console.log("***Feedback constructor: maxLength is deprecated, use maxChars instead***"),e.maxChars=e.maxLength),"undefined"!=typeof e.minLength&&(console.log("***Feedback constructor: minLength is deprecated, use minChars instead***"),e.minChars=e.minLength),this.mainText=null,this.hint=null,this.spanMainText=null;if("undefined"==typeof e.maxChars)this.maxChars=0;else{t=J.isInt(e.maxChars,0);if(t===!1)throw new TypeError("Feedback constructor: maxChars must be an integer >= 0 or undefined. Found: "+e.maxChars);this.maxChars=t}if("undefined"==typeof e.minChars)this.minChars=0;else{t=J.isInt(e.minChars,0,undefined,!0);if(t===!1)throw new TypeError("Feedback constructor: minChars must be an integer >= 0 or undefined. Found: "+e.minChars);if(this.maxChars&&t>this.maxChars)throw new TypeError("Feedback constructor: minChars cannot be greater than maxChars. Found: "+t+" > "+this.maxChars);this.minChars=t}if("undefined"==typeof e.maxWords)this.maxWords=0;else{t=J.isInt(e.maxWords,0,undefined,!0);if(t===!1)throw new TypeError("Feedback constructor: maxWords must be an integer >= 0 or undefined. Found: "+e.maxWords);this.maxWords=e.maxWords}if("undefined"==typeof e.minWords)this.minWords=0;else{t=J.isInt(e.minWords,0,undefined,!0);if(t===!1)throw new TypeError("Feedback constructor: minWords must be an integer >= 0 or undefined. Found: "+e.minWords);this.minWords=e.minWords;if(this.maxChars){t=(this.maxChars+1)/2;if(this.minWords>t)throw new TypeError("Feedback constructor: minWords cannot be larger than (maxChars+1)/2. Found: "+this.minWords+" > "+t)}}if(this.maxWords){if(this.maxChars&&this.maxChars "+this.maxWords);if(this.minChars>this.maxWords)throw new TypeError("Feedback constructor: minChars cannot be greater than maxWords. Found: "+this.minChars+" > "+this.maxWords)}if("undefined"==typeof e.rows)this.rows=3;else{if(J.isInt(e.rows,0)===!1)throw new TypeError("Feedback constructor: rows must be an integer > 0 or undefined. Found: "+e.rows);this.rows=e.rows}if("undefined"==typeof e.maxAttemptLength)this.maxAttemptLength=0;else{t=J.isNumber(e.maxAttemptLength,0);if(t===!1)throw new TypeError("Feedback constructor: options.maxAttemptLength must be a number > 0 or undefined. Found: "+e.maxAttemptLength);this.maxAttemptLength=t}this.showSubmit="undefined"==typeof e.showSubmit?!0:!!e.showSubmit;if(!e.onsubmit)this.onsubmit={feedbackOnly:!0,send:!0,updateUI:!0};else{if("object"!=typeof e.onsubmit)throw new TypeError("Feedback constructor: onsubmit must be string or object. Found: "+e.onsubmit);this.onsubmit=e.onsubmit}this._feedback=e.feedback||null,this.attempts=[],this.timeInputBegin=null,this.feedbackForm=null,this.textareaElement=null,this.charCounter=null,this.wordCounter=null,this.submitButton=null,this.setMsg=!!e.setMsg||!1}function s(){var e;return e=this.textareaElement?this.textareaElement.value:this._feedback,e?e.trim():e}e.widgets.register("Feedback",i),i.version="1.6.0",i.description="Displays a configurable feedback form",i.title="Feedback",i.className="feedback",i.texts={autoHint:function(e){var t,n;return e.minChars&&e.maxChars?t="between "+e.minChars+" and "+e.maxChars+" characters":e.minChars?(t="at least "+e.minChars+" character",e.minChars>1&&(t+="s")):e.maxChars&&(t="at most "+e.maxChars+" character",e.maxChars>1&&(t+="s")),e.minWords&&e.maxWords?n="beetween "+e.minWords+" and "+e.maxWords+" words":e.minWords?(n="at least "+e.minWords+" word",e.minWords>1&&(n+="s")):e.maxWords&&(n="at most "+e.maxWords+" word",e.maxWords>1&&(n+="s")),t?(t="("+t,n&&(t+=", and "+n),t+")"):n?"("+n+")":!1},submit:"Submit feedback",label:"Any feedback? Let us know here:",sent:"Sent!",counter:function(e,t){var n;return n=t.chars?" character":" word",t.len!==1&&(n+="s"),t.needed?n+=" needed":t.over?n+=" over":t.justcount||(n+=" remaining"),n}};var t,n,r;t="#a32020",n="#a32020",r="#78b360",i.dependencies={JSUS:{}},i.prototype.init=function(e){if("string"==typeof e.mainText)this.mainText=e.mainText;else{if("undefined"!=typeof e.mainText)throw new TypeError("Feedback.init: options.mainText must be string or undefined. Found: "+e.mainText);this.mainText=this.getText("label")}if("string"==typeof e.hint||!1===e.hint)this.hint=e.hint;else{if("undefined"!=typeof e.hint)throw new TypeError("Feedback.init: options.hint must be a string, false, or undefined. Found: "+e.hint);this.hint=this.getText("autoHint")}},i.prototype.verifyFeedback=function(e,i){var o,u,a,f,l,c,h,p,d,v,m;return o=s.call(this),u=o?o.length:0,f=this.submitButton,l=this.charCounter,c=this.wordCounter,a=!0,uthis.maxChars?(a=!1,h=u-this.maxChars,p=h+this.getText("counter",{chars:!0,over:!0,len:h}),d=n):(h=this.maxChars?this.maxChars-u:u,p=h+this.getText("counter",{chars:!0,len:h,justcount:!this.maxChars}),d=r),c&&(h=o?o.match(/\b[-?(\w+)?]+\b/gi):0,u=h?h.length:0,uthis.maxWords?(a=!1,h=u-this.maxWords,v=h+this.getText("counter",{over:!0,len:h}),m=n):(h=this.maxWords?this.maxWords-u:u,v=h+this.getText("counter",{len:h,justcount:!this.maxWords}),m=r)),i&&(f&&(f.disabled=!a),l&&(l.style.backgroundColor=d,l.innerHTML=p),c&&(c.style.backgroundColor=m,c.innerHTML=v)),!a&&("undefined"==typeof e||e)&&(this.maxAttemptLength&&u>this.maxAttemptLength&&(o=o.substr(0,this.maxAttemptLength)),this.attempts.push(o)),a},i.prototype.append=function(){var e;e=this,this.feedbackForm=W.append("form",this.bodyDiv,{className:"feedback-form"}),this.mainText&&(this.spanMainText=W.append("span",this.feedbackForm,{className:"feedback-maintext",innerHTML:this.mainText})),this.hint&&W.append("span",this.spanMainText||this.feedbackForm,{className:"feedback-hint",innerHTML:this.hint}),this.textareaElement=W.append("textarea",this.feedbackForm,{className:"form-control feedback-textarea",type:"text",rows:this.rows}),this.showSubmit&&(this.submitButton=W.append("input",this.feedbackForm,{className:"btn btn-lg btn-primary",type:"submit",value:this.getText("submit")}),J.addEvent(this.feedbackForm,"submit",function(t){t.preventDefault(),e.getValues(e.onsubmit)})),this.showCounters(),J.addEvent(this.feedbackForm,"input",function(){e.isHighlighted()&&e.unhighlight(),e.verifyFeedback(!1,!0)}),J.addEvent(this.feedbackForm,"click",function(){e.isHighlighted()&&e.unhighlight()}),this.verifyFeedback(!1,!0)},i.prototype.setValues=function(e){var t,n,r,i,s;e=e||{};if(!e.feedback){r=this.minChars||0,this.maxChars?n=this.maxChars:this.maxWords?n=this.maxWords*4:r?n=r+80:n=80,t=J.randomString(J.randomInt(r,n),"aA_1");if(this.minWords){i=this.minWords-t.split(" ").length;if(i>0)for(s=0;s")),e.verify!==!1&&(n=this.verifyFeedback(e.markAttempt,e.updateUI)),n===!1&&(e.updateUI||e.highlight)&&this.highlight(),e.feedbackOnly||(t={timeBegin:this.timeInputBegin,feedback:t,attempts:this.attempts,valid:n},e.markAttempt&&(t.isCorrect=n)),t!==""&&(e.send&&n||e.sendAnyway)&&(this.sendValues({values:t}),e.updateUI&&(this.submitButton.setAttribute("value",this.getText("sent")),this.submitButton.disabled=!0,this.textareaElement.disabled=!0)),e.reset&&this.reset(),t},i.prototype.sendValues=function(t){var n;return t=t||{feedbackOnly:!0},n=t.values||this.getValues(t),this.setMsg?("string"==typeof n&&(n={feedback:n}),e.set(n,t.to||"SERVER")):e.say("feedback",t.to||"SERVER",n),n},i.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("Feedback.highlight: border must be string or undefined. Found: "+e);if(!this.isAppended()||this.highlighted===!0)return;this.textareaElement.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},i.prototype.unhighlight=function(){if(!this.isAppended()||this.highlighted!==!0)return;this.textareaElement.style.border="",this.highlighted=!1,this.emit("unhighlighted")},i.prototype.reset=function(){this.attempts=[],this.timeInputBegin=null,this._feedback=null,this.textareaElement&&(this.textareaElement.value=""),this.isHighlighted()&&this.unhighlight()},i.prototype.disable=function(){if(!this.textareaElement||this.textareaElement.disabled)return;this.disabled=!0,this.submitElement&&(this.submitElement.disabled=!0),this.textareaElement.disabled=!0,this.emit("disabled")},i.prototype.enable=function(){if(!this.textareaElement||!this.textareaElement.disabled)return;this.disabled=!1,this.submitElement&&(this.submitElement.disabled=!1),this.textareaElement.disabled=!1,this.emit("enabled")},i.prototype.showCounters=function(){if(!this.charCounter){if(this.minChars||this.maxChars)this.charCounter=W.append("span",this.feedbackForm,{className:"feedback-char-count badge",innerHTML:this.maxChars})}else this.charCounter.style.display="";if(!this.wordCounter){if(this.minWords||this.maxWords)this.wordCounter=W.append("span",this.feedbackForm,{className:"feedback-char-count badge",innerHTML:this.maxWords}),this.charCounter&&(this.wordCounter.style["margin-left"]="10px")}else this.wordCounter.style.display=""},i.prototype.hideCounters=function(){this.charCounter&&(this.charCounter.style.display="none"),this.wordCounter&&(this.wordCounter.style.display="none")}}(node),function(e){"use strict";function i(){this.ctg=null,this.choices=n,this.header=r,this.mainText=null}e.widgets.register("GroupMalleability",i),i.version="0.1.0",i.description="Displays an interface to measure perception for group malleability.",i.title="Group Malleability",i.className="group-malleability";var t=["As hard as it is to admit, it is impossible to change the central characteristics of nationalities and groups.","Groups that are characterized by extreme and violent traits will never change as these traits are inherently ingrained in their nature.","Groups can sometimes change their outward behavior, but can never change who they really are.","Every nationality or group has a fixed set of beliefs and values that cannot be changed.","Social and political processes can lead to changes in a group's values and morality."],n=[1,2,3,4,5,6,7],r=["Strongly Oppose","Somewhat Oppose","Slightly Oppose","Neutral","Slightly Favor","Somewhat Favor","Strongly Favor"];i.texts={mainText:"Show how much you favor or oppose each idea below by selecting a number from 1 to 7 on the scale below. You can work quickly, your first feeling is generally best."},i.dependencies={},i.prototype.init=function(e){e=e||{};if(e.choices){if(!J.isArray(e.choices)||e.choices.length<2)throw new Error("GroupMalleability.init: choices must be an array of length > 1 or undefined. Found: "+e.choices);this.choices=e.choices}if(e.header){if(!J.isArray(e.header)||e.header.length!==this.choices.length)throw new Error("GroupMalleability.init: header must be an array of length equal to the number of choices or undefined. Found: "+e.header);this.header=e.header}if(e.mainText){if("string"!=typeof e.mainText&&e.mainText!==!1)throw new Error("GroupMalleability.init: mainText must be string, false, or undefined. Found: "+e.mainText);this.mainText=e.mainText}else e.mainText!==!1&&(this.mainText=this.getText("mainText"))},i.prototype.append=function(){this.ctg=e.widgets.add("ChoiceTableGroup",this.panelDiv,{id:this.id||"groupmalleability_choicetable",items:t.map(function(e,t){return["GM_"+(t+1),e]}),choices:this.choices,mainText:this.mainText,title:!1,panel:!1,requiredChoice:this.required,header:this.header})},i.prototype.getValues=function(e){return e=e||{},this.ctg.getValues(e)},i.prototype.setValues=function(e){return e=e||{},this.ctg.setValues(e)},i.prototype.enable=function(e){return this.ctg.enable(e)},i.prototype.disable=function(e){return this.ctg.disable(e)},i.prototype.highlight=function(e){return this.ctg.highlight(e)},i.prototype.unhighlight=function(e){return this.ctg.unhighlight(e)}}(node),function(e){"use strict";function t(t){var n=this;this.options=t,this.availableLanguages={en:{name:"English",nativeName:"English",shortName:"en"}},this.currentLanguage=null,this.buttonListLength=null,this.displayForm=null,this.optionsLabel={},this.optionsDisplay={},this.loadingDiv=null,this.languagesLoaded=!1,this.usingButtons=!0,this.updatePlayer="ondone",this.setUriPrefix=!0,this.notifyServer=!0,this.onLangCallback=function(t){function i(e){return function(){n.setLanguage(e,n.updatePlayer==="onselect")}}var r;while(n.displayForm.firstChild)n.displayForm.removeChild(n.displayForm.firstChild);n.availableLanguages=t.data;if(n.usingButtons)for(r in t.data)t.data.hasOwnProperty(r)&&(n.optionsLabel[r]=W.get("label",{id:r+"Label","for":r+"RadioButton"}),n.optionsDisplay[r]=W.get("input",{id:r+"RadioButton",type:"radio",name:"languageButton",value:t.data[r].name}),n.optionsDisplay[r].onclick=i(r),n.optionsLabel[r].appendChild(n.optionsDisplay[r]),n.optionsLabel[r].appendChild(document.createTextNode(t.data[r].nativeName)),W.add("br",n.displayForm),n.optionsLabel[r].className="unselectedButtonLabel",n.displayForm.appendChild(n.optionsLabel[r]));else{n.displaySelection=W.get("select","selectLanguage");for(r in t.data)n.optionsLabel[r]=document.createTextNode(t.data[r].nativeName),n.optionsDisplay[r]=W.get("option",{id:r+"Option",value:r}),n.optionsDisplay[r].appendChild(n.optionsLabel[r]),n.displaySelection.appendChild(n.optionsDisplay[r]);n.displayForm.appendChild(n.displaySelection),n.displayForm.onchange=function(){n.setLanguage(n.displaySelection.value,n.updatePlayer==="onselect")}}n.loadingDiv.style.display="none",n.languagesLoaded=!0,n.setLanguage(e.player.lang.shortName||"en",!1),n.onLangCallbackExtension&&(n.onLangCallbackExtension(t),n.onLangCallbackExtension=null)},this.onLangCallbackExtension=null}e.widgets.register("LanguageSelector",t),t.version="0.6.2",t.description="Display information about the current language and allows to change language.",t.title="Language",t.className="languageselector",t.texts.loading="Loading language information...",t.dependencies={JSUS:{}},t.prototype.init=function(t){J.mixout(t,this.options),this.options=t,"undefined"!=typeof this.options.usingButtons&&(this.usingButtons=!!this.options.usingButtons);if("undefined"!=typeof this.options.notifyServer)if(!1===this.options.notifyServer)this.options.notifyServer="never";else{if("string"!=typeof this.options.notifyServer)throw new Error("LanguageSelector.init: options.notifyServer must be "+this.options.notifyServer);if("never"!==this.options.notifyServer&&"onselect"!==this.options.notifyServer&&"ondone"!==this.options.notifyServer)throw new Error('LanguageSelector.init: invalid value for notifyServer: "'+this.options.notifyServer+'". Valid '+'values: "never","onselect", "ondone".');this.notifyServer=this.options.notifyServer}"undefined"!=typeof this.options.setUriPrefix&&(this.setUriPrefix=!!this.options.setUriPrefix),e.on.lang(this.onLangCallback),this.displayForm=W.get("form","radioButtonForm"),this.loadingDiv=W.add("div",this.displayForm),this.loadingDiv.innerHTML=this.getText("loading"),this.loadLanguages()},t.prototype.append=function(){this.bodyDiv.appendChild(this.displayForm)},t.prototype.setLanguage=function(t,n){this.usingButtons&&this.currentLanguage!==null&&this.currentLanguage!==this.availableLanguages[t]&&(this.optionsDisplay[this.currentLanguage].checked="unchecked",this.optionsLabel[this.currentLanguage].className="unselectedButtonLabel"),this.currentLanguage=t,this.usingButtons?(this.optionsDisplay[this.currentLanguage].checked="checked",this.optionsLabel[this.currentLanguage].className="selectedButtonLabel"):this.displaySelection.value=this.currentLanguage,n!==!1&&e.setLanguage(this.availableLanguages[this.currentLanguage],this.setUriPrefix,this.notifyServer)},t.prototype.updateAvalaibleLanguages=function(t){t&&t.callback&&(this.onLangCallbackExtension=t.callback),e.socket.send(e.msg.create({target:"LANG",to:"SERVER",action:"get"}))},t.prototype.loadLanguages=function(e){this.languagesLoaded?e&&e.callback&&e.callback():this.updateAvalaibleLanguages(e)},t.prototype.listeners=function(){var t;t=this,e.events.step.on("REALLY_DONE",function(){t.updatePlayer==="ondone"&&e.setLanguage(t.availableLanguages[t.currentLanguage],t.setUriPrefix,t.notifyServer)})}}(node),function(e){"use strict";function t(){this.spanCurrency=null,this.spanMoney=null,this.currency="ECU",this.money=0,this.precision=2,this.showCurrency=!0,this.classnameCurrency="moneytalkscurrency",this.classnameMoney="moneytalksmoney"}e.widgets.register("MoneyTalks",t),t.version="0.5.0",t.description="Displays the earnings of a player.",t.title="Earnings",t.className="moneytalks",t.dependencies={JSUS:{}},t.prototype.init=function(e){e=e||{},"string"==typeof e.currency&&(this.currency=e.currency),"undefined"!=typeof e.showCurrency&&(this.showCurrency=!!e.showCurrency),"number"==typeof e.money&&(this.money=e.money),"number"==typeof e.precision&&(this.precision=e.precision),"string"==typeof e.MoneyClassName&&(this.classnameMoney=e.MoneyClassName),"string"==typeof e.currencyClassName&&(this.classnameCurrency=e.currencyClassName)},t.prototype.append=function(){this.spanMoney||(this.spanMoney=document.createElement("span")),this.spanCurrency||(this.spanCurrency=document.createElement("span")),this.showCurrency||(this.spanCurrency.style.display="none"),this.spanMoney.className=this.classnameMoney,this.spanCurrency.className=this.classnameCurrency,this.spanCurrency.innerHTML=this.currency,this.spanMoney.innerHTML=this.money,this.bodyDiv.appendChild(this.spanMoney),this.bodyDiv.appendChild(this.spanCurrency)},t.prototype.listeners=function(){var t=this;e.on("MONEYTALKS",function(e,n){t.update(e,n)})},t.prototype.update=function(t,n){var r;r=J.isNumber(t);if(r===!1){e.err("MoneyTalks.update: invalid amount: "+t);return}return n&&(this.money=0),this.money+=r,this.spanMoney.innerHTML=this.money.toFixed(this.precision),this.money},t.prototype.getValues=function(){return this.money}}(node),function(e){"use strict";function t(e){this.methods={},this.method="I-PANAS-SF",this.mainText=null,this.gauge=null,this.addMethod("I-PANAS-SF",r)}function n(e,t){if(!t)throw new Error("MoodGauge.init: method "+e+"did not create element gauge.");if("function"!=typeof t.getValues)throw new Error("MoodGauge.init: method "+e+": gauge missing function getValues.");if("function"!=typeof t.enable)throw new Error("MoodGauge.init: method "+e+": gauge missing function enable.");if("function"!=typeof t.disable)throw new Error("MoodGauge.init: method "+e+": gauge missing function disable.");if("function"!=typeof t.append)throw new Error("MoodGauge.init: method "+e+": gauge missing function append.")}function r(t){var n,r,i,s,o,u,a,f;i=t.choices||["1","2","3","4","5"],r=t.emotions||["Upset","Hostile","Alert","Ashamed","Inspired","Nervous","Determined","Attentive","Afraid","Active"],s=t.left||"never",o=t.right||"always",f=r.length,n=new Array(f),a=-1;for(;++a'+r[a]+": never",right:o,choices:i};return u=e.widgets.get("ChoiceTableGroup",{id:t.id||"ipnassf",items:n,mainText:this.mainText||this.getText("mainText"),title:!1,requiredChoice:!0,storeRef:!1}),u}e.widgets.register("MoodGauge",t),t.version="0.4.0",t.description="Displays an interface to measure mood and emotions.",t.title="Mood Gauge",t.className="moodgauge",t.texts.mainText="Thinking about yourself and how you normally feel, to what extent do you generally feel: ",t.dependencies={JSUS:{}},t.prototype.init=function(e){var t;if("undefined"!=typeof e.method){if("string"!=typeof e.method)throw new TypeError("MoodGauge.init: method must be string or undefined: "+e.method);if(!this.methods[e.method])throw new Error("MoodGauge.init: method is invalid: "+e.method);this.method=e.method}if(e.mainText){if("string"!=typeof e.mainText)throw new TypeError("MoodGauge.init: mainText must be string or undefined. Found: "+e.mainText);this.mainText=e.mainText}t=this.methods[this.method].call(this,e),n(this.method,t),this.gauge=t,this.on("enabled",function(){t.enable()}),this.on("disabled",function(){t.disable()}),this.on("highlighted",function(){t.highlight()}),this.on("unhighlighted",function(){t.unhighlight()})},t.prototype.append=function(){e.widgets.append(this.gauge,this.bodyDiv,{panel:!1})},t.prototype.addMethod=function(e,t){if("string"!=typeof e)throw new Error("MoodGauge.addMethod: name must be string: "+e);if("function"!=typeof t)throw new Error("MoodGauge.addMethod: cb must be function: "+t);if(this.methods[e])throw new Error("MoodGauge.addMethod: name already existing: "+e);this.methods[e]=t},t.prototype.getValues=function(e){return this.gauge.getValues(e)},t.prototype.setValues=function(e){return this.gauge.setValues(e)}}(node),function(e){"use strict";function t(e){function t(e){var t,n,i,s;return t="/images/"+(e.content.success?"success-icon.png":"delete-icon.png"),n=document.createElement("img"),n.src=t,"object"==typeof e.content.text&&(e.content.text=r(e.content.text)),s=document.createTextNode(e.content.text),i=document.createElement("span"),i.className="requirement",i.appendChild(n),i.appendChild(s),i}this.requirements=[],this.stillChecking=0,this.withTimeout=e.withTimeout||!0,this.timeoutTime=e.timeoutTime||1e4,this.timeoutId=null,this.summary=null,this.summaryUpdate=null,this.summaryResults=null,this.dots=null,this.hasFailed=!1,this.results=[],this.completed={},this.sayResults=e.sayResults||!1,this.sayResultsLabel=e.sayResultLabel||"requirements",this.addToResults=e.addToResults||null,this.onComplete=null,this.onSuccess=null,this.onFailure=null,this.callbacksExecuted=!1,this.list=new W.List({render:{pipeline:t,returnAt:"first"}})}function n(e,t,n){var r,i,s;i=function(n,r,i){if(e.completed[t])throw new Error("Requirements.checkRequirements: test already completed: "+t);e.completed[t]=!0,e.updateStillChecking(-1),n||(e.hasFailed=!0),"string"==typeof r&&(r=[r]);if(r){if(!J.isArray(r))throw new Error("Requirements.checkRequirements: errors must be array or undefined. Found: "+r);e.displayResults(r)}e.results.push({name:t,success:n,errors:r,data:i}),e.isCheckingFinished()&&e.checkingFinished()},r=e.requirements[n];if("function"==typeof r)s=r(i);else{if("object"!=typeof r)throw new TypeError("Requirements.checkRequirements: invalid requirement: "+t+".");s=r.cb(i,r.params||{})}s&&i(s.success,s.errors,s.data)}function r(e){var t;return e.msg?t=e.msg:e.message?t=e.message:e.description?t=t.description:t=e.toString(),t}e.widgets.register("Requirements",t),t.version="0.7.2",t.description="Checks a set of requirements and display the results",t.title="Requirements",t.className="requirements",t.texts.errStr="One or more function is taking too long. This is likely to be due to a compatibility issue with your browser or to bad network connectivity.",t.texts.testPassed="All tests passed.",t.dependencies={JSUS:{},List:{}},t.prototype.init=function(e){if("object"!=typeof e)throw new TypeError("Requirements.init: conf must be object. Found: "+e);if(e.requirements){if(!J.isArray(e.requirements))throw new TypeError("Requirements.init: conf.requirements must be array or undefined. Found: "+e.requirements);this.requirements=e.requirements}if("undefined"!=typeof e.onComplete){if(null!==e.onComplete&&"function"!=typeof e.onComplete)throw new TypeError("Requirements.init: conf.onComplete must be function, null or undefined. Found: "+e.onComplete);this.onComplete=e.onComplete}if("undefined"!=typeof e.onSuccess){if(null!==e.onSuccess&&"function"!=typeof e.onSuccess)throw new TypeError("Requirements.init: conf.onSuccess must be function, null or undefined. Found: "+e.onSuccess);this.onSuccess=e.onSuccess}if("undefined"!=typeof e.onFailure){if(null!==e.onFailure&&"function"!=typeof e.onFailure)throw new TypeError("Requirements.init: conf.onFailure must be function, null or undefined. Found: "+e.onFailure);this.onFailure=e.onFailure}if(e.maxExecTime){if(null!==e.maxExecTime&&"number"!=typeof e.maxExecTime)throw new TypeError("Requirements.init: conf.onMaxExecTime must be number, null or undefined. Found: "+e.maxExecTime);this.withTimeout=!!e.maxExecTime,this.timeoutTime=e.maxExecTime}},t.prototype.addRequirements=function(){var e,t;e=-1,t=arguments.length;for(;++e0&&e.displayResults([e.getText("errStr")]),e.timeoutId=null,e.hasFailed=!0,e.checkingFinished()},this.timeoutTime)},t.prototype.clearTimeout=function(){this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)},t.prototype.updateStillChecking=function(e,t){var n,r;this.stillChecking=t?e:this.stillChecking+e,n=this.requirements.length,r=n-this.stillChecking,this.summaryUpdate.innerHTML=" ("+r+" / "+n+")"},t.prototype.isCheckingFinished=function(){return this.stillChecking<=0},t.prototype.checkingFinished=function(t){var n;if(this.callbacksExecuted&&!t)return;this.callbacksExecuted=!0,this.timeoutId&&clearTimeout(this.timeoutId),this.dots.stop(),this.sayResults&&(n={success:!this.hasFailed,results:this.results},this.addToResults&&J.mixin(n,this.addToResults()),e.say(this.sayResultsLabel,"SERVER",n)),this.onComplete&&this.onComplete(),this.hasFailed?this.onFailure&&this.onFailure():this.onSuccess&&this.onSuccess()},t.prototype.displayResults=function(e){var t,n;if(!this.list)throw new Error("Requirements.displayResults: list not found. Have you called .append() first?");if(!J.isArray(e))throw new TypeError("Requirements.displayResults: results must be array. Found: "+e);if(!this.hasFailed&&this.stillChecking<=0)this.list.addDT({success:!0,text:this.getText("testPassed")});else{t=-1,n=e.length;for(;++tand',a=e+s,a+=i.currencyAfter?t+o:o+t,a+=u+n+s,a+(i.currencyAfter?r+o:o+r)}function r(t){var r,i,s,o,u,a,f,l,c,h,p,d;a=t.values||[2,1.6,3.85,.1],t.scale&&(a=a.map(function(e){return e*t.scale})),f=a[0].toFixed(2),l=a[1].toFixed(2),c=a[2].toFixed(2),h=a[3].toFixed(2),o=10,r=new Array(o);for(s=0;s 0 or undefined. Found: "+t.boxValue)}else this.boxValue=.01;this.currency=t.currency||"USD",this.revealProbBomb="undefined"==typeof t.revealProbBomb?!0:!!t.revealProbBomb;if("undefined"!=typeof t.totBoxes){if(!J.isInt(t.totBoxes,0,1e4,!1,!0))throw new TypeError("Bomb.init: maxBoxes must be an integer > 0 and <= 10000 or undefined. Found: "+t.totBoxes);this.totBoxes=t.totBoxes}else this.totBoxes=100;if("undefined"!=typeof t.maxBoxes){if(!J.isInt(t.maxBoxes,0,this.totBoxes))throw new TypeError("Bomb.init: maxBoxes must be a positive integer <= "+this.totBoxes+" or undefined. Found: "+t.maxBoxes);this.maxBoxes=t.maxBoxes}else this.maxBoxes=r===1?this.totBoxes-1:this.totBoxes;if("undefined"!=typeof t.boxesInRow){if(!J.isInt(t.boxesInRow,0))throw new TypeError("Bomb.init: boxesInRow must be a positive integer or undefined. Found: "+t.boxesInRow);this.boxesInRow=t.boxesInRow>this.totBoxes?this.totBoxes:t.boxesInRow}else this.boxesInRow=this.totBoxes<10?this.totBoxes:10;return this.withPrize="undefined"==typeof t.withPrize?!0:!!t.withPrize,i=Math.random()>=r?-1:Math.ceil(Math.random()*this.totBoxes),{setValues:function(e){f.setValues(e)},getValues:function(e){var t,r,i,s;return e=e||{},r=f.getValues(),"undefined"!=typeof h?(i=h,s=!0):(i=parseInt(f.slider.value,10),s=!1),t={value:i,isCorrect:s,totalMove:r.totalMove,isWinner:c,time:r.time,reward:0},!t.isCorrect&&("undefined"==typeof e.highlight||e.highlight)&&f.highlight(),c===!0&&(t.reward=h*n.boxValue),t},highlight:function(){f.highlight()},unhighlight:function(){f.unhighlight()},append:function(){var t;W.add("div",n.bodyDiv,{innerHTML:n.mainText||n.getText("bomb_mainText",r)}),f=e.widgets.add("Slider",n.bodyDiv,{min:0,max:n.maxBoxes,hint:n.getText("bomb_sliderHint"),title:!1,initialValue:0,displayValue:!1,displayNoChange:!1,type:"flat",required:!0,panel:!1,onmove:function(e){var t,r,i,o;n._unhighlight(),e>0?(l.style.display="",l.disabled=!1,a.innerHTML=""):(l.style.display="none",a.innerHTML=n.getText("bomb_warn"),l.disabled=!0);for(t=0;tt?r.style.background="#1be139":r.style.background="#000000";W.gid("bomb_numBoxes").innerText=e,i=n.currency,o=n.boxValue,n.withPrize&&(W.gid("bomb_boxValue").innerText=o+i,W.gid("bomb_totalWin").innerText=Number(e*o).toFixed(2)+i)},storeRef:!1,width:"100%"}),t=Math.ceil(n.totBoxes/n.boxesInRow),W.add("div",n.bodyDiv,{innerHTML:u(t,n.boxesInRow,n.totBoxes)}),o=W.add("div",n.bodyDiv,{className:"risk-info"}),W.add("p",o,{innerHTML:n.getText("bomb_numBoxes")+' 0'}),n.withPrize&&(W.add("p",o,{innerHTML:n.getText("bomb_boxValue")+' '+this.boxValue+""}),W.add("p",o,{innerHTML:n.getText("bomb_totalWin")+' 0'})),a=W.add("p",o,{id:"bomb_result"}),l=W.add("button",n.bodyDiv,{className:"btn-danger",innerHTML:n.getText("bomb_openButton")}),l.style.display="none",l.onclick=function(){var e;h=parseInt(f.slider.value,10),i>-1?(W.gid(s(i-1)).style.background="#fa0404",c=hn){i=i+'';break}i=i+'
'}return i+="",i}function u(e,t,n){var r,i,s,u;i='';for(r=0;rn&&(u=n-r*t-1),i+=o(r,t,u);return i+="

",i}e.widgets.register("RiskGauge",t),t.version="0.8.0",t.description="Displays an interface to measure risk preferences with different methods.",t.title="Risk Gauge",t.className="riskgauge",t.texts={holt_laury_mainText:"Below you find a series of hypothetical lotteries, each contains two lotteries with different probabalities of winning. In each row, select the lottery you would rather take part in.",bomb_mainText:function(e,t){var n;return n='

',n+="Below there are "+e.totBoxes+" black boxes. ",n+="Every box contains a prize of "+e.boxValue+" "+e.currency+", but ",t===1?n+="one random box contains a bomb.":e.revealProbBomb?n+="with probability "+t+" one random box contains a bomb.":n+="one random box might contain a bomb.",n+=" You must decide how many boxes you want to open.",n+="

",e.withPrize&&(n+='

',n+="You will receive a reward equal to the sum of all the prizes in every opened box. However, if you open the box with the bomb, you get nothing.

"),n+='

',n+="How many boxes do you want to open ",n+="between 1 and "+e.maxBoxes+"?

",n},bomb_sliderHint:'Move the slider to choose the number of boxes to open, then click "Open Boxes"',bomb_boxValue:"Prize per box: ",bomb_numBoxes:"Number of boxes: ",bomb_totalWin:"Total reward: ",bomb_openButton:"Open Boxes",bomb_warn:"Open at least one box.",bomb_won:"You won! You did not open the box with the bomb.",bomb_lost:"You lost! You opened the box with the bomb."},t.texts.mainText=t.texts.holt_laury_mainText,t.dependencies={JSUS:{}},t.prototype.init=function(t){var n,r;if("undefined"!=typeof t.method){if("string"!=typeof t.method)throw new TypeError("RiskGauge.init: method must be string or undefined: "+t.method);if(!this.methods[t.method])throw new Error("RiskGauge.init: method is invalid: "+t.method);this.method=t.method}if(t.mainText){if("string"!=typeof t.mainText)throw new TypeError("RiskGauge.init: mainText must be string or undefined. Found: "+t.mainText);this.mainText=t.mainText}n=this.methods[this.method].call(this,t),r=this,n.isHidden=function(){return r.isHidden()},n.isCollapsed=function(){return r.isCollapsed()};if(!e.widgets.isWidget(n))throw new Error("RiskGauge.init: method "+this.method+" created invalid gauge: missing default widget "+"methods.");this.gauge=n,this.on("enabled",function(){n.enable&&n.enable()}),this.on("disabled",function(){n.disable&&n.disable()}),this.on("highlighted",function(){n.highlight&&n.highlight()}),this.on("unhighlighted",function(){n.unhighlight&&n.unhighlight()})},t.prototype.append=function(){e.widgets.append(this.gauge,this.bodyDiv,{panel:!1})},t.prototype.addMethod=function(e,t){if("string"!=typeof e)throw new TypeError("RiskGauge.addMethod: name must be string: "+e);if("function"!=typeof t)throw new TypeError("RiskGauge.addMethod: cb must be function: "+t);if(this.methods[e])throw new Error("RiskGauge.addMethod: name already existing: "+e);this.methods[e]=t},t.prototype.getValues=function(e){return this.gauge.getValues(e)},t.prototype.setValues=function(e){return this.gauge.setValues(e)}}(node),function(e){"use strict";function i(){this.sdo=null,this.scale="SDO7s",this.choices=n,this.header=r,this.mainText=null}e.widgets.register("SDO",i),i.version="0.3.0",i.description="Displays an interface to measure Social Dominance Orientation (S.D.O.).",i.title="SDO",i.className="SDO";var t={SDO7:["Some groups of people must be kept in their place.","It's probably a good thing that certain groups are at the top and other groups are at the bottom.","An ideal society requires some groups to be on top and others to be on the bottom.","Some groups of people are simply inferior to other groups.","Groups at the bottom are just as deserving as groups at the top.","No one group should dominate in society.","Groups at the bottom should not have to stay in their place.","Group dominance is a poor principle.","We should not push for group equality.","We shouldn't try to guarantee that every group has the same quality of life.","It is unjust to try to make groups equal.","Group equality should not be our primary goal.","We should work to give all groups an equal chance to succeed.","We should do what we can to equalize conditions for different groups.","No matter how much effort it takes, we ought to strive to ensure that all groups have the same chance in life.","Group equality should be our ideal."],SDO16:["Some groups of people are simply inferior to other groups.","In getting what you want, it is sometimes necessary to use force against other groups.","It's OK if some groups have more of a chance in life than others.","To get ahead in life, it is sometimes necessary to step on other groups.","If certain groups stayed in their place, we would have fewer problems.","It's probably a good thing that certain groups are at the top and other groups are at the bottom.","Inferior groups should stay in their place.","Sometimes other groups must be kept in their place.","It would be good if groups could be equal.","Group equality should be our ideal.","All groups should be given an equal chance in life.","We should do what we can to equalize conditions for different groups.","Increased social equality is beneficial to society.","We would have fewer problems if we treated people more equally.","We should strive to make incomes as equal as possible.","No group should dominate in society."]};t.SDO7s=[t.SDO7[2],t.SDO7[3],t.SDO7[5],t.SDO7[6],t.SDO7[11],t.SDO7[10],t.SDO7[13],t.SDO7[12]];var n=[1,2,3,4,5,6,7],r=["Strongly Oppose","Somewhat Oppose","Slightly Oppose","Neutral","Slightly Favor","Somewhat Favor","Strongly Favor"];i.texts={mainText:"Show how much you favor or oppose each idea below by selecting a number from 1 to 7 on the scale below. You can work quickly, your first feeling is generally best."},i.dependencies={},i.prototype.init=function(e){e=e||{};if(e.scale){if(e.scale!=="SDO16"&&e.scale!=="SDO7"&&e.scale!=="SDO7s")throw new Error("SDO.init: scale must be SDO16, SDO7, SDO7s or undefined. Found: "+e.scale);this.scale=e.scale}if(e.choices){if(!J.isArray(e.choices)||e.choices.length<2)throw new Error("SDO.init: choices must be an array of length > 1 or undefined. Found: "+e.choices);this.choices=e.choices}if(e.header){if(!J.isArray(e.header)||e.header.length!==this.choices.length)throw new Error("SDO.init: header must be an array of length equal to the number of choices or undefined. Found: "+e.header);this.header=e.header}if(e.mainText){if("string"!=typeof e.mainText&&e.mainText!==!1)throw new Error("SDO.init: mainText must be string, false, or undefined. Found: "+e.mainText);this.mainText=e.mainText}},i.prototype.append=function(){this.sdo=e.widgets.add("ChoiceTableGroup",this.panelDiv,{id:this.id||"SDO_choicetable",items:this.getItems(this.scale),choices:this.choices,mainText:this.mainText||this.getText("mainText"),title:!1,panel:!1,requiredChoice:this.required,header:this.header})},i.prototype.getItems=function(){var e=this.scale;return t[e].map(function(t,n){return[e+"_"+(n+1),t]})},i.prototype.getValues=function(e){return e=e||{},this.sdo.getValues(e)},i.prototype.setValues=function(e){return e=e||{},this.sdo.setValues(e)},i.prototype.enable=function(e){return this.sdo.enable(e)},i.prototype.disable=function(e){return this.sdo.disable(e)},i.prototype.highlight=function(e){return this.sdo.highlight(e)},i.prototype.unhighlight=function(e){return this.sdo.unhighlight(e)}}(node),function(e){"use strict";function t(){var e;e=this,this.slider=null,this.rangeFill=null,this.scale=1,this.currentValue=50,this.initialValue=50,this.mainText=null,this.required=null,this.requiredChoice=null,this.hint=null,this.min=0,this.max=100,this.correctValue=null,this.displayValue=!0,this.valueSpan=null,this.displayNoChange=!0,this.noChangeSpan=null,this.totalMove=0,this.type="volume",this.hoverColor="#2076ea";var t=null;this.listener=function(n){if(!n&&t)return;e.isHighlighted()&&e.unhighlight(),t=setTimeout(function(){var r,i;r=(e.slider.value-e.min)*e.scale,i=r-e.currentValue,e.currentValue=r,e.type==="volume"?(r>99&&(r=99),e.rangeFill.style.width=r+"%"):e.rangeFill.style.width="99%",e.displayValue&&(e.valueSpan.innerHTML=e.getText("currentValue",e.slider.value)),e.displayNoChange&&n!==!0&&e.noChangeCheckbox.checked&&(e.noChangeCheckbox.checked=!1,J.removeClass(e.noChangeSpan,"italic")),e.totalMove+=Math.abs(i),e.onmove&&e.onmove.call(e,e.slider.value,i),t=null},0)},this.onmove=null,this.timeFrom="step"}e.widgets.register("Slider",t),t.version="0.4.0",t.description="Creates a configurable slider",t.title=!1,t.className="slider",t.texts={currentValue:function(e,t){return"Value: "+t},noChange:"No change"},t.prototype.init=function(e){var t,n;n="Slider.init: ";if("undefined"!=typeof e.min){t=J.isInt(e.min);if("number"!=typeof t)throw new TypeError(n+"min must be an integer or "+"undefined. Found: "+e.min);this.min=t}if("undefined"!=typeof e.max){t=J.isInt(e.max);if("number"!=typeof t)throw new TypeError(n+"max must be an integer or "+"undefined. Found: "+e.max);this.max=t}this.scale=100/(this.max-this.min),t=e.initialValue;if("undefined"!=typeof t){if(t==="random")t=J.randomInt(this.min-1,this.max);else{t=J.isInt(t,this.min,this.max,!0,!0);if("number"!=typeof t)throw new TypeError(n+"initialValue must be an "+"integer >= "+this.min+" and =< "+this.max+" or undefined. Found: "+e.initialValue)}this.initialValue=this.currentValue=t}"undefined"!=typeof e.displayValue&&(this.displayValue=!!e.displayValue),"undefined"!=typeof e.displayNoChange&&(this.displayNoChange=!!e.displayNoChange);if(e.type){if(e.type!=="volume"&&e.type!=="flat")throw new TypeError(n+'type must be "volume", "flat", or '+"undefined. Found: "+e.type);this.type=e.type}t=e.requiredChoice,"undefined"!=typeof t?console.log("***Slider.init: requiredChoice is deprecated. Use required instead.***"):"undefined"!=typeof e.required&&(t=e.required),"undefined"!=typeof t&&(this.requiredChoice=this.required=!!t);if(e.mainText){if("string"!=typeof e.mainText)throw new TypeError(n+"mainText must be string or "+"undefined. Found: "+e.mainText);this.mainText=e.mainText}if("undefined"!=typeof e.hint){if(!1!==e.hint&&"string"!=typeof e.hint)throw new TypeError(n+"hint must be a string, false, or "+"undefined. Found: "+e.hint);this.hint=e.hint}this.required&&this.hint!==!1&&(this.hint||(this.hint="Movement required"),this.hint+=" *");if(e.onmove){if("function"!=typeof e.onmove)throw new TypeError(n+"onmove must be a function or "+"undefined. Found: "+e.onmove);this.onmove=e.onmove}if(e.width){if("string"!=typeof e.width)throw new TypeError(n+"width must be string or "+"undefined. Found: "+e.width);this.sliderWidth=e.width}if(e.hoverColor){if("string"!=typeof e.hoverColor)throw new TypeError(n+"hoverColor must be string or "+"undefined. Found: "+e.hoverColor);this.hoverColor=e.hoverColor}if("undefined"!=typeof e.correctValue){if(!1===J.isNumber(e.correctValue,this.min,this.max,!0,!0))throw new Error(n+"correctValue must be a number between "+this.min+" and "+this.max+". Found: "+e.correctValue);this.correctValue=e.correctValue}},t.prototype.append=function(){var e,t,n=this;this.mainText&&(this.spanMainText=W.append("span",this.bodyDiv,{className:"slider-maintext",innerHTML:this.mainText})),this.hint&&W.append("span",this.bodyDiv,{className:"slider-hint",innerHTML:this.hint}),e=W.add("div",this.bodyDiv,{className:"container-slider"}),this.rangeFill=W.add("div",e,{className:"fill-slider"}),this.slider=W.add("input",e,{className:"volume-slider",name:"rangeslider",type:"range",min:this.min,max:this.max}),this.slider.onmouseover=function(){t=n.rangeFill.style.background||"black",n.rangeFill.style.background=n.hoverColor},this.slider.onmouseout=function(){n.rangeFill.style.background=t},this.sliderWidth&&(this.slider.style.width=this.sliderWidth),this.displayValue&&(this.valueSpan=W.add("span",this.bodyDiv,{className:"slider-display-value"})),this.displayNoChange&&(this.noChangeSpan=W.add("span",this.bodyDiv,{className:"slider-display-nochange",innerHTML:this.getText("noChange")+" "}),this.noChangeCheckbox=W.add("input",this.noChangeSpan,{type:"checkbox"}),this.noChangeCheckbox.onclick=function(){if(n.noChangeCheckbox.checked){if(n.slider.value===n.initialValue)return;n.slider.value=n.initialValue,n.listener(!0),J.addClass(n.noChangeSpan,"italic")}else J.removeClass(n.noChangeSpan,"italic")}),this.slider.oninput=this.listener,this.slider.value=this.initialValue,this.slider.oninput()},t.prototype.getValues=function(t){var n,r,i;t=t||{},n=!0,"undefined"==typeof t.highlight&&(t.highlight=!0),r=this.currentValue,i=this.noChangeCheckbox&&this.noChangeCheckbox.checked;if(this.required&&this.totalMove===0&&!i||null!==this.correctValue&&this.correctValue!==r)t.highlight&&this.highlight(),n=!1;return{value:r,noChange:!!i,initialValue:this.initialValue,totalMove:this.totalMove,isCorrect:n,time:e.timer.getTimeSince(this.timeFrom)}},t.prototype.setValues=function(e){e=e||{},this.slider.value=e.value,this.slider.oninput()}}(node),function(e){"use strict";function t(){this.methods={},this.method="Slider",this.mainText=null,this.gauge=null,this.addMethod("Slider",n)}function n(t){var n,r,i,s,o,u,a;r=t.sliders||[[[85,85],[85,76],[85,68],[85,59],[85,50],[85,41],[85,33],[85,24],[85,15]],[[85,15],[87,19],[89,24],[91,28],[93,33],[94,37],[96,41],[98,46],[100,50]],[[50,100],[54,98],[59,96],[63,94],[68,93],[72,91],[76,89],[81,87],[85,85]],[[50,100],[54,89],[59,79],[63,68],[68,58],[72,47],[76,36],[81,26],[85,15]],[[100,50],[94,56],[88,63],[81,69],[75,75],[69,81],[63,88],[56,94],[50,100]],[[100,50],[98,54],[96,59],[94,63],[93,68],[91,72],[89,76],[87,81],[85,85]]],this.sliders=r,a=t.renderer||function(e,t,n){e.innerHTML=t[0]+"
"+t[1]},u=r.length,n=new Array(u),o=-1;for(;++oextra bonus.
Choose the preferred bonus amounts (in cents) for you and the other participant in each row.
We will select one row at random and add the bonus to your and the other participant's payment. Your choice will remain anonymous.",left:"Your Bonus:
Other's Bonus:"},t.dependencies={},t.prototype.init=function(t){var n,r;if("undefined"!=typeof t.method){if("string"!=typeof t.method)throw new TypeError("SVOGauge.init: method must be string or undefined. Found: "+t.method);if(!this.methods[t.method])throw new Error("SVOGauge.init: method is invalid: "+t.method);this.method=t.method}if("undefined"!=typeof t.mainText){if(t.mainText!==!1&&"string"!=typeof t.mainText)throw new TypeError("SVOGauge.init: mainText must be string false, or undefined. Found: "+t.mainText);this.mainText=t.mainText}n=this.methods[this.method].call(this,t),r=this,n.isHidden=function(){return r.isHidden()},n.isCollapsed=function(){return r.isCollapsed()};if(!e.widgets.isWidget(n))throw new Error("SVOGauge.init: method "+this.method+" created invalid gauge: missing default widget "+"methods.");this.gauge=n,this.on("enabled",function(){n.enable()}),this.on("disabled",function(){n.disable()}),this.on("highlighted",function(){n.highlight()}),this.on("unhighlighted",function(){n.unhighlight()})},t.prototype.append=function(){e.widgets.append(this.gauge,this.bodyDiv)},t.prototype.addMethod=function(e,t){if("string"!=typeof e)throw new Error("SVOGauge.addMethod: name must be string: "+e);if("function"!=typeof t)throw new Error("SVOGauge.addMethod: cb must be function: "+t);if(this.methods[e])throw new Error("SVOGauge.addMethod: name already existing: "+e);this.methods[e]=t},t.prototype.getValues=function(e){return e=e||{},"undefined"==typeof e.processChoice&&(e.processChoice=function(e){return e===null?null:this.choices[e]}),this.gauge.getValues(e)},t.prototype.setValues=function(e){return this.gauge.setValues(e)}}(node),function(e){"use strict";function t(){this.options=null,this.displayMode=null,this.stager=null,this.gamePlot=null,this.curStage=null,this.totStage=null,this.curRound=null,this.totRound=null,this.stageOffset=null,this.totStageOffset=null,this.oldStageId=null,this.separator=" / ",this.layout=null}function n(e,t){l(this,e,"COUNT_UP_STAGES",t),c(this,"stagediv",this.visualRound.getText("stage"))}function r(e,t){l(this,e,"COUNT_DOWN_STAGES",t),c(this,"stagediv",e.getText("stageLeft"))}function i(e,t){l(this,e,"COUNT_UP_STEPS",t),c(this,"stepdiv",this.visualRound.getText("step"))}function s(e,t){l(this,e,"COUNT_DOWN_STEPS",t),c(this,"stepdiv",this.visualRound.getText("stepLeft"))}function o(e,t){l(this,e,"COUNT_UP_ROUNDS",t),c(this,"rounddiv",e.getText("round"))}function u(e,t){l(this,e,"COUNT_DOWN_ROUNDS",t),c(this,"rounddiv",e.getText("roundLeft"))}function a(e,t,n){this.visualRound=e,this.displayModes=t,this.name=t.join("&"),this.options=n||{},this.displayDiv=null,this.init(n)}function f(e,t,n){return t==="vertical"||t==="multimode_vertical"||t==="all_vertical"?(e.displayDiv.style.float="none",e.titleDiv.style.float="none",e.titleDiv.style["margin-right"]="0px",e.contentDiv.style.float="none",!0):t==="horizontal"?(e.displayDiv.style.float="none",e.titleDiv.style.float="left",e.titleDiv.style["margin-right"]="6px",e.contentDiv.style.float="right",!0):t==="multimode_horizontal"?(e.displayDiv.style.float="left",e.titleDiv.style.float="none",e.titleDiv.style["margin-right"]="0px",e.contentDiv.style.float="none",n||(e.displayDiv.style["margin-right"]="10px"),!0):t==="all_horizontal"?(e.displayDiv.style.float="left",e.titleDiv.style.float="left",e.titleDiv.style["margin-right"]="6px",e.contentDiv.style.float="right",n||(e.displayDiv.style["margin-right"]="10px"),!0):!1}function l(e,t,n,r){r=r||{},e.visualRound=t,e.name=n,r.toTotal&&(e.name+="_TO_TOTAL"),e.options=r,e.displayDiv=null,e.titleDiv=null,e.contentDiv=null,e.current=null,e.textDiv=null,e.total=null}function c(e,t,n){e.displayDiv=W.get("div",{className:t}),e.titleDiv=W.add("div",e.displayDiv,{className:"title",innerHTML:n}),e.contentDiv=W.add("div",e.displayDiv,{className:"content"}),e.current=W.append("span",e.contentDiv,{className:"number"}),e.options.toTotal&&(e.textDiv=W.append("span",e.contentDiv,{className:"text",innerHTML:e.visualRound.separator}),e.total=W.append("span",e.contentDiv,{className:"number"})),e.updateDisplay()}e.widgets.register("VisualRound",t),t.version="0.9.0",t.description="Displays current/total/left round/stage/step. ",t.title=!1,t.className="visualround",t.texts={round:"Round",step:"Step",stage:"Stage",roundLeft:"Rounds Left",stepLeft:"Steps Left",stageLeft:"Stages Left"},t.dependencies={GamePlot:{}},t.prototype.init=function(t){t=t||{},J.mixout(t,this.options),this.options=t,this.stageOffset=this.options.stageOffset||0,this.totStageOffset="undefined"==typeof this.options.totStageOffset?this.stageOffset:this.options.totStageOffset,this.options.flexibleMode&&(this.curStage=this.options.curStage||1,this.curStage-=this.options.stageOffset||0,this.curStep=this.options.curStep||1,this.curRound=this.options.curRound||1,this.totStage=this.options.totStage,this.totRound=this.options.totRound,this.totStep=this.options.totStep,this.oldStageId=this.options.oldStageId),this.gamePlot||(this.gamePlot=e.game.plot),this.stager||(this.stager=this.gamePlot.stager),this.updateInformation(),!this.options.displayMode&&this.options.displayModeNames&&(console.log("***VisualTimer.init: options.displayModeNames is deprecated. Use options.displayMode instead.***"),this.options.displayMode=this.options.displayModeNames),this.options.displayMode?this.setDisplayMode(this.options.displayMode):this.setDisplayMode(["COUNT_UP_ROUNDS_TO_TOTAL_IFNOT1","COUNT_UP_STAGES_TO_TOTAL"]),"undefined"!=typeof t.separator&&(this.separator=t.separator),"undefined"!=typeof t.layout&&(this.layout=t.layout);if("undefined"!=typeof t.preprocess){if("function"!=typeof t.preprocess)throw new TypeError("VisualRound.init: preprocess must function or undefined. Found: "+t.preprocess);this.preprocess=t.preprocess}this.updateDisplay()},t.prototype.append=function(){this.activate(this.displayMode),this.updateDisplay()},t.prototype.updateDisplay=function(){this.displayMode&&this.displayMode.updateDisplay()},t.prototype.setDisplayMode=function(e){var t,f,l;if("string"==typeof e)e=[e];else if(!J.isArray(e))throw new TypeError("VisualRound.setDisplayMode: displayMode must be array or string. Found: "+e);f=e.length;if(f===0)throw new Error("VisualRound.setDisplayMode: displayMode is empty");if(this.displayMode){if(e.join("&")===this.displayMode.name)return;this.deactivate(this.displayMode)}l=[],t=-1;for(;++tt.stage?i=s:i=1,i}function i(e){var t,n,r;t=e.split(" "),e=s(t[0]),r=t.length,r>1&&(e+=" "+s(t[1]));if(r>2)for(n=2;n'+e.getText(i)+""+r),W.add("span",e.div,{innerHTML:r,className:"visualstage-"+i})}function a(e,t){var n;n=t.indexOf(e[0]);if(n===-1)return"unknown item: "+e[0];t.splice(n,1),n=t.indexOf(e[1]);if(n===-1)return"unknown item: "+e[1];t.splice(n,1),n=t.indexOf(e[2]);if(n===-1)return"unknown item: "+e[2];t.splice(n,1);if(t.length)return"duplicated entry: "+t[0];return}var t=W.Table;e.widgets.register("VisualStage",n),n.version="0.11.0",n.description="Displays the name of the current, previous and next step of the game.",n.title=!1,n.className="visualstage",n.texts={miss:"",current:"Stage: ",previous:"Prev: ",next:"Next: "},n.dependencies={Table:{}},n.prototype.init=function(e){var t;if("undefined"!=typeof e.displayMode){if(e.displayMode!=="inline"&&e.displayMode!=="table")throw new TypeError('VisualStage.init: displayMode must be "inline", "table" or undefined. Found: '+e.displayMode);this.displayMode=e.displayMode}"undefined"!=typeof e.addRound&&(this.addRound=!!e.addRound),"undefined"!=typeof e.previous&&(this.showPrevious=!!e.previous),"undefined"!=typeof e.next&&(this.showNext=!!e.next),"undefined"!=typeof e.current&&(this.showCurrent=!!e.current);if("undefined"!=typeof e.order){if(!J.isArray(e.order)||e.order.length!==3)throw new TypeError("VisualStage.init: order must be an array of length 3 or undefined. Found: "+e.order);t=a(e.order,this.order.slice(0));if(t)throw new TypeError("VisualStage.init: order contains errors: "+e.order);this.order=e.order}else this.displayMode==="inline"&&(this.order=["previous","current","next"]);if("undefined"!=typeof e.preprocess){if("function"!=typeof e.preprocess)throw new TypeError("VisualStage.init: preprocess must be function or undefined. Found: "+e.preprocess);this.preprocess=e.preprocess}"undefined"!=typeof e.capitalize&&(this.capitalize=!!e.capitalize),"undefined"!=typeof e.replaceUnderscore&&(this.replaceUnderscore=!!e.replaceUnderscore)},n.prototype.append=function(){this.displayMode==="table"?(this.table=new t,this.bodyDiv.appendChild(this.table.table)):this.div=W.append("div",this.bodyDiv),this.updateDisplay()},n.prototype.listeners=function(){var t=this;e.on("STEP_CALLBACK_EXECUTED",function(){t.updateDisplay()})},n.prototype.updateDisplay=function(){var t,n,r,i,s,a,f,l;f={},t=e.game.getCurrentGameStage(),t&&(this.showCurrent&&(i=this.getStepName(t,t,"current"),f.current=i),this.showNext&&(n=e.game.plot.next(t),n&&(s=this.getStepName(n,t,"next"),f.next=s)),this.showPrevious&&(r=e.game.plot.previous(t),r&&(a=this.getStepName(r,t,"previous"),f.previous=a))),this.displayMode==="table"?(this.table.clear(!0),o(this,0,f),o(this,1,f),o(this,2,f),l=this.table.selexec("y","=",0),l.addClass("strong"),this.table.parse()):(this.div.innerHTML="",u(this,0,f),u(this,1,f),u(this,2,f))},n.prototype.getStepName=function(t,n,s){var o,u,a,f;return o=e.game.plot.getProperty(t,"name"),"function"==typeof o?(a=o,o=null):"object"==typeof o&&o!==null&&(a=o.preprocess,f=o.addRound,o=o.name),o||(o=e.game.plot.getStep(t),o?(o=o.id,this.replaceUnderscore&&(o=o.replace(/_/g," ")),this.capitalize&&(o=i(o))):o=this.getText("miss")),a||(a=this.preprocess),"undefined"==typeof f&&(f=this.addRound),u=r(t,n,s),a&&(o=a.call(e.game,o,s,u)),f&&u&&(o+=" "+u),o}}(node),function(e){"use strict";function t(){this.gameTimer=null,this.mainBox=null,this.waitBox=null,this.activeBox=null,this.isInitialized=!1,this.options={},this.internalTimer=null}function n(e){this.boxDiv=null,this.titleDiv=null,this.bodyDiv=null,this.timeLeft=null,this.boxDiv=W.get("div"),this.titleDiv=W.add("div",this.boxDiv),this.bodyDiv=W.add("div",this.boxDiv),this.init(e)}function r(t){t.internalTimer?(t.gameTimer.isDestroyed()||e.timer.destroyTimer(t.gameTimer),t.internalTimer=null):t.gameTimer.removeHook("VisualTimer_"+t.wid)}e.widgets.register("VisualTimer",t),t.version="0.9.3",t.description="Display a configurable timer for the game. Can trigger events. Only for countdown smaller than 1h.",t.title="Time Left",t.className="visualtimer",t.dependencies={GameTimer:{}},t.prototype.init=function(t){var r,i;t=t||{};if("object"!=typeof t)throw new TypeError("VisualTimer.init: options must be object or undefined. Found: "+t);i={};if("undefined"!=typeof t.gameTimer){if(this.gameTimer)throw new Error("GameTimer.init: options.gameTimer cannot be set if a gameTimer is already existing: "+this.name);if("object"!=typeof t.gameTimer)throw new TypeError("VisualTimer.init: options.gameTimer must be object or undefined. Found: "+t.gameTimer);this.gameTimer=t.gameTimer}else this.isInitialized||(this.internalTimer=!0,this.gameTimer=e.timer.createTimer({name:t.name||"VisualTimer_"+J.randomInt(1e7)}));if(t.hooks){if(!this.internalTimer)throw new Error("VisualTimer.init: cannot add hooks on external gameTimer.");J.isArray(t.hooks)||(i.hooks=[t.hooks])}else i.hooks=[];this.isInitialized||i.hooks.push({name:"VisualTimer_"+this.wid,hook:this.updateDisplay,ctx:this}),"undefined"!=typeof t.milliseconds&&(i.milliseconds=e.timer.parseInput("milliseconds",t.milliseconds)),"undefined"!=typeof t.update?i.update=e.timer.parseInput("update",t.update):i.update=1e3,"undefined"!=typeof t.timeup&&(i.timeup=t.timeup),this.gameTimer.init(i),r=this.gameTimer,this.options=i,"undefined"==typeof this.options.stopOnDone&&(this.options.stopOnDone=!0),"undefined"==typeof this.options.startOnPlaying&&(this.options.startOnPlaying=!0),this.options.mainBoxOptions||(this.options.mainBoxOptions={}),this.options.waitBoxOptions||(this.options.waitBoxOptions={}),J.mixout(this.options.mainBoxOptions,{classNameBody:t.className,hideTitle:!0}),J.mixout(this.options.waitBoxOptions,{title:"Max. wait timer",classNameTitle:"waitTimerTitle",classNameBody:"waitTimerBody",hideBox:!0}),this.mainBox?this.mainBox.init(this.options.mainBoxOptions):this.mainBox=new n(this.options.mainBoxOptions),this.waitBox?this.waitBox.init(this.options.waitBoxOptions):this.waitBox=new n(this.options.waitBoxOptions),this.activeBox=this.options.activeBox||this.mainBox,this.isInitialized=!0},t.prototype.append=function(){this.bodyDiv.appendChild(this.mainBox.boxDiv),this.bodyDiv.appendChild(this.waitBox.boxDiv),this.activeBox=this.mainBox,this.updateDisplay()},t.prototype.clear=function(e){var t;return e=e||{},t=this.options,r(this),this.gameTimer=null,this.activeBox=null,this.isInitialized=!1,this.init(e),t},t.prototype.updateDisplay=function(){var e,t,n;if(!this.gameTimer.milliseconds||this.gameTimer.milliseconds===0){this.activeBox.bodyDiv.innerHTML="00:00";return}e=this.gameTimer.milliseconds-this.gameTimer.timePassed,e=J.parseMilliseconds(e),t=e[2]<10?"0"+e[2]:e[2],n=e[3]<10?"0"+e[3]:e[3],this.activeBox.bodyDiv.innerHTML=t+":"+n},t.prototype.start=function(){this.updateDisplay(),this.gameTimer.start()},t.prototype.restart=function(e){this.stop(),"number"==typeof e&&(e={milliseconds:e}),this.init(e),this.start()},t.prototype.stop=function(){this.gameTimer.isStopped()||(this.activeBox.timeLeft=this.gameTimer.timeLeft,this.gameTimer.stop())},t.prototype.switchActiveBoxTo=function(e){this.activeBox.timeLeft=this.gameTimer.timeLeft||0,this.activeBox=e,this.updateDisplay()},t.prototype.startWaiting=function(e){"undefined"==typeof e&&(e={}),"undefined"==typeof e.milliseconds&&(e.milliseconds=this.gameTimer.timeLeft),"undefined"==typeof e.mainBoxOptions&&(e.mainBoxOptions={}),"undefined"==typeof e.waitBoxOptions&&(e.waitBoxOptions={}),e.mainBoxOptions.classNameBody="strike",e.mainBoxOptions.timeLeft=this.gameTimer.timeLeft||0,e.activeBox=this.waitBox,e.waitBoxOptions.hideBox=!1,this.restart(e)},t.prototype.startTiming=function(e){"undefined"==typeof e&&(e={}),"undefined"==typeof e.mainBoxOptions&&(e.mainBoxOptions={}),"undefined"==typeof e.waitBoxOptions&&(e.waitBoxOptions={}),e.activeBox=this.mainBox,e.waitBoxOptions.timeLeft=this.gameTimer.timeLeft||0,e.waitBoxOptions.hideBox=!0,e.mainBoxOptions.classNameBody="",this.restart(e)},t.prototype.resume=function(){this.gameTimer.resume()},t.prototype.setToZero=function(){this.stop(),this.activeBox.bodyDiv.innerHTML="00:00",this.activeBox.setClassNameBody("strike")},t.prototype.isTimeup=function(){return this.gameTimer.isTimeup()},t.prototype.doTimeUp=function(){this.gameTimer.doTimeUp()},t.prototype.listeners=function(){var t=this;if(!this.internalTimer)return;e.on("PLAYING",function(){var e;t.options.startOnPlaying&&(e=t.gameTimer.getStepOptions(),e?(e.update=t.update,e.timeup=undefined,t.startTiming(e)):t.gameTimer.isRunning()||t.setToZero())}),e.on("REALLY_DONE",function(){t.options.stopOnDone&&(t.gameTimer.isStopped()||t.stop())}),this.on("destroyed",function(){r(t),t.bodyDiv.removeChild(t.mainBox.boxDiv),t.bodyDiv.removeChild(t.waitBox.boxDiv)})},n.prototype.init=function(e){e&&(e.hideTitle?this.hideTitle():this.unhideTitle(),e.hideBody?this.hideBody():this.unhideBody(),e.hideBox?this.hideBox():this.unhideBox()),this.setTitle(e.title||""),this.setClassNameTitle(e.classNameTitle||""),this.setClassNameBody(e.classNameBody||""),e.timeLeft&&(this.timeLeft=e.timeLeft)},n.prototype.hideBox=function(){this.boxDiv.style.display="none"},n.prototype.unhideBox=function(){this.boxDiv.style.display=""},n.prototype.hideTitle=function(){this.titleDiv.style.display="none"},n.prototype.unhideTitle=function(){this.titleDiv.style.display=""},n.prototype.hideBody=function(){this.bodyDiv.style.display="none"},n.prototype.unhideBody=function(){this.bodyDiv.style.display=""},n.prototype.setTitle=function(e){this.titleDiv.innerHTML=e},n.prototype.setClassNameTitle=function(e){this.titleDiv.className=e},n.prototype.setClassNameBody=function(e){this.bodyDiv.className=e}}(node),function(e){"use strict";function t(){this.connected=0,this.poolSize=0,this.nGames=undefined,this.groupSize=0,this.waitTime=null,this.executionMode=null,this.startDate=null,this.timeoutId=null,this.execModeDiv=null,this.playerCount=null,this.startDateDiv=null,this.msgDiv=null,this.timerDiv=null,this.timer=null,this.dots=null,this.onTimeout=null,this.disconnectIfNotSelected=null,this.playWithBotOption=null,this.playBotBtn=null,this.selectTreatmentOption=null,this.selectedTreatment=null}e.widgets.register("WaitingRoom",t),t.version="1.3.0",t.description="Displays a waiting room for clients.",t.title="Waiting Room",t.className="waitingroom",t.dependencies={VisualTimer:{}},t.sounds={dispatch:"/sounds/doorbell.ogg"},t.texts={blinkTitle:"GAME STARTS!",waitingForConf:"Waiting to receive data",executionMode:function(e){return e.executionMode==="WAIT_FOR_N_PLAYERS"?"Waiting for All Players to Connect: ":e.executionMode==="WAIT_FOR_DISPATCH"?"Task will start soon. Please be patient.":"Task will start at:
"+e.startDate},disconnect:'You have been disconnected. Please try again later.

',waitedTooLong:"Waiting for too long. Please look for a HIT called Trouble Ticket and file a new trouble ticket reporting your experience.",notEnoughPlayers:'

Thank you for your patience.
Unfortunately, there are not enough participants in your group to start the experiment.
',roomClosed:' The waiting room is CLOSED. You have been disconnected. Please try again later.

',tooManyPlayers:function(e,t){var n;return n="There are more players in this waiting room than playslots in the game. ",e.poolSize===1?n+="Each player will play individually.":n+="Only "+t.nGames+" players will be selected "+"to play the game.",n},notSelectedClosed:'

Unfortunately, you were not selected to join the game this time. Thank you for your participation.



',notSelectedOpen:'

Unfortunately, you were not selected to join the game this time, but you may join the next one.Ok, I got it.



Thank you for your participation.

',exitCode:function(e,t){return"
You have been disconnected. "+("undefined"!=typeof t.exit?"Please report this exit code: "+t.exit:"")+"
"},playBot:function(e){return e.poolSize===e.groupSize&&e.groupSize===1?"Play":e.groupSize===2?"Play With Bot":"Play With Bots"},connectingBots:function(e){return console.log(e.poolSize,e.groupSize),e.poolSize===e.groupSize&&e.groupSize===1?"Starting, Please Wait...":e.groupSize===2?"Connecting Bot, Please Wait...":"Connecting Bot/s, Please Wait..."},selectTreatment:"Select Treatment ",gameTreatments:"Game:",defaultTreatments:"Defaults:"},t.prototype.init=function(t){var n=this;if("object"!=typeof t)throw new TypeError("WaitingRoom.init: conf must be object. Found: "+t);if(!t.executionMode)return;this.executionMode=t.executionMode;if(t.onTimeout){if("function"!=typeof t.onTimeout)throw new TypeError("WaitingRoom.init: conf.onTimeout must be function, null or undefined. Found: "+t.onTimeout);this.onTimeout=t.onTimeout}if(t.waitTime){if(null!==t.waitTime&&"number"!=typeof t.waitTime)throw new TypeError("WaitingRoom.init: conf.waitTime must be number, null or undefined. Found: "+t.waitTime);this.waitTime=t.waitTime}t.startDate&&(this.startDate=(new Date(t.startDate)).toString());if(t.poolSize){if(t.poolSize&&"number"!=typeof t.poolSize)throw new TypeError("WaitingRoom.init: conf.poolSize must be number or undefined. Found: "+t.poolSize);this.poolSize=t.poolSize}if(t.groupSize){if(t.groupSize&&"number"!=typeof t.groupSize)throw new TypeError("WaitingRoom.init: conf.groupSize must be number or undefined. Found: "+t.groupSize);this.groupSize=t.groupSize}if(t.nGames){if(t.nGames&&"number"!=typeof t.nGames)throw new TypeError("WaitingRoom.init: conf.nGames must be number or undefined. Found: "+t.nGames);this.nGames=t.nGames}if(t.connected){if(t.connected&&"number"!=typeof t.connected)throw new TypeError("WaitingRoom.init: conf.connected must be number or undefined. Found: "+t.connected);this.connected=t.connected}if(t.disconnectIfNotSelected){if("boolean"!=typeof t.disconnectIfNotSelected)throw new TypeError("WaitingRoom.init: conf.disconnectIfNotSelected must be boolean or undefined. Found: "+t.disconnectIfNotSelected);this.disconnectIfNotSelected=t.disconnectIfNotSelected}else this.disconnectIfNotSelected=!1;t.playWithBotOption?this.playWithBotOption=!0:this.playWithBotOption=!1,t.selectTreatmentOption?this.selectTreatmentOption=!0:this.selectTreatmentOption=!1,this.displayExecMode(),this.playWithBotOption&&!document.getElementById("bot_btn")&&function(n){var r=document.createElement("div");r.role="group",r["aria-label"]="Play Buttons",r.className="btn-group";var i=document.createElement("input");i.className="btn btn-primary btn-lg",i.value=n.getText("playBot"),i.id="bot_btn",i.type="button",i.onclick=function(){n.playBotBtn.value=n.getText("connectingBots"),n.playBotBtn.disabled=!0,e.say("PLAYWITHBOT","SERVER",n.selectedTreatment),setTimeout(function(){n.playBotBtn.value=n.getText("playBot"),n.playBotBtn.disabled=!1},5e3)},r.appendChild(i),n.playBotBtn=i;if(n.selectTreatmentOption){var s=document.createElement("div");s.role="group",s["aria-label"]="Select Treatment",s.className="btn-group";var o=document.createElement("button");o.className="btn btn-default btn-lg dropdown-toggle",o["data-toggle"]="dropdown",o["aria-haspopup"]="true",o["aria-expanded"]="false",o.innerHTML=n.getText("selectTreatment");var u=document.createElement("span");u.className="caret",o.appendChild(u);var a=document.createElement("ul");a.className="dropdown-menu",a.style["text-align"]="left";var f,l,c,h,p,d;if(t.availableTreatments){f=document.createElement("li"),f.innerHTML=n.getText("gameTreatments"),f.className="dropdown-header",a.appendChild(f);for(c in t.availableTreatments)t.availableTreatments.hasOwnProperty(c)&&(f=document.createElement("li"),f.id=c,l=document.createElement("a"),l.href="#",l.innerHTML=""+c+": "+t.availableTreatments[c],f.appendChild(l),c==="treatment_latin_square"?d=f:c==="treatment_rotate"?h=f:c==="treatment_random"?p=f:a.appendChild(f));f=document.createElement("li"),f.role="separator",f.className="divider",a.appendChild(f),f=document.createElement("li"),f.innerHTML=n.getText("defaultTreatments"),f.className="dropdown-header",a.appendChild(f),a.appendChild(h),a.appendChild(p),a.appendChild(d)}s.appendChild(o),s.appendChild(a),r.appendChild(s),o.onclick=function(){a.style.display===""?a.style.display="block":a.style.display=""},a.onclick=function(e){var t;t=e.target,a.style.display="",t=t.parentNode.id,t||(t=e.target.parentNode.parentNode.id);if(!t)return;o.innerHTML=t+" ",o.appendChild(u),n.selectedTreatment=t},n.treatmentBtn=o}n.bodyDiv.appendChild(document.createElement("br")),n.bodyDiv.appendChild(r)}(this),this.on("destroyed",function(){n.dots&&n.dots.stop(),e.deregisterSetup("waitroom")})},t.prototype.startTimer=function(){var t=this;if(this.timer)return;if(!this.waitTime)return;this.timerDiv||(this.timerDiv=document.createElement("div"),this.timerDiv.id="timer-div"),this.timerDiv.appendChild(document.createTextNode("Maximum Waiting Time: ")),this.timer=e.widgets.append("VisualTimer",this.timerDiv,{milliseconds:this.waitTime,timeup:function(){t.bodyDiv.innerHTML=t.getText("waitedTooLong")},update:1e3}),this.timer.setTitle(),this.timer.panelDiv.className="ng_widget visualtimer",this.bodyDiv.appendChild(this.timerDiv),this.timer.start()},t.prototype.clearTimeout=function(){this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)},t.prototype.updateState=function(e){if(!e)return;"number"==typeof e.connected&&(this.connected=e.connected),"number"==typeof e.poolSize&&(this.poolSize=e.poolSize),"number"==typeof e.groupSize&&(this.groupSize=e.groupSize)},t.prototype.updateDisplay=function(){var e,t;this.connected>this.poolSize?(t=Math.floor(this.connected/this.groupSize),"undefined"!=typeof this.nGames&&(t=t>this.nGames?this.nGames:t),e=t*this.groupSize,this.playerCount.innerHTML=''+this.connected+""+" / "+this.poolSize,this.playerCountTooHigh.style.display="",this.playerCountTooHigh.innerHTML=this.getText("tooManyPlayers",{nGames:e})):(this.playerCount.innerHTML=this.connected+" / "+this.poolSize,this.playerCountTooHigh.style.display="none")},t.prototype.displayExecMode=function(){this.bodyDiv.innerHTML="",this.execModeDiv=document.createElement("div"),this.execModeDiv.id="exec-mode-div",this.execModeDiv.innerHTML=this.getText("executionMode"),this.playerCount=document.createElement("p"),this.playerCount.id="player-count",this.execModeDiv.appendChild(this.playerCount),this.playerCountTooHigh=document.createElement("div"),this.playerCountTooHigh.style.display="none",this.execModeDiv.appendChild(this.playerCountTooHigh),this.startDateDiv=document.createElement("div"),this.startDateDiv.style.display="none",this.execModeDiv.appendChild(this.startDateDiv),this.dots=W.getLoadingDots(),this.execModeDiv.appendChild(this.dots.span),this.bodyDiv.appendChild(this.execModeDiv),this.msgDiv=document.createElement("div"),this.bodyDiv.appendChild(this.msgDiv),this.waitTime&&this.startTimer()},t.prototype.append=function(){this.bodyDiv.innerHTML=this.getText("waitingForConf")},t.prototype.listeners=function(){var t;t=this,e.registerSetup("waitroom",function(n){if(!n)return;if("object"!=typeof n){e.warn("waiting room widget: invalid setup object: "+n);return}return n.executionMode?t.init(n):(t.setSounds(n.sounds),t.setTexts(n.texts)),n}),e.on.data("PLAYERSCONNECTED",function(e){if(!e.data)return;t.connected=e.data,t.updateDisplay()}),e.on.data("DISPATCH",function(e){var n,r;e=e||{},n=e.data||{},t.dots&&t.dots.stop(),n.action==="allPlayersConnected"?t.alertPlayer():(r=t.getText("exitCode",n),n.action==="notEnoughPlayers"?(t.bodyDiv.innerHTML=t.getText(n.action),t.onTimeout&&t.onTimeout(e.data),t.disconnect(t.bodyDiv.innerHTML+r)):n.action==="notSelected"?!1===n.shouldDispatchMoreGames||t.disconnectIfNotSelected?(t.bodyDiv.innerHTML=t.getText("notSelectedClosed"),t.disconnect(t.bodyDiv.innerHTML+r)):t.msgDiv.innerHTML=t.getText("notSelectedOpen"):n.action==="disconnect"&&t.disconnect(t.bodyDiv.innerHTML+r))}),e.on.data("TIME",function(){e.info("waiting room: TIME IS UP!"),t.stopTimer()}),e.on.data("WAITTIME",function(e){t.updateState(e.data),t.updateDisplay()}),e.on("SOCKET_DISCONNECT",function(){t.stopTimer(),t.bodyDiv.innerHTML=t.getText("disconnect")}),e.on.data("ROOM_CLOSED",function(){t.disconnect(t.getText("roomClosed"))})},t.prototype.stopTimer=function(){this.timer&&(e.info("waiting room: STOPPING TIMER"),this.timer.destroy())},t.prototype.disconnect=function(t){t&&this.setText("disconnect",t),e.socket.disconnect(),this.stopTimer()},t.prototype.alertPlayer=function(){var t,n,r,i;r=this.getText("blinkTitle"),i=this.getSound("dispatch"),i&&J.playSound(i);if(!r)return;document.hasFocus&&document.hasFocus()?J.blinkTitle(r,{repeatFor:1}):(t=J.blinkTitle(r,{stopOnFocus:!0,stopOnClick:window}),n=function(){var e;t(),e=W.getFrame(),e&&e.removeEventListener("mouseover",n,!1)},e.events.ng.once("FRAME_GENERATED",function(e){e.addEventListener("mouseover",n,!1)}))}}(node) \ No newline at end of file +(function(e){"use strict";function r(){}function i(e,t,n,r,i){var s;s="undefined"!=typeof e[n][t]?e[n][t]:e.constructor[n][t];if("undefined"==typeof s)throw new Error(r+": name not found: "+t);if("function"==typeof s){s=s(e,i);if("string"!=typeof s&&s!==!1)throw new TypeError(r+': cb "'+t+'" did not '+"return neither string or false. Found: "+s)}return s}function s(e,n,r,i,s,o){var u,a,f;s||(s=e.constructor[n]),"undefined"==typeof o&&(o={}),u={};if(t.isArray(s)){a=-1,f=s.length;for(;++a1&&(u=f.docked[f.docked.length-2],a=o(u.panelDiv.style.right),a+=u.panelDiv.offsetWidth),a+=r,n.panelDiv.style.right=a+"px",l=0,a+=n.panelDiv.offsetWidth+s;while(f.docked.length>1&&a>e.innerWidth&&l1&&(n+=''+(e.senderToNameMap[t.id]||t.id)+": "),n+=t.msg+"",n},quit:function(e,t){return(e.senderToNameMap[t.id]||t.id)+" left the chat"},noMoreParticipants:function(){return"No active participant left. Chat disabled."},collapse:function(e,t){return(e.senderToNameMap[t.id]||t.id)+" "+(t.collapsed?"mini":"maxi")+"mized the chat"},textareaPlaceholder:function(e){return e.useSubmitEnter?"Type something and press enter to send":"Type something"},submitButton:"Send",isTyping:"is typing..."},n.version="1.5.0",n.description="Offers a uni-/bi-directional communication interface between players, or between players and the server.",n.className="chat",n.panel=!1,n.prototype.init=function(n){var r,i,s,o,u;n=n||{},u=this,this.receiverOnly=!!n.receiverOnly,r=n.preprocessMsg;if("function"==typeof r)this.preprocessMsg=r;else if(r)throw new TypeError("Chat.init: preprocessMsg must be function or undefined. Found: "+r);r=n.chatEvent;if(r){if("string"!=typeof r)throw new TypeError("Chat.init: chatEvent must be a non-empty string or undefined. Found: "+r);this.chatEvent=n.chatEvent}else this.chatEvent="CHAT";this.storeMsgs=!!n.storeMsgs,this.storeMsgs&&(this.db||(this.db=new t)),this.useSubmitButton="undefined"==typeof n.useSubmitButton?J.isMobileAgent():!!n.useSubmitButton,this.useSubmitEnter="undefined"==typeof n.useSubmitEnter?!0:!!n.useSubmitEnter,r=n.participants;if(!J.isArray(r)||!r.length)throw new TypeError("Chat.init: participants must be a non-empty array. Found: "+r);this.recipientsIds=new Array(r.length),this.recipientsIdsQuitted=[],this.recipientToSenderMap={},this.recipientToNameMap={},this.senderToNameMap={},this.senderToRecipientMap={};for(i=0;i"+this.title+""),this.stats.unread++)),!0)},n.prototype.disable=function(){this.submitButton&&(this.submitButton.disabled=!0),this.textarea.disabled=!0,this.disabled=!0},n.prototype.enable=function(){this.submitButton&&(this.submitButton.disabled=!1),this.textarea.disabled=!1,this.disabled=!1},n.prototype.getValues=function(){var e;return e={participants:this.participants,totSent:this.stats.sent,totReceived:this.stats.received,totUnread:this.stats.unread,initialMsg:this.initialMsg},this.db&&(e.msgs=this.db.fetch()),e},n.prototype.sendMsg=function(t){var n,r,i;if(this.isDisabled()){e.warn("Chat is disable, msg not sent.");return}if("object"==typeof t){if("undefined"!=typeof t.msg&&"object"==typeof t.msg)throw new TypeError("Chat.sendMsg: opts.msg cannot be object. Found: "+t.msg)}else if("undefined"==typeof t)t={msg:this.readTextarea()};else{if("string"!=typeof t&&"number"!=typeof t)throw new TypeError("Chat.sendMsg: opts must be string, number, object, or undefined. Found: "+t);t={msg:t}}t.msg=this.renderMsg(t,"outgoing");if(t.msg===""){e.warn("Chat: message has no text, not sent.");return}r=t.recipients||this.recipientsIds;if(r.length===0){e.warn("Chat: empty recipient list, message not sent.");return}n=r.length===1?r[0]:r,e.say(this.chatEvent,n,t),t.silent||(i=this,this.writeMsg("outgoing",t),i.textarea&&setTimeout(function(){i.textarea.value=""})),this.amTypingTimeout&&(clearTimeout(this.amTypingTimeout),this.amTypingTimeout=null)}}(node),function(e){"use strict";function n(e){var t=this;this.options=null,this.table=null,this.sc=null,this.fp=null,this.canvas=null,this.changes=[],this.onChange=null,this.onChangeCb=function(e,n){"undefined"==typeof n&&(n=!1),e||(t.sc?e=t.sc.getValues():e=i.random()),t.draw(e,n)},this.timeFrom="step",this.features=null}function r(e,t){this.canvas=new W.Canvas(e),this.scaleX=e.width/n.width,this.scaleY=e.height/n.heigth,this.face=null}function i(e,t){var n;if("undefined"==typeof e)for(n in i.defaults)i.defaults.hasOwnProperty(n)&&(n==="color"?this.color="red":n==="lineWidth"?this.lineWidth=1:n==="scaleX"?this.scaleX=1:n==="scaleY"?this.scaleY=1:this[n]=i.defaults[n].min+Math.random()*i.defaults[n].range);else{if("object"!=typeof e)throw new TypeError("FaceVector constructor: faceVector must be object or undefined.");this.scaleX=e.scaleX||1,this.scaleY=e.scaleY||1,this.color=e.color||"green",this.lineWidth=e.lineWidth||1,t=t||i.defaults;for(n in t)t.hasOwnProperty(n)&&(e.hasOwnProperty(n)?this[n]=e[n]:this[n]=t?t[n]:i.defaults[n].value)}}var t=W.Table;e.widgets.register("ChernoffFaces",n),n.version="0.6.2",n.description="Display parametric data in the form of a Chernoff Face.",n.className="chernofffaces",n.dependencies={Table:{},Canvas:{},SliderControls:{}},n.FaceVector=i,n.FacePainter=r,n.width=100,n.height=100,n.onChange="CF_CHANGE",n.prototype.init=function(t){this.options=t,t.features?this.features=new i(t.features):this.features||(this.features=i.random()),this.fp&&this.fp.draw(this.features),t.onChange===!1||t.onChange===null?this.onChange&&(e.off(this.onChange,this.onChangeCb),this.onChange=null):(this.onChange="undefined"==typeof t.onChange?n.onChange:t.onChange,e.on(this.onChange,this.onChangeCb))},n.prototype.getCanvas=function(){return this.canvas},n.prototype.buildHTML=function(){var n,r,s,o;if(this.table)return;o=this.options,s={},this.id&&(s.id=this.id),"string"==typeof o.className?s.className=o.className:o.className!==!1&&(s.className="cf_table"),this.table=new t(s),this.canvas||this.buildCanvas();if("undefined"==typeof o.controls||o.controls)r=J.mergeOnKey(i.defaults,this.features,"value"),n={id:"cf_controls",features:r,onChange:this.onChange,submit:"Send"},"object"==typeof o.controls?this.sc=o.controls:this.sc=e.widgets.get("SliderControls",n);this.sc?this.table.addRow([{content:this.sc,id:this.id+"_td_controls"},{content:this.canvas,id:this.id+"_td_cf"}]):this.table.add({content:this.canvas,id:this.id+"_td_cf"}),this.table.parse()},n.prototype.buildCanvas=function(){var e;this.canvas||(e=this.options,e.canvas||(e.canvas={},"undefined"!=typeof e.height&&(e.canvas.height=e.height),"undefined"!=typeof e.width&&(e.canvas.width=e.width)),this.canvas=W.get("canvas",e.canvas),this.canvas.id="ChernoffFaces_canvas",this.fp=new r(this.canvas),this.fp.draw(this.features))},n.prototype.append=function(){this.table||this.buildHTML(),this.bodyDiv.appendChild(this.table.table)},n.prototype.draw=function(t,n){var r;if("object"!=typeof t)throw new TypeError("ChernoffFaces.draw: features must be object.");this.options.trackChanges&&("string"==typeof this.timeFrom?r=e.timer.getTimeSince(this.timeFrom):r=Date.now?Date.now():(new Date).getTime(),this.changes.push({time:r,change:t})),this.features=t instanceof i?t:new i(t,this.features),this.fp.redraw(this.features),this.sc&&n!==!1&&(this.sc.init({features:J.mergeOnKey(i.defaults,t,"value")}),this.sc.refresh())},n.prototype.getValues=function(e){return e&&e.changes?{changes:this.changes,cf:this.features}:this.fp.face},n.prototype.randomize=function(){var e;return e=i.random(),this.fp.redraw(e),this.sc&&(this.sc.init({features:J.mergeOnValue(i.defaults,e),onChange:this.onChange}),this.sc.refresh()),!0},r.prototype.draw=function(e,t,n){if(!e)return;this.face=e,this.fit2Canvas(e),this.canvas.scale(e.scaleX,e.scaleY),t=t||this.canvas.centerX,n=n||this.canvas.centerY,this.drawHead(e,t,n),this.drawEyes(e,t,n),this.drawPupils(e,t,n),this.drawEyebrow(e,t,n),this.drawNose(e,t,n),this.drawMouth(e,t,n)},r.prototype.redraw=function(e,t,n){this.canvas.clear(),this.draw(e,t,n)},r.prototype.scale=function(e,t){this.canvas.scale(this.scaleX,this.scaleY)},r.prototype.fit2Canvas=function(e){var t;if(!this.canvas){console.log("No canvas found");return}this.canvas.width>this.canvas.height?t=this.canvas.width/e.head_radius*e.head_scale_x:t=this.canvas.height/e.head_radius*e.head_scale_y,e.scaleX=t/2,e.scaleY=t/2},r.prototype.drawHead=function(e,t,n){var r=e.head_radius;this.canvas.drawOval({x:t,y:n,radius:r,scale_x:e.head_scale_x,scale_y:e.head_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyes=function(e,t,n){var i=r.computeFaceOffset(e,e.eye_height,n),s=e.eye_spacing,o=e.eye_radius;this.canvas.drawOval({x:t-s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawPupils=function(e,t,n){var i=e.pupil_radius,s=e.eye_spacing,o=r.computeFaceOffset(e,e.eye_height,n);this.canvas.drawOval({x:t-s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyebrow=function(e,t,n){var i=r.computeEyebrowOffset(e,n),s=e.eyebrow_spacing,o=e.eyebrow_length,u=e.eyebrow_angle;this.canvas.drawLine({x:t-s,y:i,length:o,angle:u,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawLine({x:t+s,y:i,length:0-o,angle:-u,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawNose=function(e,t,n){var i=r.computeFaceOffset(e,e.nose_height,n),s=t+e.nose_width/2,o=i+e.nose_length,u=s-e.nose_width,a=o;this.canvas.ctx.lineWidth=e.lineWidth,this.canvas.ctx.strokeStyle=e.color,this.canvas.ctx.save(),this.canvas.ctx.beginPath(),this.canvas.ctx.moveTo(t,i),this.canvas.ctx.lineTo(s,o),this.canvas.ctx.lineTo(u,a),this.canvas.ctx.stroke(),this.canvas.ctx.restore()},r.prototype.drawMouth=function(e,t,n){var i=r.computeFaceOffset(e,e.mouth_height,n),s=t-e.mouth_width/2,o=t+e.mouth_width/2,u=i-e.mouth_top_y,a=i+e.mouth_bottom_y;this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,u,o,i),this.canvas.ctx.stroke(),this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,a,o,i),this.canvas.ctx.stroke()},r.computeFaceOffset=function(e,t,n){n=n||0;var r=n-e.head_radius+e.head_radius*2*t;return r},r.computeEyebrowOffset=function(e,t){t=t||0;var n=2;return r.computeFaceOffset(e,e.eye_height,t)-n-e.eyebrow_eyedistance},i.defaults={head_radius:{min:10,max:100,step:.01,value:30,label:"Face radius"},head_scale_x:{min:.2,max:2,step:.01,value:.5,label:"Scale head horizontally"},head_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale head vertically"},eye_height:{min:.1,max:.9,step:.01,value:.4,label:"Eye height"},eye_radius:{min:2,max:30,step:.01,value:5,label:"Eye radius"},eye_spacing:{min:0,max:50,step:.01,value:10,label:"Eye spacing"},eye_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale eyes horizontally"},eye_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale eyes vertically"},pupil_radius:{min:1,max:9,step:.01,value:1,label:"Pupil radius"},pupil_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale pupils horizontally"},pupil_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale pupils vertically"},eyebrow_length:{min:1,max:30,step:.01,value:10,label:"Eyebrow length"},eyebrow_eyedistance:{min:.3,max:10,step:.01,value:3,label:"Eyebrow from eye"},eyebrow_angle:{min:-2,max:2,step:.01,value:-0.5,label:"Eyebrow angle"},eyebrow_spacing:{min:0,max:20,step:.01,value:5,label:"Eyebrow spacing"},nose_height:{min:.4,max:1,step:.01,value:.4,label:"Nose height"},nose_length:{min:.2,max:30,step:.01,value:15,label:"Nose length"},nose_width:{min:0,max:30,step:.01,value:10,label:"Nose width"},mouth_height:{min:.2,max:2,step:.01,value:.75,label:"Mouth height"},mouth_width:{min:2,max:100,step:.01,value:20,label:"Mouth width"},mouth_top_y:{min:-10,max:30,step:.01,value:-2,label:"Upper lip"},mouth_bottom_y:{min:-10,max:30,step:.01,value:20,label:"Lower lip"},scaleX:{min:0,max:20,step:.01,value:.2,label:"Scale X"},scaleY:{min:0,max:20,step:.01,value:.2,label:"Scale Y"},color:{min:0,max:20,step:.01,value:.2,label:"color"},lineWidth:{min:0,max:20,step:.01,value:.2,label:"lineWidth"}},function(e){var t;for(t in e)e.hasOwnProperty(t)&&(e[t].range=e[t].max-e[t].min)}(i.defaults),i.random=function(){return console.log("*** FaceVector.random is deprecated. Use new FaceVector() instead."),new i}}(node),function(e){"use strict";function n(n){this.options=n,this.id=n.id,this.table=new t({id:"cf_table"}),this.root=n.root||document.createElement("div"),this.root.id=this.id,this.sc=e.widgets.get("Controls.Slider"),this.fp=null,this.canvas=null,this.dims=null,this.change="CF_CHANGE";var r=this;this.changeFunc=function(){r.draw(r.sc.getAllValues())},this.features=null,this.controls=null}function r(e,t){this.canvas=new W.Canvas(e),this.scaleX=e.width/n.defaults.canvas.width,this.scaleY=e.height/n.defaults.canvas.heigth}function i(e){e=e||{},this.scaleX=e.scaleX||1,this.scaleY=e.scaleY||1,this.color=e.color||"green",this.lineWidth=e.lineWidth||1;for(var t in i.defaults)i.defaults.hasOwnProperty(t)&&(e.hasOwnProperty(t)?this[t]=e[t]:this[t]=i.defaults[t].value)}var t=W.Table;e.widgets.register("ChernoffFacesSimple",n),n.defaults={},n.defaults.id="ChernoffFaces",n.defaults.canvas={},n.defaults.canvas.width=100,n.defaults.canvas.heigth=100,n.version="0.4",n.description="Display parametric data in the form of a Chernoff Face.",n.dependencies={Table:{},Canvas:{},"Controls.Slider":{}},n.FaceVector=i,n.FacePainter=r,n.prototype.init=function(t){this.id=t.id||this.id;var s=this.id+"_";this.features=t.features||this.features||i.random(),this.controls="undefined"!=typeof t.controls?t.controls:!0;var o=t.idCanvas?t.idCanvas:s+"canvas";this.dims={width:t.width?t.width:n.defaults.canvas.width,height:t.height?t.height:n.defaults.canvas.heigth},this.canvas=W.getCanvas(o,this.dims),this.fp=new r(this.canvas),this.fp.draw(new i(this.features));var u={id:"cf_controls",features:J.mergeOnKey(i.defaults,this.features,"value"),change:this.change,fieldset:{id:this.id+"_controls_fieldest",legend:this.controls.legend||"Controls"},submit:"Send"};this.sc=e.widgets.get("Controls.Slider",u),this.controls&&this.table.add(this.sc),"undefined"==typeof t.change?e.on(this.change,this.changeFunc):(t.change?e.on(t.change,this.changeFunc):e.removeListener(this.change,this.changeFunc),this.change=t.change),this.table.add(this.canvas),this.table.parse(),this.root.appendChild(this.table.table)},n.prototype.getRoot=function(){return this.root},n.prototype.getCanvas=function(){return this.canvas},n.prototype.append=function(e){return e.appendChild(this.root),this.table.parse(),this.root},n.prototype.listeners=function(){},n.prototype.draw=function(e){if(!e)return;var t=new i(e);this.fp.redraw(t),this.sc.init({features:J.mergeOnKey(i.defaults,e,"value")}),this.sc.refresh()},n.prototype.getAllValues=function(){return this.fp.face},n.prototype.randomize=function(){var e=i.random();this.fp.redraw(e);var t={features:J.mergeOnKey(i.defaults,e,"value"),change:this.change};return this.sc.init(t),this.sc.refresh(),!0},r.prototype.draw=function(e,t,n){if(!e)return;this.face=e,this.fit2Canvas(e),this.canvas.scale(e.scaleX,e.scaleY),t=t||this.canvas.centerX,n=n||this.canvas.centerY,this.drawHead(e,t,n),this.drawEyes(e,t,n),this.drawPupils(e,t,n),this.drawEyebrow(e,t,n),this.drawNose(e,t,n),this.drawMouth(e,t,n)},r.prototype.redraw=function(e,t,n){this.canvas.clear(),this.draw(e,t,n)},r.prototype.scale=function(e,t){this.canvas.scale(this.scaleX,this.scaleY)},r.prototype.fit2Canvas=function(e){var t;if(!this.canvas){console.log("No canvas found");return}this.canvas.width>this.canvas.height?t=this.canvas.width/e.head_radius*e.head_scale_x:t=this.canvas.height/e.head_radius*e.head_scale_y,e.scaleX=t/2,e.scaleY=t/2},r.prototype.drawHead=function(e,t,n){var r=e.head_radius;this.canvas.drawOval({x:t,y:n,radius:r,scale_x:e.head_scale_x,scale_y:e.head_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyes=function(e,t,n){var i=r.computeFaceOffset(e,e.eye_height,n),s=e.eye_spacing,o=e.eye_radius;this.canvas.drawOval({x:t-s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawPupils=function(e,t,n){var i=e.pupil_radius,s=e.eye_spacing,o=r.computeFaceOffset(e,e.eye_height,n);this.canvas.drawOval({x:t-s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyebrow=function(e,t,n){var i=r.computeEyebrowOffset(e,n),s=e.eyebrow_spacing,o=e.eyebrow_length,u=e.eyebrow_angle;this.canvas.drawLine({x:t-s,y:i,length:o,angle:u,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawLine({x:t+s,y:i,length:0-o,angle:-u,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawNose=function(e,t,n){var i=r.computeFaceOffset(e,e.nose_height,n),s=t+e.nose_width/2,o=i+e.nose_length,u=s-e.nose_width,a=o;this.canvas.ctx.lineWidth=e.lineWidth,this.canvas.ctx.strokeStyle=e.color,this.canvas.ctx.save(),this.canvas.ctx.beginPath(),this.canvas.ctx.moveTo(t,i),this.canvas.ctx.lineTo(s,o),this.canvas.ctx.lineTo(u,a),this.canvas.ctx.stroke(),this.canvas.ctx.restore()},r.prototype.drawMouth=function(e,t,n){var i=r.computeFaceOffset(e,e.mouth_height,n),s=t-e.mouth_width/2,o=t+e.mouth_width/2,u=i-e.mouth_top_y,a=i+e.mouth_bottom_y;this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,u,o,i),this.canvas.ctx.stroke(),this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,a,o,i),this.canvas.ctx.stroke()},r.computeFaceOffset=function(e,t,n){n=n||0;var r=n-e.head_radius+e.head_radius*2*t;return r},r.computeEyebrowOffset=function(e,t){t=t||0;var n=2;return r.computeFaceOffset(e,e.eye_height,t)-n-e.eyebrow_eyedistance},i.defaults={head_radius:{min:10,max:100,step:.01,value:30,label:"Face radius"},head_scale_x:{min:.2,max:2,step:.01,value:.5,label:"Scale head horizontally"},head_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale head vertically"},eye_height:{min:.1,max:.9,step:.01,value:.4,label:"Eye height"},eye_radius:{min:2,max:30,step:.01,value:5,label:"Eye radius"},eye_spacing:{min:0,max:50,step:.01,value:10,label:"Eye spacing"},eye_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale eyes horizontally"},eye_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale eyes vertically"},pupil_radius:{min:1,max:9,step:.01,value:1,label:"Pupil radius"},pupil_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale pupils horizontally"},pupil_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale pupils vertically"},eyebrow_length:{min:1,max:30,step:.01,value:10,label:"Eyebrow length"},eyebrow_eyedistance:{min:.3,max:10,step:.01,value:3,label:"Eyebrow from eye"},eyebrow_angle:{min:-2,max:2,step:.01,value:-0.5,label:"Eyebrow angle"},eyebrow_spacing:{min:0,max:20,step:.01,value:5,label:"Eyebrow spacing"},nose_height:{min:.4,max:1,step:.01,value:.4,label:"Nose height"},nose_length:{min:.2,max:30,step:.01,value:15,label:"Nose length"},nose_width:{min:0,max:30,step:.01,value:10,label:"Nose width"},mouth_height:{min:.2,max:2,step:.01,value:.75,label:"Mouth height"},mouth_width:{min:2,max:100,step:.01,value:20,label:"Mouth width"},mouth_top_y:{min:-10,max:30,step:.01,value:-2,label:"Upper lip"},mouth_bottom_y:{min:-10,max:30,step:.01,value:20,label:"Lower lip"}},i.random=function(){var e={};for(var t in i.defaults)i.defaults.hasOwnProperty(t)&&(J.inArray(t,["color","lineWidth","scaleX","scaleY"])||(e[t]=i.defaults[t].min+Math.random()*i.defaults[t].max));return e.scaleX=1,e.scaleY=1,e.color="green",e.lineWidth=1,new i(e)},i.prototype.shuffle=function(){for(var e in this)this.hasOwnProperty(e)&&i.defaults.hasOwnProperty(e)&&e!=="color"&&(this[e]=i.defaults[e].min+Math.random()*i.defaults[e].max)},i.prototype.distance=function(e){return i.distance(this,e)},i.distance=function(e,t){var n=0,r;for(var i in e)e.hasOwnProperty(i)&&(r=e[i]-t[i],n+=r*r);return Math.sqrt(n)},i.prototype.toString=function(){var e="Face: ";for(var t in this)this.hasOwnProperty(t)&&(e+=t+" "+this[t]);return e}}(node),function(e){"use strict";function n(){this.dl=null,this.mainText=null,this.spanMainText=null,this.forms=null,this.formsById=null,this.order=null,this.shuffleForms=null,this.group=null,this.groupOrder=null,this.formsOptions={title:!1,frame:!1,storeRef:!1},this.simplify=null,this.freeText=null,this.textarea=null,this.required=null,this.oneByOne=null,this.oneByOneCounter=0,this.oneByOneResults={},this.conditionals={},this.doneBtn=null,this.backBtn=null,this.honeypot=null,this.qCounter=1,this.qCounterSymbol="Q",this.qCounterCb=function(e,t,n,r){return''+e.qCounterSymbol+e.qCounter++ +" "+t},this.autoId=!0,this.delayOnNext=350}function r(e,t,n,r){var i;return(t.required||t.requiredChoice)&&(e.choice===null||t.selectMultiple&&!e.choice.length)&&(r&&r.missValues.push(t.id),i=t),n.markAttempt&&e.isCorrect===!1&&(i=t),i}function i(e,t){var n,r,i;n=e.conditionals[t];if(n){if("function"==typeof n)return n.call(e,e.formsById);for(r in n)if(n.hasOwnProperty(r)){i=e.formsById[r];if(!i)continue;if(J.isArray(n[r])){if(!J.inArray(i.currentChoice,n[r]))return!1}else if(i.currentChoice!==n[r])return!1}}return!0}function s(e){var t,n,r,i;i=document.createElement("dl"),t=-1,n=e.forms.length;for(;++t=this.forms.length-1)return!1;n.hide(),this.backBtn&&this.backBtn.disable(),this.doneBtn&&this.doneBtn.disable(),s=500;while(n&&!r&&this.oneByOneCountern.currentChoice.length)?n.customInput.show():(!n.selectMultiple||f)&&n.customInput.hide());if(a){n.unsetCurrentChoice(r),J.removeClass(i,"selected");if(n.selectMultiple){o=-1,u=n.selected.length;for(;++o 1. Found: "+r)}this.selectMultiple=r,r&&(this.selected=[],this.currentChoice=[]);if("number"==typeof n.requiredChoice){if(!J.isInt(n.requiredChoice,0))throw new Error("ChoiceTable.init: if number, requiredChoice must a positive integer. Found: "+n.requiredChoice);if("number"==typeof this.selectMultiple&&n.requiredChoice>this.selectMultiple)throw new Error("ChoiceTable.init: requiredChoice cannot be larger than selectMultiple. Found: "+n.requiredChoice+" > "+this.selectMultiple);this.requiredChoice=n.requiredChoice}else if("boolean"==typeof n.requiredChoice)this.requiredChoice=n.requiredChoice?1:null;else if("undefined"!=typeof n.requiredChoice)throw new TypeError("ChoiceTable.init: opts.requiredChoice be number, boolean or undefined. Found: "+n.requiredChoice);"undefined"!=typeof n.oneTimeClick&&(this.oneTimeClick=!!n.oneTimeClick);if("string"==typeof n.group||"number"==typeof n.group)this.group=n.group;else if("undefined"!=typeof n.group)throw new TypeError("ChoiceTable.init: opts.group must be string, number or undefined. Found: "+n.group);if("number"==typeof n.groupOrder)this.groupOrder=n.groupOrder;else if("undefined"!=typeof n.groupOrder)throw new TypeError("ChoiceTable.init: opts.groupOrder must be number or undefined. Found: "+n.groupOrder);if("function"==typeof n.listener)this.listener=function(e){n.listener.call(this,e)};else if("undefined"!=typeof n.listener)throw new TypeError("ChoiceTable.init: opts.listener must be function or undefined. Found: "+n.listener);if("function"==typeof n.onclick)this.onclick=n.onclick;else if("undefined"!=typeof n.onclick)throw new TypeError("ChoiceTable.init: opts.onclick must be function or undefined. Found: "+n.onclick);r=n.mainText;if("function"==typeof r){r=r.call(this);if("string"!=typeof r)throw new TypeError("ChoiceTable.init: opts.mainText cb must return a string. Found: "+r)}if("string"==typeof r)this.mainText=r;else if("undefined"!=typeof r)throw new TypeError("ChoiceTable.init: opts.mainText must be function, string or undefined. Found: "+r);r=n.hint;if("function"==typeof r){r=r.call(this);if("string"!=typeof r&&!1!==r)throw new TypeError("ChoiceTable.init: opts.hint cb must return string or false. Found: "+r)}if("string"==typeof r||!1===r)this.hint=r,this.requiredChoice&&r!==!1&&this.displayRequired&&(this.hint+=" "+this.requiredMark);else{if("undefined"!=typeof r)throw new TypeError("ChoiceTable.init: opts.hint must be a string, false, or undefined. Found: "+r);this.hint=this.getText("autoHint")}if(n.timeFrom===!1||"string"==typeof n.timeFrom)this.timeFrom=n.timeFrom;else if("undefined"!=typeof n.timeFrom)throw new TypeError("ChoiceTable.init: opts.timeFrom must be string, false, or undefined. Found: "+n.timeFrom);if("string"==typeof n.separator)this.separator=n.separator;else if("undefined"!=typeof n.separator)throw new TypeError("ChoiceTable.init: opts.separator must be string, or undefined. Found: "+n.separator);r=this.id+this.separator.substring(0,this.separator.length-1);if(this.id.indexOf(this.separator)!==-1||r.indexOf(this.separator)!==-1)throw new Error("ChoiceTable.init: separator cannot be included in the id or in the concatenation (id + separator). Please specify the right separator option. Found: "+this.separator);r=n.left;if("function"==typeof r){r=r.call(this);if("string"!=typeof r&&"undefined"!=typeof r)throw new TypeError("ChoiceTable.init: opts.left cb must return string or undefined. Found: "+r)}if("string"==typeof r||"number"==typeof r)this.left=""+r;else if(J.isNode(n.left)||J.isElement(n.left))this.left=n.left;else if("undefined"!=typeof n.left)throw new TypeError("ChoiceTable.init: opts.left must be string, number, function, an HTML Element or undefined. Found: "+r);r=n.right;if("function"==typeof r){r=r.call(this);if("string"!=typeof r&&"undefined"!=typeof r)throw new TypeError("ChoiceTable.init: opts.right cb must return string or undefined. Found: "+r)}if("string"==typeof r||"number"==typeof r)this.right=""+r;else if(J.isNode(n.right)||J.isElement(n.right))this.right=n.right;else if("undefined"!=typeof n.right)throw new TypeError("ChoiceTable.init: opts.right must be string, number, an HTML Element or undefined. Found: "+n.right);if("undefined"==typeof n.className)this.className=t.className;else if(n.className===!1)this.className=!1;else if("string"==typeof n.className)this.className=t.className+" "+n.className;else{if(!J.isArray(n.className))throw new TypeError("ChoiceTable.init: opts.className must be string, array, or undefined. Found: "+n.className);this.className=[t.className].concat(n.className)}n.tabbable!==!1&&(this.tabbable=!0);if("function"==typeof n.renderer)this.renderer=n.renderer;else if("undefined"!=typeof n.renderer)throw new TypeError("ChoiceTable.init: opts.renderer must be function or undefined. Found: "+n.renderer);if("object"==typeof n.table)this.table=n.table;else if("undefined"!=typeof n.table&&!1!==n.table)throw new TypeError("ChoiceTable.init: opts.table must be object, false or undefined. Found: "+n.table);this.table=n.table,this.freeText="string"==typeof n.freeText?n.freeText:!!n.freeText,r=n.choicesSetSize,"function"==typeof r&&(r=r.call(this));if("undefined"!=typeof r){if(!J.isInt(r,0))throw new Error("ChoiceTable.init: choicesSetSize must be undefined or an integer > 0. Found: "+r);if(this.left||this.right)throw new Error("ChoiceTable.init: choicesSetSize option cannot be specified when either left or right options are set.");this.choicesSetSize=r}"undefined"!=typeof n.sameWidthCells&&(this.sameWidthCells=n.sameWidthCells),"undefined"!=typeof n.other&&(this.other=n.other),r=n.choices;if("function"==typeof r){r=r.call(this);if(!J.isArray(r)||!r.length)throw new TypeError("ChoiceTable.init: opts.choices cb must return a non-empty array. Found: "+r)}"undefined"!=typeof r&&this.setChoices(r),r=n.correctChoice,"undefined"!=typeof r&&(this.requiredChoice&&(this.requiredChoice=null,this.required=null,e.warn("ChoiceTable.init: requiredChoice and correctChoice are both set; requiredChoice ignored.")),"function"==typeof r&&(r=r.call(this)),this.setCorrectChoice(n.correctChoice)),r=n.disabledChoices;if("undefined"!=typeof r){"function"==typeof r&&(r=r.call(this));if(!J.isArray(n.disabledChoices))throw new TypeError("ChoiceTable.init: disabledChoices must be undefined or array. Found: "+r);r&&function(){for(s=0;s=0&&e.splice(n,1)),this.choices=e,t=e.length,this.order=J.seq(0,t-1),this.shuffleChoices&&(this.order=J.shuffle(this.order)),function(n){var r,i,s,o,u=[],a;for(r=-1;++r1&&u.sort(function(e,t){return e.fixed=this.choicesSetSize)break}this.rightCell&&(i||(o=r(this,"right")),o.appendChild(this.rightCell)),t!==n&&e.call(this,t,n,i,s)}return function(){var t,n,r;if(!this.choicesCells)throw new Error("ChoiceTable.buildTable: choices not set, cannot build table. Id: "+this.id);n=this.orientation==="H",t=this.choicesCells.length,r="number"==typeof this.choicesSetSize,e.call(this,-1,t,n,r),this.enable()}}(),t.prototype.buildTableAndChoices=function(){var e,t,n,i,s,o;n=this.choices.length,this.choicesCells=new Array(n),e=-1,o=this.orientation==="H",o&&(i=r(this,"main"),this.left&&(s=this.renderSpecial("left",this.left),i.appendChild(s)));for(;++e=this.requiredChoice:this.currentChoice!==null;a=this.correctChoice;if(null===a)return f?l:null;if(!this.selectMultiple)return this.currentChoice===a;J.isArray(a)||(a=[a]),n=a.length,i=this.currentChoice.length;if(n!==i)return!1;t=-1,o=this.currentChoice.slice(0);for(;++tr)throw new Error("ChoiceTable.setValues: values array cannot be larger than max allowed set: "+s+" > "+r);r=e.values}for(;++i

If you need a copy of this consent form, you may print a copy of this page for your records.

",printBtn:"Print this page",consentTerms:"Do you understand and consent to these terms?",agree:"Yes, I agree",notAgree:"No, I do not agree",showHideConsent:function(e,t){return(t==="hide"?"Hide":"Show")+" Consent Form"}},t.prototype.init=function(t){var n;t=t||{},this.consentTexts=t.consent||e.game.settings.CONSENT;if(this.consentTexts&&"object"!=typeof this.consentTexts)throw new TypeError("Consent.init: consent must be object or undefined. Found: "+this.consentTexts);this.showPrint=t.showPrint===!1?!1:!0,this.showBtns=t.showAgreeBtns===!1?!1:!0,this.disconnect=t.disconnect===!1?!1:!0,this.doneOnAgree=t.doneOnAgree===!1?!1:!0;if(J.isArray(t.checkboxes))n=this,t.checkboxes.forEach(function(e){if("function"==typeof e){e=e();if(e===!1)return}n.checkboxes.push(e)});else if(t.checkboxes)throw new TypeError("Consent.init: checkboxes must be array or undefined. Found: "+this.checkboxes);s(this,t,"prefix"),s(this,t,"fineprint"),s(this,t,"consentId"),"undefined"==typeof t.consentId&&(this.consentId=r(this,this.consentId))},t.prototype.enable=function(){if(this.agreed!==null)return;n(!0)},t.prototype.disable=function(){n(!1)},t.prototype.append=function(){var t,n,s,o,u,a,f,l;t=this,W.hide(r(this,"notAgreed")),n=W.gid(this.consentId),n||(e.warn('Consent.append: the page does not contain an element with id "'+this.consentId+"\", it will use widget's root"),n=w.bodyDiv),o="",s=W.isRTL(this.bodyDiv);if(this.checkboxes.length||this.fineprint)o+='
',this.checkboxes.length&&(o+="
",this.checkboxes.forEach(function(e,n){var r,u,a,f;r=i(t,n+1),f="form-check",s&&(f+="-reverse"),"object"==typeof e?(u=e.label,f+=" "+e.className):u=e,a='',u='",o+="
",o+='
',o+=s?u+a:a+u,o+="
"}),o+="
"),this.fineprint&&(o+='

',o+=this.fineprint,o+="

"),o+="
";this.showPrint&&(o=this.getText("printText"),o+='

'),this.showBtns!==!1&&(o+=""+this.getText("consentTerms")+"
",o+='"),n.innerHTML+=o,setTimeout(function(){W.adjustFrameHeight()})},t.prototype.listeners=function(){var t=this,n=this.consentTexts;e.on("FRAME_LOADED",function(){var i,s,o,u;if(n)for(o in n)n.hasOwnProperty(o)&&(u=o.toLowerCase(),u=u.replace(new RegExp("_","g"),"-"),W.setInnerHTML(u,n[o]));if(!t.showBtns)return;i=W.gid(r(this,"agree")),s=W.gid(r(this,"notAgree")),i.onclick=function(){var n;e.emit("CONSENT_ACCEPTING"),n=t.getValues({agreed:!0});if(!n.consent)return;this.agreed=!0,e.emit("CONSENT_ACCEPTED",n),t.doneOnAgree&&e.done(n)},s.onclick=function(){var n,o;o=confirm(t.getText("areYouSure"));if(!o)return;e.emit("CONSENT_REJECTING"),t.agreed=!1,e.set({consent:!1,time:e.timer.getTimeSince("step"),timeup:!1}),i.disabled=!0,s.disabled=!0,i.onclick=null,s.onclick=null,t.disconnect&&(e.game.discBox&&e.game.discBox.destroy(),e.socket.disconnect()),W.hide(t.consentId),W.show(r(t,"notAgreed")),n=W.gid(r(t,"show-consent")),n&&(n.onclick=function(){var e,n;e=W.toggle(t.consentId),n=e.style.display===""?"hide":"show",this.innerHTML=t.getText("showHideConsent",n)}),e.emit("CONSENT_REJECTED")}})},t.prototype.getValues=function(t){var n,r;return r=this,n={consent:!0},t=t||{},this.checkboxes.length&&(n.checkboxes=!0,this.checkboxes.forEach(function(s,o){var u,a,f;a=i(r,o+1),u=W.gid(a);if(!u)e.warn("Consent: could not find checkbox "+a);else{f=r.checkboxes[o],n[a]=u.checked;if("string"==typeof f||f.required!==!1)u.checked||(n.checkboxes=n.consent=!1,t.highlight!==!1&&W.shake(u))}})),this.agreed!==!0&&this.showBtns&&!t.agreed&&(n.consent=!1),n}}(node),function(e){"use strict";function t(){this.mainText=null,this.content=null,this.hint=null}e.widgets.register("ContentBox",t),t.version="0.2.0",t.description="Simply displays some content",t.panel=!1,t.className="contentbox",t.prototype.init=function(e){if("string"==typeof e.mainText)this.mainText=e.mainText;else if("undefined"!=typeof e.mainText)throw new TypeError("ContentBox.init: mainText must be string or undefined. Found: "+e.mainText);if("string"==typeof e.content)this.content=e.content;else if("undefined"!=typeof e.content)throw new TypeError("ContentBox.init: content must be string or undefined. Found: "+e.content);if("string"==typeof e.hint)this.hint=e.hint;else if("undefined"!=typeof e.hint)throw new TypeError("ContentBox.init: hint must be string or undefined. Found: "+e.hint)},t.prototype.append=function(){this.mainText&&W.append("span",this.bodyDiv,{className:"contentbox-maintext",innerHTML:this.mainText}),this.content&&W.append("div",this.bodyDiv,{className:"contentbox-content",innerHTML:this.content}),this.hint&&W.append("span",this.bodyDiv,{className:"contentbox-hint",innerHTML:this.hint})}}(node),function(e){"use strict";function t(e){this.options=e,this.listRoot=null,this.submit=null,this.changeEvent="Controls_change",this.hasChanged=!1}function n(e){t.call(this,e)}function r(e){t.call(this,e)}function i(e){t.call(this,e),this.groupName="undefined"!=typeof e.name?e.name:W.generateUniqueId(),this.radioElem=null}e.widgets.register("Controls",t),t.version="0.5.1",t.description="Wraps a collection of user-inputs controls.",t.className="controls",t.prototype.add=function(e,t,n){},t.prototype.getItem=function(e,t){},t.prototype.init=function(e){this.hasChanged=!1,"undefined"!=typeof e.change&&(e.change?this.changeEvent=e.change:this.changeEvent=!1),this.list=new W.List(e),this.listRoot=this.list.getRoot(),e.features&&(this.features=e.features,this.populate())},t.prototype.append=function(){var t=this,n="submit_Controls";this.list.parse(),this.bodyDiv.appendChild(this.listRoot),this.options.submit&&(this.options.submit.id&&(n=this.options.submit.id,this.option.submit=this.option.submit.name),this.submit=W.add("button",this.bodyDiv,J.merge(this.options.attributes,{id:n,innerHTML:this.options.submit})),this.submit.onclick=function(){t.options.change&&e.emit(t.options.change)})},t.prototype.parse=function(){return this.list.parse()},t.prototype.populate=function(){var t,n,r,i,s,o=this;for(t in this.features)this.features.hasOwnProperty(t)&&(r=this.features[t],n=t,r.id&&(n=r.id,delete r.id),i=document.createElement("div"),s=this.add(i,n,r),this.changeEvent&&(s.onchange=function(){e.emit(o.changeEvent)}),r.label&&W.add("label",i,{"for":s.id,innerHTML:r.label}),this.list.addDT(i))},t.prototype.listeners=function(){var t=this;e.on(this.changeEvent,function(){t.hasChanged=!0})},t.prototype.refresh=function(){var e,t;for(e in this.features)this.features.hasOwnProperty(e)&&(t=W.getElementById(e),t&&(t.value=this.features[e].value));return!0},t.prototype.getValues=function(){var e,t,n;e={};for(n in this.features)this.features.hasOwnProperty(n)&&(t=W.getElementById(n),t&&(e[n]=Number(t.value)));return e},t.prototype.highlight=function(e){return W.highlight(this.listRoot,e)},n.prototype.__proto__=t.prototype,n.prototype.constructor=n,n.version="0.2.2",n.description="Collection of Sliders.",n.title="Slider Controls",n.className="slidercontrols",n.dependencies={Controls:{}},e.widgets.register("SliderControls",n),n.prototype.add=function(e,t,n){return n=n||{},n.id=t,n.type="range",W.add("input",e,n)},n.prototype.getItem=function(e,t){return t=t||{},t.id=e,W.get("input",t)},r.prototype.__proto__=t.prototype,r.prototype.constructor=r,r.version="0.14",r.description="Collection of jQuery Sliders.",r.title="jQuery Slider Controls",r.className="jqueryslidercontrols",r.dependencies={jQuery:{},Controls:{}},e.widgets.register("jQuerySliderControls",r),r.prototype.add=function(e,t,n){var r=jQuery("
",{id:t}).slider(),i=r.appendTo(e);return i[0]},r.prototype.getItem=function(e,t){var n=jQuery("
",{id:e}).slider();return n},i.prototype.__proto__=t.prototype,i.prototype.constructor=i,i.version="0.1.2",i.description="Collection of Radio Controls.",i.title="Radio Controls",i.className="radiocontrols",i.dependencies={Controls:{}},e.widgets.register("RadioControls",i),i.prototype.populate=function(){var t,n,r,i,s;s=this,this.radioElem||(this.radioElem=document.createElement("radio"),this.radioElem.group=this.name||"radioGroup",this.radioElem.group=this.className||"radioGroup",this.bodyDiv.appendChild(this.radioElem));for(t in this.features)this.features.hasOwnProperty(t)&&(r=this.features[t],n=t,r.id&&(n=r.id,delete r.id),i=this.add(this.radioElem,n,r),this.changeEvent&&(i.onchange=function(){e.emit(s.changeEvent)}),this.list.addDT(i))},i.prototype.add=function(e,t,n){var r;return"undefined"==typeof n.name&&(n.name=this.groupName),n.id=t,n.type="radio",r=W.add("input",e,n),r.appendChild(document.createTextNode(n.label)),r},i.prototype.getItem=function(e,t){return t=t||{},"undefined"==typeof t.name&&(t.name=this.groupName),t.id=e,t.type="radio",W.get("input",t)},i.prototype.getValues=function(){var e,t;for(e in this.features)if(this.features.hasOwnProperty(e)){t=W.getElementById(e);if(t.checked)return t.value}return!1}}(node),function(e){"use strict";function d(){this.input=null,this.placeholder=null,this.inputWidth=null,this.type=null,this.preprocess=null,this.validation=null,this.userValidation=null,this.validationSpeed=500,this.postprocess=null,this.oninput=null,this.params={},this.errorBox=null,this.mainText=null,this.hint=null,this.requiredChoice=null,this.required=null,this.timeBegin=null,this.timeEnd=null,this.checkbox=null,this.checkboxText=null,this.checkboxCb=null,this.orientation=null}function v(e,t){var n,r;if("string"==typeof e){e=e==="today"?new Date:new Date(e),r=e.getDate();if(!r)return!1}try{n={day:r||e.getDate(),month:e.getMonth()+1,year:e.getFullYear(),obj:e}}catch(i){return!1}return n.str=(t.dayPos?n.day+t.sep+n.month:n.month+t.sep+n.day)+t.sep,n.str+=t.yearDigits===2?n.year.substring(3,4):n.year,n}function m(e){switch(e){case"usStatesTerrByAbbrLow":return p||(m("usStatesTerrLow"),p=J.reverseObj(o,g)),p;case"usStatesTerrByAbbr":return u||(m("usStatesTerr"),u=J.reverseObj(o)),u;case"usTerrByAbbrLow":return l||(l=J.reverseObj(r,g)),l;case"usTerrByAbbr":return i||(i=J.reverseObj(r)),i;case"usStatesByAbbrLow":return c||(c=J.reverseObj(n,g)),c;case"usStatesByAbbr":return s||(s=J.reverseObj(n)),s;case"usStatesTerrLow":return h||(a||(a=y(n)),f||(f=y(r)),h=J.merge(a,f)),h;case"usStatesTerr":return o||(o=J.merge(n,r)),o;case"usStatesLow":return a||(a=y(n)),a;case"usStates":return n;case"usTerrLow":return f||(f=y(r)),f;case"usTerr":return r;default:throw new Error("getUsStatesList: unknown request: "+e)}}function g(e,t){return[e.toLowerCase(),t]}function y(e){var t,n;n={};for(t in e)e.hasOwnProperty(t)&&(n[t.toLowerCase()]=e[t]);return n}function b(e){return e.length===5&&J.isInt(e,0)}e.widgets.register("CustomInput",d),d.version="0.12.0",d.description="Creates a configurable input form",d.panel=!1,d.className="custominput",d.types={text:!0,number:!0,"float":!0,"int":!0,date:!0,list:!0,us_city_state_zip:!0,us_state:!0,us_zip:!0};var t={",":"comma"," ":"space",".":"dot"},n={Alabama:"AL",Alaska:"AK",Arizona:"AZ",Arkansas:"AR",California:"CA",Colorado:"CO",Connecticut:"CT",Delaware:"DE",Florida:"FL",Georgia:"GA",Hawaii:"HI",Idaho:"ID",Illinois:"IL",Indiana:"IN",Iowa:"IA",Kansas:"KS",Kentucky:"KY",Louisiana:"LA",Maine:"ME",Maryland:"MD",Massachusetts:"MA",Michigan:"MI",Minnesota:"MN",Mississippi:"MS",Missouri:"MO",Montana:"MT",Nebraska:"NE",Nevada:"NV","New Hampshire":"NH","New Jersey":"NJ","New Mexico":"NM","New York":"NY","North Carolina":"NC","North Dakota":"ND",Ohio:"OH",Oklahoma:"OK",Oregon:"OR",Pennsylvania:"PA","Rhode Island":"RI","South Carolina":"SC","South Dakota":"SD",Tennessee:"TN",Texas:"TX",Utah:"UT",Vermont:"VT",Virginia:"VA",Washington:"WA","West Virginia":"WV",Wisconsin:"WI",Wyoming:"WY"},r={"American Samoa":"AS","District of Columbia":"DC","Federated States of Micronesia":"FM",Guam:"GU","Marshall Islands":"MH","Northern Mariana Islands":"MP",Palau:"PW","Puerto Rico":"PR","Virgin Islands":"VI"},i,s,o,u,a,f,l,c,h,p;d.texts={listErr:"Check that there are no empty items; do not end with the separator",listSizeErr:function(e,t){return e.params.fixedSize?e.params.minItems+" items required":t==="min"?"Too few items. Min: "+e.params.minItems:"Too many items. Max: "+e.params.maxItems},usStateAbbrErr:"Not a valid state abbreviation (must be 2 characters)",usStateErr:"Not a valid state (full name required)",usZipErr:"Not a valid ZIP code (must be 5 digits)",autoHint:function(e){var n,r;if(e.type==="list")r=t[e.params.listSep]||e.params.listSep,n="(if more than one, separate with "+r+")";else if(e.type==="us_state")n=e.params.abbr?"(Use 2-letter abbreviation)":"(Type the full name of the state)";else if(e.type==="us_zip")n="(Use 5-digit ZIP code)";else if(e.type==="us_city_state_zip")r=e.params.listSep,n="(Format: Town"+r+" State"+r+" ZIP code)";else if(e.type==="date")e.params.minDate&&e.params.maxDate?n="(Must be between "+e.params.minDate.str+" and "+e.params.maxDate.str+")":e.params.minDate?n="(Must be after "+e.params.minDate.str+")":e.params.maxDate?n="(Must be before "+e.params.maxDate.str+")":n="(Format: "+e.params.format+")";else if(e.type==="number"||e.type==="int"||e.type==="float")e.params.min&&e.params.max?n="(Must be between "+e.params.min+" and "+e.params.max+")":e.params.min?n="(Must be after "+e.params.min+")":e.params.max&&(n="(Must be before "+e.params.max+")");return e.required&&e.displayRequired?(n||"")+" "+e.requiredMark:n||!1},numericErr:function(e){var t,n;return n=e.params,n.exactly?"Must enter "+n.lower:(t="Must be ",e.type==="float"?t+="a floating point number":e.type==="int"&&(t+="an integer"),n.between?(t+=" "+(n.leq?"≥ ":"<")+n.lower,t+=" and ",t+=(n.ueq?"≤ ":"> ")+n.upper):"undefined"!=typeof n.lower?t+=" "+(n.leq?"≥ ":"< ")+n.lower:"undefined"!=typeof n.upper&&(t+=" "+(n.ueq?"≤ ":"> ")+n.upper),t)},textErr:function(e,t){var n,r;return t==="num"?"Cannot contain numbers":(r=e.params,n="Must be ",r.exactly?n+="exactly "+(r.lower+1):r.between?n+="between "+r.lower+" and "+r.upper:"undefined"!=typeof r.lower?n+=" more than "+(r.lower-1):"undefined"!=typeof r.upper&&(n+=" less than "+(r.upper+1)),n+=" characters long",r.between&&(n+=" (extremes included)"),n+=". Current length: "+t,n)},dateErr:function(e,t){return t==="invalid"?"Date is invalid":t==="min"?"Date must be after "+e.params.minDate.str:t==="max"?"Date must be before "+e.params.maxDate.str:"Must follow format "+e.params.format},emptyErr:"Cannot be empty"},d.prototype.init=function(e){var t,n,r,i,s,o;r=this,i="CustomInput.init: ";if("undefined"==typeof e.orientation)t="V";else{if("string"!=typeof e.orientation)throw new TypeError("CustomInput.init: orientation must be string, or undefined. Found: "+e.orientation);t=e.orientation.toLowerCase().trim();if(t==="h")t="H";else{if(t!=="v")throw new Error("CustomInput.init: unknown orientation: "+t);t="V"}}this.orientation=t,"undefined"!=typeof e.required&&(this.required=this.requiredChoice=!!e.required);if("undefined"!=typeof e.requiredChoice){if(!!this.required!=!!e.requiredChoice)throw new TypeError("CustomInput.init: required and requiredChoice are incompatible. Option requiredChoice will be deprecated.");this.required=this.requiredChoice=!!e.requiredChoice}"undefined"==typeof this.required&&(this.required=this.requiredChoice=!1);if(e.userValidation){if("function"!=typeof e.userValidation)throw new TypeError("CustomInput.init: userValidation must be function or undefined. Found: "+e.userValidation);this.userValidation=e.userValidation}if(e.type){if(!d.types[e.type])throw new Error(i+"type not supported: "+e.type);this.type=e.type}else this.type="text";if(e.validation){if("function"!=typeof e.validation)throw new TypeError(i+"validation must be function "+"or undefined. Found: "+e.validation);n=e.validation}if(this.type==="number"||this.type==="float"||this.type==="int"||this.type==="text"){s=this.type==="text";if("undefined"!=typeof e.min){t=J.isNumber(e.min);if(!1===t)throw new TypeError(i+"min must be number or "+"undefined. Found: "+e.min);this.params.lower=e.min,this.params.leq=!0}if("undefined"!=typeof e.max){t=J.isNumber(e.max);if(!1===t)throw new TypeError(i+"max must be number or "+"undefined. Found: "+e.max);this.params.upper=e.max,this.params.ueq=!0}e.strictlyGreater&&(this.params.leq=!1),e.strictlyLess&&(this.params.ueq=!1);if("undefined"!=typeof this.params.lower&&"undefined"!=typeof this.params.upper){if(this.params.lower>this.params.upper)throw new TypeError(i+"min cannot be greater "+"than max. Found: "+e.min+"> "+e.max);if(this.params.lower===this.params.upper){if(!this.params.leq||!this.params.ueq)throw new TypeError(i+"min cannot be equal to "+"max when strictlyGreater or "+"strictlyLess are set. "+"Found: "+e.min);if(this.type==="int"||this.type==="text")if(J.isFloat(this.params.lower))throw new TypeError(i+"min cannot be a "+"floating point number "+"and equal to "+"max, when type "+'is not "float". Found: '+e.min);this.params.exactly=!0}else this.params.between=!0}if(s){this.params.noNumbers=e.noNumbers;if("undefined"!=typeof this.params.lower){if(this.params.lower<0)throw new TypeError(i+"min cannot be negative "+'when type is "text". Found: '+this.params.lower);this.params.leq||this.params.lower++}if("undefined"!=typeof this.params.upper){if(this.params.upper<0)throw new TypeError(i+"max cannot be negative "+'when type is "text". Found: '+this.params.upper);this.params.ueq||this.params.upper--}n||(n=function(e){var t,n,i,s;n=r.params,t=e.length,i={value:e};if(n.noNumbers&&/\d/.test(e))s=r.getText("textErr","num");else{if(n.exactly)s=t!==n.lower;else if("undefined"!=typeof n.lower&&tn.upper)s=!0;s&&(s=r.getText("textErr",t))}return s&&(i.err=s),i}),o=function(){var e,t;return e="undefined"!=typeof r.params.lower?r.params.lower+1:5,t="undefined"!=typeof r.params.upper?r.params.upper:e+5,J.randomString(J.randomInt(e,t))}}else n||(n=function(){var e;return r.type==="float"?e=J.isFloat:r.type==="int"?e=J.isInt:e=J.isNumber,function(t){var n,i;return i=r.params,n=e(t,i.lower,i.upper,i.leq,i.ueq),n!==!1?{value:n}:{value:t,err:r.getText("numericErr")}}}()),o=function(){var e,t,n;return e=r.params,r.type==="float"?J.random():(t=0,"undefined"!=typeof e.lower&&(t=e.leq?e.lower-1:e.lower),"undefined"!=typeof e.upper?n=e.ueq?e.upper:e.upper-1:n=100+t,J.randomInt(t,n))};this.params.upper&&(this.params.upper<10?this.inputWidth="100px":this.params.upper<20&&(this.inputWidth="200px"))}else if(this.type==="date"){if("undefined"!=typeof e.format){if(e.format!=="mm-dd-yy"&&e.format!=="dd-mm-yy"&&e.format!=="mm-dd-yyyy"&&e.format!=="dd-mm-yyyy"&&e.format!=="mm.dd.yy"&&e.format!=="dd.mm.yy"&&e.format!=="mm.dd.yyyy"&&e.format!=="dd.mm.yyyy"&&e.format!=="mm/dd/yy"&&e.format!=="dd/mm/yy"&&e.format!=="mm/dd/yyyy"&&e.format!=="dd/mm/yyyy")throw new Error(i+"date format is invalid. Found: "+e.format);this.params.format=e.format}else this.params.format="mm/dd/yyyy";this.params.sep=this.params.format.charAt(2),t=this.params.format.split(this.params.sep),this.params.yearDigits=t[2].length,this.params.dayPos=t[0].charAt(0)==="d"?0:1,this.params.monthPos=this.params.dayPos?0:1,this.params.dateLen=t[2].length+6;if(e.minDate){t=v(e.minDate,this.params);if(!t)throw new Error(i+"minDate must be a Date object. "+"Found: "+e.minDate);this.params.minDate=t}if(e.maxDate){t=v(e.maxDate,this.params);if(!t)throw new Error(i+"maxDate must be a Date object. "+"Found: "+e.maxDate);if(this.params.minDate&&this.params.minDate.obj>t.obj)throw new Error(i+"maxDate cannot be prior to "+"minDate. Found: "+t.str+" < "+this.params.minDate.str);this.params.maxDate=t}this.params.yearDigits===2?this.inputWidth="100px":this.inputWidth="150px",this.placeholder=this.params.format,n||(n=function(e){var t,n,i,s,o,u,a;t=r.params,n=e.split(t.sep);if(n.length!==3)return{err:r.getText("dateErr")};if(n[2].length!==t.yearDigits)return{err:r.getText("dateErr")};s={},t.yearDigits===2?(u=-1,a=100):(u=-1,a=1e4),i=J.isInt(n[2],u,a),i!==!1?s.year=i:s.err=!0,i=J.isInt(n[t.monthPos],1,12,1,1),i?s.month=i:s.err=!0,i===1||i===3||i===5||i===7||i===8||i===10||i===12?o=31:i!==2?o=30:o=s.year%4===0&&s.year%100!==0||s.year%400===0?29:28,s.month=i,i=J.isInt(n[t.dayPos],1,o,1,1),i?s.day=i:s.err=!0;if(s.err)s.err=r.getText("dateErr","invalid");else if(t.minDate||t.maxDate)i=new Date(e),t.minDate.obj&&t.minDate.obj>i?s.err=r.getText("dateErr","min"):t.maxDate.obj&&t.maxDate.objt)throw new TypeError(i+"maxItems must be larger "+"than minItems. Found: "+t+" < "+this.params.minItems);this.params.maxItems=t}}n||(n=function(e){var t,n,i,s,o;e=e.split(r.params.listSep),n=e.length;if(!n)return e;s=r.params.itemValidation,t=0,i=e[0].trim();if(!i)return{err:r.getText("listErr")};if(s){o=s(i,1);if(o)return o}e[t++]=i;if(n>1){i=e[1].trim();if(!i)return{err:r.getText("listErr")};if(s){o=s(i,t+1);if(o)return o}e[t++]=i}if(n>2){i=e[2].trim();if(!i)return{err:r.getText("listErr")};if(s){o=s(i,t+1);if(o)return o}e[t++]=i}if(n>3)for(;tr.params.maxItems?{err:r.getText("listSizeErr","max")}:{value:e}}),this.type==="us_city_state_zip"?o=function(){var e;return e=r.params.listSep+" ",J.randomString(8)+e+J.randomKey(u)+e+(Math.floor(Math.random()*9e4)+1e4)}:o=function(e){var t,n,i,s,o,u;t=r.params,n=t.minItems||0,e.availableValues?(i=J.randomInt(n,e.availableValues.length),i--,u=J.sample(0,i-1)):(i=J.randomInt(n,t.maxItems||n+5),i--),o="";for(s=0;sthis.params.dateLen&&(e.value=e.value.substring(0,this.params.dateLen))}:(this.type==="list"||this.type==="us_city_state_zip")&&this.params.listSep.trim()!==""&&(this.preprocess=function(e){var t,n;n=e.value.length,t=r.params.listSep,n>1&&n===e.selectionStart&&e.value.charAt(n-1)===t&&e.value.charAt(n-2)!==t&&(e.value+=" ")}));if(e.postprocess){if("function"!=typeof e.postprocess)throw new TypeError(i+"postprocess must be function or "+"undefined. Found: "+e.postprocess);this.postprocess=e.postprocess}if(e.oninput){if("function"!=typeof e.oninput)throw new TypeError(i+"oninput must be function or "+"undefined. Found: "+e.oninput);this.oninput=e.oninput}if("undefined"!=typeof e.validationSpeed){t=J.isInt(e.valiadtionSpeed,0,undefined,!0);if(t===!1)throw new TypeError(i+"validationSpeed must a non-negative "+"number or undefined. Found: "+e.validationSpeed);this.validationSpeed=t}if(e.mainText){if("string"!=typeof e.mainText)throw new TypeError(i+"mainText must be string or "+"undefined. Found: "+e.mainText);this.mainText=e.mainText}if("undefined"!=typeof e.hint){if(!1!==e.hint&&"string"!=typeof e.hint)throw new TypeError(i+"hint must be a string, false, or "+"undefined. Found: "+e.hint);this.hint=e.hint,this.required&&this.displayRequired&&(this.hint+=" "+this.requiredMark)}else this.hint=this.getText("autoHint");if(e.placeholder){if("string"!=typeof e.placeholder)throw new TypeError(i+"placeholder must be string or "+"undefined. Found: "+e.placeholder);this.placeholder=e.placeholder}if(e.width){if("string"!=typeof e.width)throw new TypeError(i+"width must be string or "+"undefined. Found: "+e.width);this.inputWidth=e.width}if(e.checkboxText){if("string"!=typeof e.checkboxText)throw new TypeError(i+"checkboxText must be string or "+"undefined. Found: "+e.checkboxText);this.checkboxText=e.checkboxText}if(e.checkboxCb){if(!this.checkboxText)throw new TypeError(i+"checkboxCb cannot be defined "+"if checkboxText is not defined");if("function"!=typeof e.checkboxCb)throw new TypeError(i+"checkboxCb must be function or "+"undefined. Found: "+e.checkboxCb);this.checkboxCb=e.checkboxCb}},d.prototype.append=function(){var t,n;t=this,this.mainText&&(this.spanMainText=W.append("span",this.bodyDiv,{className:"custominput-maintext",innerHTML:this.mainText})),this.hint&&W.append("span",this.spanMainText||this.bodyDiv,{className:"custominput-hint",innerHTML:this.hint}),this.input=W.append("input",this.bodyDiv),this.placeholder&&(this.input.placeholder=this.placeholder),this.inputWidth&&(this.input.style.width=this.inputWidth),this.errorBox=W.append("div",this.bodyDiv,{className:"errbox"}),this.input.oninput=function(){t.timeBegin?t.timeEnd=e.timer.getTimeSince("step"):t.timeEnd=t.timeBegin=e.timer.getTimeSince("step"),n&&clearTimeout(n),t.isHighlighted()&&t.unhighlight(),t.preprocess&&t.preprocess(t.input),n=setTimeout(function(){var e;t.validation&&(e=t.validation(t.input.value),e.err&&t.setError(e.err)),t.oninput&&t.oninput(e,t)},t.validationSpeed)},this.input.onclick=function(){t.isHighlighted()&&t.unhighlight()},this.checkboxText&&(this.checkbox=W.append("input",this.bodyDiv,{type:"checkbox",className:"custominput-checkbox"}),W.append("span",this.bodyDiv,{className:"custominput-checkbox-text",innerHTML:this.checkboxText}),this.checkboxCb&&J.addEvent(this.checkbox,"change",function(){t.checkboxCb(t.checkbox.checked,t)}))},d.prototype.setError=function(e){this.errorBox.innerHTML=e,this.highlight()},d.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("CustomInput.highlight: border must be string or undefined. Found: "+e);if(!this.input||this.highlighted)return;this.input.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},d.prototype.unhighlight=function(){if(!this.input||this.highlighted!==!0)return;this.input.style.border="",this.highlighted=!1,this.errorBox.innerHTML="",this.emit("unhighlighted")},d.prototype.disable=function(e){if(this.disabled)return;if(!this.isAppended())return;this.disabled=!0,this.input.disabled=!0,this.checkbox&&(!e||e.checkbox!==!1)&&(this.checkbox.disable=!0),this.emit("disabled")},d.prototype.enable=function(e){if(this.disabled!==!0)return;if(!this.isAppended())return;this.disabled=!1,this.input.disabled=!1,this.checkbox&&(!e||e.checkbox!==!1)&&(this.checkbox.disable=!1),this.emit("enabled")},d.prototype.reset=function(){this.input&&(this.input.value=""),this.isHighlighted()&&this.unhighlight(),this.timeBegin=this.timeEnd=null},d.prototype.getValues=function(e){var t,n;return e=e||{},t=this.input.value,e.valuesOnly?t:("undefined"==typeof e.markAttempt&&(e.markAttempt=!0),"undefined"==typeof e.highlight&&(e.highlight=!0),t=this.validation?this.validation(t):{value:t},n=!t.err,t.timeBegin=this.timeBegin,t.timeEnd=this.timeEnd,this.postprocess&&(t.value=this.postprocess(t.value,n)),n?(e.markAttempt&&(t.isCorrect=!0),e.reset&&this.reset()):(e.highlight&&this.setError(t.err),e.markAttempt&&(t.isCorrect=!1)),this.checkbox&&(t.checked=this.checkbox.checked),t.id=this.id,t)},d.prototype.setValues=function(e){var t,n;e=e||{};if("undefined"!=typeof e.value)t=e.value;else if("undefined"!=typeof e.values)t=e.values;else if(e.availableValues){n=e.availableValues;if(!J.isArray(n)||!n.length)throw new TypeError("CustomInput.setValues: availableValues must be a non-empty array or undefined. Found: "+n);if(this.type==="list"){if(n.lengththis.n&&(this.path.transition().duration(500).ease("linear").attr("transform","translate("+t(-1)+")"),this.data.shift())}}(node),function(e){"use strict";function n(){this.table=null,this.interval=null,this.intervalTime=1e3}var t=W.Table;e.widgets.register("DebugInfo",n),n.version="0.6.2",n.description="Display basic info a client's status.",n.title="Debug Info",n.className="debuginfo",n.dependencies={Table:{}},n.prototype.init=function(t){var n;"number"==typeof t.intervalTime&&(this.intervalTime=t.intervalTime),n=this,this.on("destroyed",function(){clearInterval(n.interval),n.interval=null,e.silly("DebugInfo destroyed.")})},n.prototype.append=function(){var e;this.table=new t,this.bodyDiv.appendChild(this.table.table),this.updateAll(),e=this,this.interval=setInterval(function(){e.updateAll()},this.intervalTime)},n.prototype.updateAll=function(){var t,n,r,i,s,o,u,a,f,l,c,h;if(!this.bodyDiv){e.err("DebugInfo.updateAll: bodyDiv not found.");return}h="-",r=h,n=h,t=e.game.getCurrentGameStage(),t&&(c=e.game.plot.getStep(t),r=c?c.id:"-",n=t.toString()),s=J.getKeyByValue(e.constants.stageLevels,e.game.getStageLevel()),o=J.getKeyByValue(e.constants.stateLevels,e.game.getStateLevel()),u=J.getKeyByValue(e.constants.windowLevels,W.getStateLevel()),i=e.player?e.player.id:h,a=e.errorManager.lastErr||h,l=e.game.settings&&e.game.settings.treatmentName?e.game.settings.treatmentName:h,f=e.socket.connected?"yes":"no",this.table.clear(!0),this.table.addRow(["Treatment: ",l]),this.table.addRow(["Connected: ",f]),this.table.addRow(["Player Id: ",i]),this.table.addRow(["Stage No: ",n]),this.table.addRow(["Stage Id: ",r]),this.table.addRow(["Stage Lvl: ",s]),this.table.addRow(["State Lvl: ",o]),this.table.addRow(["Players : ",e.game.pl.size()]),this.table.addRow(["Win Lvl: ",u]),this.table.addRow(["Win Loads: ",W.areLoading]),this.table.addRow(["Last Err: ",a]),this.table.parse()}}(node),function(e){"use strict";function t(){this.buttonsDiv=null,this.hiddenTypes={},this.counterIn=0,this.counterOut=0,this.counterLog=0,this.wall=null,this.wallDiv=null,this.origMsgInCb=null,this.origMsgOutCb=null,this.origLogCb=null}e.widgets.register("DebugWall",t),t.version="1.1.0",t.description="Intercepts incoming and outgoing messages, and logs and prints them numbered and timestamped. Warning! Modifies core functions, therefore its usage in production is not recommended.",t.title="Debug Wall",t.className="debugwall",t.prototype.init=function(t){var n;n=this,t.msgIn!==!1&&(this.origMsgInCb=e.socket.onMessage,e.socket.onMessage=function(t){n.write("in",n.makeTextIn(t)),n.origMsgInCb.call(e.socket,t)}),t.msgOut!==!1&&(this.origMsgOutCb=e.socket.send,e.socket.send=function(t){n.write("out",n.makeTextOut(t)),n.origMsgOutCb.call(e.socket,t)}),t.log!==!1&&(this.origLogCb=e.log,e.log=function(t,r,i){n.write(r||"info",n.makeTextLog(t,r,i)),n.origLogCb.call(e,t,r,i)});if(t.hiddenTypes){if("object"!=typeof t.hiddenTypes)throw new TypeError("DebugWall.init: hiddenTypes must be object. Found: "+t.hiddenTypes);this.hiddenTypes=t.hiddenTypes}this.on("destroyed",function(){n.origLogCb&&(e.log=n.origLogCb),n.origMsgOutCb&&(e.socket.send=n.origMsgOutCb),n.origMsgInCb&&(e.socket.onMessage=n.origMsgInCb)})},t.prototype.append=function(){var e,t,n,r,i,s;this.buttonsDiv=W.add("div",this.bodyDiv,{className:"wallbuttonsdiv"}),i=W.add("div",this.buttonsDiv,{className:"btn-group",role:"group","aria-label":"Toggle visibility of messages on wall"}),W.add("input",i,{id:"debug-wall-incoming",className:"btn-check",autocomplete:"off",checked:!0,type:"checkbox"}),e=W.add("label",i,{className:"btn btn-outline-primary","for":"debug-wall-incoming",innerHTML:"Incoming"}),W.add("input",i,{id:"debug-wall-outgoing",className:"btn-check",autocomplete:"off",checked:!0,type:"checkbox"}),t=W.add("label",i,{className:"btn btn-outline-primary","for":"debug-wall-outgoing",innerHTML:"Outgoing"}),W.add("input",i,{id:"debug-wall-log",className:"btn-check",autocomplete:"off",checked:!0,type:"checkbox"}),n=W.add("label",i,{className:"btn btn-outline-primary","for":"debug-wall-log",innerHTML:"Log"}),r=this,W.add("button",this.buttonsDiv,{className:"btn btn-outline-danger me-2",innerHTML:"Clear"}).onclick=function(){r.clear()},this.buttonsDiv.appendChild(i),s=function(e){var t,n,i,s;s="wall_"+e,t=r.wall.getElementsByClassName(s);if(!t||!t.length)return;i=t[0].style.display===""?"none":"";for(n=0;na?(r=W.add("span",l,{className:u+"_click",innerHTML:n.substr(0,a)}),s=W.add("span",r,{className:u+"_extra",innerHTML:n.substr(a,n.length),id:"wall_"+t+"_"+o,style:{display:"none"}}),i=W.add("span",r,{className:u+"_dots",innerHTML:" ...",id:"wall_"+t+"_"+o}),r.onclick=function(){i.style.display==="none"?(i.style.display="",s.style.display="none"):(i.style.display="none",s.style.display="")}):r=W.add("span",l,{innerHTML:n}),this.wallDiv.scrollTop=this.wallDiv.scrollHeight):e.warn("Wall not appended, cannot write.")},t.prototype.makeTextIn=function(e){var t,n;return n=new Date(e.created),t=n.getHours()+":"+n.getMinutes()+":"+n.getSeconds()+":"+n.getMilliseconds(),t+=" | "+e.to+" | "+e.target+" | "+e.action+" | "+e.text+" | "+e.data,t},t.prototype.makeTextOut=function(e){var t;return t=e.from+" | "+e.target+" | "+e.action+" | "+e.text+" | "+e.data,t},t.prototype.makeTextLog=function(e){return e}}(node),function(e){"use strict";function t(){this.showStatus=null,this.showDiscBtn=null,this.statusSpan=null,this.disconnectBtn=null,this.userDiscFlag=null,this.ee=null,this.disconnectCb=null,this.connectCb=null}e.widgets.register("DisconnectBox",t),t.version="0.4.0",t.description="Monitors and handles disconnections",t.panel=!1,t.className="disconnectbox",t.texts={leave:"Leave Task",left:"You Left",disconnected:"Disconnected!",connected:"Connected"},t.dependencies={},t.prototype.init=function(e){if(e.connectCb){if("function"!=typeof e.connectCb)throw new TypeError("DisconnectBox.init: connectCb must be function or undefined. Found: "+e.connectCb);this.connectCb=e.connectCb}if(e.disconnectCb){if("function"!=typeof e.disconnectCb)throw new TypeError("DisconnectBox.init: disconnectCb must be function or undefined. Found: "+e.disconnectCb);this.disconnectCb=e.disconnectCb}this.showDiscBtn=!!e.showDiscBtn,this.showStatus=!!e.showStatus},t.prototype.append=function(){var t,n;t=this,n=e.socket.isConnected(),this.showStatus&&(this.statusSpan=W.add("span",this.bodyDiv),this.updateStatus(n?"connected":"disconnected")),this.showDiscBtn&&(this.disconnectBtn=W.add("button",this.bodyDiv,{innerHTML:this.getText(n?"leave":"left"),className:"btn",style:{"margin-left":"10px"}}),n||(this.disconnectBtn.disabled=!0),this.disconnectBtn.onclick=function(){t.disconnectBtn.disabled=!0,t.userDiscFlag=!0,e.socket.disconnect()})},t.prototype.updateStatus=function(t){if(!this.statusSpan){e.warn("DisconnectBox.updateStatus: display disabled.");return}this.statusSpan.innerHTML=this.getText(t),this.statusSpan.className=t==="disconnected"?"text-danger":""},t.prototype.listeners=function(){var t;t=this,this.ee=e.getCurrentEventEmitter(),this.ee.on("SOCKET_DISCONNECT",function(){t.statusSpan&&t.updateStatus("disconnected"),t.disconnectBtn&&(t.disconnectBtn.disabled=!0,t.disconnectBtn.innerHTML=t.getText("left")),t.disconnectCb&&t.disconnectCb(t.userDiscFlag)}),this.ee.on("SOCKET_CONNECT",function(){t.statusSpan&&t.updateStatus("connected"),t.disconnectBtn&&(t.disconnectBtn.disabled=!1,t.disconnectBtn.innerHTML=t.getText("leave")),t.connectCb&&t.disconnectCb(),t.userDiscFlag=!1})}}(node),function(e){"use strict";function t(t){var n;n=this;if("object"==typeof t.button)this.button=t.button;else{if("undefined"!=typeof t.button)throw new TypeError("DoneButton constructor: options.button must be object or undefined. Found: "+t.button);this.button=document.createElement("button")}this.button.onclick=function(){if(n.onclick&&!1===n.onclick())return;if(e.game.isWidgetStep()&&e.widgets.last.next()!==!1)return;e.done()&&n.disable()},this.onclick=null,this.disableOnDisconnect=null,this.delayOnPlaying=800}function n(t,n,r){var i;if("undefined"!=typeof n){if("function"!=typeof n&&n!==null)throw i="DoneButton.init",r&&(i+=" (step property)"),new TypeError(i+": onclick must be function, null,"+" or undefined. Found: "+n);t.onclick=n}r&&e.once("REALLY_DONE",function(){t.onclick=null})}e.widgets.register("DoneButton",t),t.version="1.1.0",t.description="Creates a button that if pressed emits node.done().",t.panel=!1,t.className="donebutton",t.texts.done="Done",t.prototype.init=function(e){var r;e=e||{};if("undefined"==typeof e.id)r=t.className;else if("string"==typeof e.id)r=e.id;else{if(!1!==e.id)throw new TypeError("DoneButton.init: id must be string, false, or undefined. Found: "+e.id);r=!1}r&&(this.button.id=r);if("undefined"==typeof e.classNameBtn)r="btn btn-lg btn-primary";else if(e.classNameBtn===!1)r="";else if("string"==typeof e.classNameBtn)r=e.classNameBtn;else{if(!J.isArray(e.classNameBtn))throw new TypeError("DoneButton.init: classNameBtn must be string, array, or undefined. Found: "+e.classNameBtn);r=e.classNameBtn.join(" ")}this.button.className=r,this.button.innerHTML="string"==typeof e.text?e.text:this.getText("done"),this.disableOnDisconnect="undefined"==typeof e.disableOnDisconnect?!0:!!e.disableOnDisconnect,r=e.delayOnPlaying;if("number"==typeof r)this.delayOnPlaying=r;else if("undefined"!=typeof r)throw new TypeError("DoneButton.init: delayOnPlaying must be number or undefined. Found: "+r);n(this,e.onclick)},t.prototype.append=function(){e.game.isReady()||(this.disabled=!0,this.button.disabled=!0),this.bodyDiv.appendChild(this.button)},t.prototype.listeners=function(){var t,r;t=this,e.on("PLAYING",function(){var i,s;i=e.game.getProperty("donebutton"),i===!1||i&&i.enableOnPlaying===!1?t.disable():(i&&i.hasOwnProperty&&i.hasOwnProperty("delayOnPlaying")?s=i.delayOnPlaying:s=t.delayOnPlaying,s?setTimeout(function(){r||t.enable()},s):t.enable()),"string"==typeof i?t.button.innerHTML=i:i&&(i.text&&(t.button.innerHTML=i.text),i.onclick&&n(t,i.onclick,!0))}),this.disableOnDisconnect&&(e.on("SOCKET_DISCONNECT",function(){t.isDisabled()||(t.disable(),r=!0)}),e.on("SOCKET_CONNECT",function(){r&&(t.isDisabled()&&t.enable(),r=!1)}))},t.prototype.updateText=function(t,n){var r,i;n&&(i=this,r=this.button.innerHTML,e.timer.setTimeout(function(){i.button.innerHTML=r},n)),this.button.innerHTML=t},t.prototype.disable=function(e){if(this.disabled)return;this.disabled=!0,this.button.disabled=!0,this.emit("disabled",e)},t.prototype.enable=function(e){if(!this.disabled)return;this.disabled=!1,this.button.disabled=!1,this.emit("enabled",e)}}(node),function(e){function t(){var t;t=this,this.id=null,this.mainText=null,this.hint=null,this.labelText=null,this.placeholder=null,this.choices=null,this.tag=null,this.menu=null,this.datalist=null,this.listener=function(n){var r,i;n=n||window.event,r=n.target||n.srcElement,t.currentChoice=r.value,t.currentChoice.length===0&&(t.currentChoice=null),"string"==typeof t.timeFrom?t.timeCurrentChoice=e.timer.getTimeSince(t.timeFrom):t.timeCurrentChoice=Date.now?Date.now():(new Date).getTime(),t.numberOfChanges++,t.isHighlighted()&&t.unhighlight(),i&&clearTimeout(i),i=setTimeout(function(){t.verifyChoice(),t.verifyChoice().err&&t.setError(t.verifyChoice().err)},t.validationSpeed),t.onchange&&t.onchange(t.currentChoice,r,t)},this.onchange=null,this.timeCurrentChoice=null,this.timeFrom="step",this.numberOfChanges=0,this.currentChoice=null,this.shuffleChoices=null,this.order=null,this.errorBox=null,this.correctChoice=null,this.requiredChoice=null,this.fixedChoice=null,this.inputWidth=null,this.validation=null,this.validationSpeed=500}function n(e,t){return r(e.choices[t])}function r(e){return"object"==typeof e&&("undefined"!=typeof e.name?e=e.name:e=e[1]),e}function i(e,t){var n,r,i;r=e.choices.length;for(n=0;ne.choices.length)throw new Error("Dropdown.init: correctChoice length cannot exceed opts.choices length");this.correctChoice=e.correctChoice}if("boolean"==typeof e.fixedChoice)this.fixedChoice=e.fixedChoice;else if("undefined"!=typeof e.fixedChoice)throw new TypeError("Dropdown.init: fixedChoice be boolean or undefined. Found: "+e.fixedChoice);if("undefined"==typeof e.tag)this.tag="datalist";else{if("datalist"!==e.tag&&"select"!==e.tag)throw new TypeError('Dropdown.init: tag must be "datalist", "select" or undefined. Found: '+e.tag);this.tag=e.tag}if("function"==typeof e.listener)this.listener=function(t){e.listener.call(this,t)};else if("undefined"!=typeof e.listener)throw new TypeError("Dropdown.init: listener must be function or undefined. Found: "+e.listener);if("function"==typeof e.onchange)this.onchange=e.onchange;else if("undefined"!=typeof e.onchange)throw new TypeError("Dropdownn.init: onchange must be function or undefined. Found: "+e.onchange);if("function"==typeof e.validation)this.validation=e.validation;else if("undefined"!=typeof e.validation)throw new TypeError("Dropdownn.init: validation must be function or undefined. Found: "+e.validation);"undefined"==typeof e.shuffleChoices?t=!1:t=!!e.shuffleChoices,this.shuffleChoices=t;if(e.width){if("string"!=typeof e.width)throw new TypeError("Dropdownn.init:width must be string or undefined. Found: "+e.width);this.inputWidth=e.width}if("undefined"!=typeof e.validationSpeed){t=J.isInt(e.valiadtionSpeed,0,undefined,!0);if(t===!1)throw new TypeError("Dropdownn.init: validationSpeed must a non-negative number or undefined. Found: "+e.validationSpeed);this.validationSpeed=t}t=e.hint;if("function"==typeof t){t=t.call(this);if("string"!=typeof t&&!1!==t)throw new TypeError("Dropdown.init: hint cb must return string or false. Found: "+t)}if("string"==typeof t||!1===t)this.hint=t;else if("undefined"!=typeof t)throw new TypeError("Dropdown.init: hint must be a string, false, or undefined. Found: "+t);this.requiredChoice&&t!==!1&&e.displayRequired!==!1&&(this.hint=t?this.hint+" "+this.requiredMark:" "+this.requiredMark)},t.prototype.append=function(){if(W.gid(this.id))throw new Error("Dropdown.append: id is not unique: "+this.id);var e;this.mainText&&(e=W.append("span",this.bodyDiv,{className:"dropdown-maintext",innerHTML:this.mainText})),this.hint&&W.append("span",e||this.bodyDiv,{className:"dropdown-hint",innerHTML:this.hint}),this.labelText&&W.append("label",this.bodyDiv,{innerHTML:this.labelText}),this.setChoices(this.choices,!0),this.errorBox=W.append("div",this.bodyDiv,{className:"errbox"})},t.prototype.setChoices=function(e,t){var n,r,i,s,o,u,a;this.choices=e;if(!t)return;n=this.tag==="datalist",this.menu?(i=n?this.datalist:this.menu,i.innerHTML=""):n?(this.menu=W.add("input",this.bodyDiv,{id:this.id,autocomplete:"off"}),this.datalist=i=W.add("datalist",this.bodyDiv,{id:this.id+"_datalist"}),this.menu.setAttribute("list",this.datalist.id)):(i=W.get("select"),i.id=this.id,this.bodyDiv.appendChild(i),this.menu=i),this.inputWidth&&(this.menu.style.width=this.inputWidth),this.placeholder&&(n?this.menu.placeholder=this.placeholder:W.add("option",this.menu,{value:"",innerHTML:this.placeholder,disabled:"",selected:"",hidden:""})),o=e.length,r=J.seq(0,o-1),this.shuffleChoices&&(r=J.shuffle(r));for(s=0;s=0),this.fixedChoice&&this.choices.indexOf(n)<0&&(i.value=!1),this.validation&&this.validation(this.currentChoice,i),i},t.prototype.setError=function(e){this.errorBox&&(this.errorBox.innerHTML=e||""),e?this.highlight():this.unhighlight()},t.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("Dropdown.highlight: border must be string or undefined. Found: "+e);if(this.highlighted)return;this.menu.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},t.prototype.unhighlight=function(){if(this.highlighted!==!0)return;this.menu.style.border="",this.highlighted=!1,this.setError(),this.emit("unhighlighted")},t.prototype.selectChoice=function(t){var r;if(!this.choices||!this.choices.length)return;if("undefined"==typeof t)return;r=t;if(this.tag==="select"){if("string"==typeof t){r=i(this,t);if(r===-1){e.warn("Dropdown.selectChoice: choice not found: "+t);return}}else if(null===t||!1===t)r=0;else{if("number"!=typeof t)throw new TypeError("Dropdown.selectChoice: invalid choice: "+t);r++}this.menu.selectedIndex=r}else{if("number"==typeof t){r=n(this,t);if("undefined"==typeof r){e.warn("Dropdown.selectChoice: choice not found: "+t);return}}else if("string"!=typeof t)throw new TypeError("Dropdown.selectChoice: invalid choice: "+t);this.menu.value=r}return this.listener({target:this.menu}),r},t.prototype.setValues=function(e){var t,n,r,i,s,o;if(!this.choices||!this.choices.length)throw new Error("Dropdown.setValues: no choices found.");"undefined"==typeof e&&(e={});if(e.correct&&this.correctChoice!==null){n=J.isArray(this.correctChoice)?this.correctChoice:[this.correctChoice],r=-1,i=n.length;for(;++re.length-3&&(e=e.substring(0,r+3))),n?Number(e):e}e.widgets.register("EndScreen",t),t.version="0.8.0",t.description="Game end screen. With end game message, email form, and exit code.",t.className="endscreen",t.texts={headerMessage:"Thank you for participating!",message:"You have now completed this task and your data has been saved. Please go back to the Amazon Mechanical Turk web site and submit the HIT.",totalWin:"Your total win:",exitCode:"Your exit code:",errTotalWin:"Error: invalid total win.",errExitCode:"Error: invalid exit code.",copyButton:"Copy",exitCopyMsg:"Exit code copied to clipboard.",exitCopyError:"Failed to copy exit code. Please copy it manually."},t.dependencies={Feedback:{},EmailForm:{}},t.prototype.init=function(t){"undefined"!=typeof t.askServer&&(this.askServer=!!t.askServer);if(t.email===!1)this.showEmailForm=!1;else if("boolean"==typeof t.showEmailForm)this.showEmailForm=t.showEmailForm;else if("undefined"!=typeof t.showEmailForm)throw new TypeError("EndScreen.init: opts.showEmailForm must be boolean or undefined. Found: "+t.showEmailForm);if(t.feedback===!1)this.showFeedbackForm=!1;else if("boolean"==typeof t.showFeedbackForm)this.showFeedbackForm=t.showFeedbackForm;else if("undefined"!=typeof t.showFeedbackForm)throw new TypeError("EndScreen.init: opts.showFeedbackForm must be boolean or undefined. Found: "+t.showFeedbackForm);if(t.totalWin===!1)this.showTotalWin=!1;else if("boolean"==typeof t.showTotalWin)this.showTotalWin=t.showTotalWin;else if("undefined"!=typeof t.showTotalWin)throw new TypeError("EndScreen.init: opts.showTotalWin must be boolean or undefined. Found: "+t.showTotalWin);if(t.exitCode===!1)t.showExitCode!==!1;else if("boolean"==typeof t.showExitCode)this.showExitCode=t.showExitCode;else if("undefined"!=typeof t.showExitCode)throw new TypeError("EndScreen.init: opts.showExitCode must be boolean or undefined. Found: "+t.showExitCode);if("string"==typeof t.totalWinCurrency&&t.totalWinCurrency.trim()!=="")this.totalWinCurrency=t.totalWinCurrency;else if("undefined"!=typeof t.totalWinCurrency)throw new TypeError("EndScreen.init: opts.totalWinCurrency must be undefined or a non-empty string. Found: "+t.totalWinCurrency);if(t.totalWinCb){if("function"!=typeof t.totalWinCb)throw new TypeError("EndScreen.init: opts.totalWinCb must be function or undefined. Found: "+t.totalWinCb);this.totalWinCb=t.totalWinCb}this.showEmailForm&&!this.emailForm&&(this.emailForm=e.widgets.get("EmailForm",J.mixin({onsubmit:{send:!0,emailOnly:!0,updateUI:!0},storeRef:!1,texts:{label:"If you would like to be contacted for future studies, please enter your email (optional):",errString:"Please enter a valid email and retry"},setMsg:!0},t.email))),this.showFeedbackForm&&(this.feedback=e.widgets.get("Feedback",J.mixin({storeRef:!1,minChars:50,setMsg:!0},t.feedback)))},t.prototype.append=function(){this.endScreenHTML=this.makeEndScreen(),this.bodyDiv.appendChild(this.endScreenHTML),this.askServer&&setTimeout(function(){e.say("WIN")})},t.prototype.makeEndScreen=function(){var t,n,r,i,s,o,u,a,f,l,c,h,p=this;return t=document.createElement("div"),t.className="endscreen",n=document.createElement("h1"),n.innerHTML=this.getText("headerMessage"),t.appendChild(n),r=document.createElement("p"),r.innerHTML=this.getText("message"),t.appendChild(r),this.showTotalWin&&(i=document.createElement("div"),s=document.createElement("p"),s.innerHTML=""+this.getText("totalWin")+"",o=document.createElement("input"),o.className="endscreen-total form-control",o.setAttribute("disabled","true"),s.appendChild(o),i.appendChild(s),t.appendChild(i),this.totalWinInputElement=o),this.showExitCode&&(u=document.createElement("div"),u.className="input-group",a=document.createElement("span"),a.innerHTML=""+this.getText("exitCode")+"",f=document.createElement("input"),f.id="exit_code",f.className="endscreen-exit-code form-control",f.setAttribute("disabled","true"),c=document.createElement("span"),c.className="input-group-btn",l=document.createElement("button"),l.className="btn btn-outline-secondary endscreen-copy-btn",l.innerHTML=this.getText("copyButton"),l.type="button",l.onclick=function(){p.copy(f.value)},c.appendChild(l),t.appendChild(a),u.appendChild(c),u.appendChild(f),t.appendChild(u),this.exitCodeInputElement=f),h=e.game.settings.BASE_PAY,"undefined"!=typeof h&&this.updateDisplay({basePay:h,total:h,exitCode:"N/A"}),this.showEmailForm&&e.widgets.append(this.emailForm,t,{title:!1,panel:!1}),this.showFeedbackForm&&e.widgets.append(this.feedback,t,{title:!1,panel:!1}),t},t.prototype.listeners=function(){var t;t=this,e.on.data("WIN",function(e){t.updateDisplay(e.data)})},t.prototype.copy=function(e){var t=document.createElement("input");try{document.body.appendChild(t),t.value=e,t.select(),document.execCommand("copy",!1),t.remove(),alert(this.getText("exitCopyMsg"))}catch(n){alert(this.getText("exitCopyError"))}},t.prototype.updateDisplay=function(t){var r,i,s,o,u,a,f,l,c,h;if(this.totalWinCb)i=this.totalWinCb(t,this);else{if("undefined"==typeof t.total&&"undefined"==typeof t.totalRaw)throw new Error("EndScreen.updateDisplay: data.total and data.totalRaw cannot be both undefined.");"undefined"!=typeof t.total&&(i=J.isNumber(t.total),i===!1&&(e.err("EndScreen.updateDisplay: invalid data.total: "+t.total),i=this.getText("errTotalWin"),l=!0)),r="","undefined"!=typeof t.basePay&&(r=n(t.basePay,this.maxDec)),"undefined"!=typeof t.bonus&&t.showBonus!==!1&&(r!==""&&(r+=" + "),r+=n(t.bonus,this.maxDec));if(t.partials)if(!J.isArray(t.partials))e.err("EndScreen error, partials must be array. Found: "+t.partials);else{h=t.partials.length;for(c=0;c= 0 or undefined. Found: "+e.maxChars);this.maxChars=t}if("undefined"==typeof e.minChars)this.minChars=0;else{t=J.isInt(e.minChars,0,undefined,!0);if(t===!1)throw new TypeError("Feedback constructor: minChars must be an integer >= 0 or undefined. Found: "+e.minChars);if(this.maxChars&&t>this.maxChars)throw new TypeError("Feedback constructor: minChars cannot be greater than maxChars. Found: "+t+" > "+this.maxChars);this.minChars=t}if("undefined"==typeof e.maxWords)this.maxWords=0;else{t=J.isInt(e.maxWords,0,undefined,!0);if(t===!1)throw new TypeError("Feedback constructor: maxWords must be an integer >= 0 or undefined. Found: "+e.maxWords);this.maxWords=e.maxWords}if("undefined"==typeof e.minWords)this.minWords=0;else{t=J.isInt(e.minWords,0,undefined,!0);if(t===!1)throw new TypeError("Feedback constructor: minWords must be an integer >= 0 or undefined. Found: "+e.minWords);this.minWords=e.minWords;if(this.maxChars){t=(this.maxChars+1)/2;if(this.minWords>t)throw new TypeError("Feedback constructor: minWords cannot be larger than (maxChars+1)/2. Found: "+this.minWords+" > "+t)}}if(this.maxWords){if(this.maxChars&&this.maxChars "+this.maxWords);if(this.minChars>this.maxWords)throw new TypeError("Feedback constructor: minChars cannot be greater than maxWords. Found: "+this.minChars+" > "+this.maxWords)}if(this.minWords||this.minChars||this.maxWords||this.maxChars)this.required=!0;if("undefined"==typeof e.rows)this.rows=3;else{if(J.isInt(e.rows,0)===!1)throw new TypeError("Feedback constructor: rows must be an integer > 0 or undefined. Found: "+e.rows);this.rows=e.rows}if("undefined"==typeof e.maxAttemptLength)this.maxAttemptLength=0;else{t=J.isNumber(e.maxAttemptLength,0);if(t===!1)throw new TypeError("Feedback constructor: options.maxAttemptLength must be a number > 0 or undefined. Found: "+e.maxAttemptLength);this.maxAttemptLength=t}this.showSubmit="undefined"==typeof e.showSubmit?!0:!!e.showSubmit;if(!e.onsubmit)this.onsubmit={feedbackOnly:!0,send:!0,updateUI:!0};else{if("object"!=typeof e.onsubmit)throw new TypeError("Feedback constructor: onsubmit must be string or object. Found: "+e.onsubmit);this.onsubmit=e.onsubmit}this._feedback=e.feedback||null,this.attempts=[],this.timeInputBegin=null,this.feedbackForm=null,this.textareaElement=null,this.charCounter=null,this.wordCounter=null,this.submitButton=null,this.setMsg=!!e.setMsg||!1}function s(){var e;return e=this.textareaElement?this.textareaElement.value:this._feedback,e?e.trim():e}e.widgets.register("Feedback",i),i.version="1.6.0",i.description="Displays a configurable feedback form",i.className="feedback",i.texts={autoHint:function(e){var t,n;return e.minChars&&e.maxChars?t="between "+e.minChars+" and "+e.maxChars+" characters":e.minChars?(t="at least "+e.minChars+" character",e.minChars>1&&(t+="s")):e.maxChars&&(t="at most "+e.maxChars+" character",e.maxChars>1&&(t+="s")),e.minWords&&e.maxWords?n="beetween "+e.minWords+" and "+e.maxWords+" words":e.minWords?(n="at least "+e.minWords+" word",e.minWords>1&&(n+="s")):e.maxWords&&(n="at most "+e.maxWords+" word",e.maxWords>1&&(n+="s")),t?(t="("+t,n&&(t+=", and "+n),t+")"):n?"("+n+")":!1},submit:"Submit feedback",label:"Any feedback? Let us know here:",sent:"Sent!",counter:function(e,t){var n;return n=t.chars?" character":" word",t.len!==1&&(n+="s"),t.needed?n+=" needed":t.over?n+=" over":t.justcount||(n+=" remaining"),n}};var t,n,r;t="#a32020",n="#a32020",r="#78b360",i.prototype.init=function(e){if("string"==typeof e.mainText)this.mainText=e.mainText;else{if("undefined"!=typeof e.mainText)throw new TypeError("Feedback.init: options.mainText must be string or undefined. Found: "+e.mainText);this.mainText=this.getText("label")}if("string"==typeof e.hint||!1===e.hint)this.hint=e.hint;else{if("undefined"!=typeof e.hint)throw new TypeError("Feedback.init: options.hint must be a string, false, or undefined. Found: "+e.hint);this.hint=this.getText("autoHint")}},i.prototype.verifyFeedback=function(e,i){var o,u,a,f,l,c,h,p,d,v,m;return o=s.call(this),u=o?o.length:0,f=this.submitButton,l=this.charCounter,c=this.wordCounter,a=!0,uthis.maxChars?(a=!1,h=u-this.maxChars,p=h+this.getText("counter",{chars:!0,over:!0,len:h}),d=n):(h=this.maxChars?this.maxChars-u:u,p=h+this.getText("counter",{chars:!0,len:h,justcount:!this.maxChars}),d=r),c&&(h=o?o.match(/\b[-?(\w+)?]+\b/gi):0,u=h?h.length:0,uthis.maxWords?(a=!1,h=u-this.maxWords,v=h+this.getText("counter",{over:!0,len:h}),m=n):(h=this.maxWords?this.maxWords-u:u,v=h+this.getText("counter",{len:h,justcount:!this.maxWords}),m=r)),i&&(f&&(f.disabled=!a),l&&(l.style.backgroundColor=d,l.innerHTML=p),c&&(c.style.backgroundColor=m,c.innerHTML=v)),!a&&("undefined"==typeof e||e)&&(this.maxAttemptLength&&u>this.maxAttemptLength&&(o=o.substr(0,this.maxAttemptLength)),this.attempts.push(o)),a},i.prototype.isChoiceDone=function(){return this.verifyFeedback()},i.prototype.append=function(){var e;e=this,this.feedbackForm=W.append("form",this.bodyDiv,{className:"feedback-form"}),this.mainText&&(this.spanMainText=W.append("span",this.feedbackForm,{className:"feedback-maintext",innerHTML:this.mainText})),this.hint&&W.append("span",this.spanMainText||this.feedbackForm,{className:"feedback-hint",innerHTML:this.hint}),this.textareaElement=W.append("textarea",this.feedbackForm,{className:"form-control feedback-textarea",type:"text",rows:this.rows}),this.showSubmit&&(this.submitButton=W.append("input",this.feedbackForm,{className:"btn btn-lg btn-primary",type:"submit",value:this.getText("submit")}),J.addEvent(this.feedbackForm,"submit",function(t){t.preventDefault(),e.getValues(e.onsubmit)})),this.showCounters(),J.addEvent(this.feedbackForm,"input",function(){e.isHighlighted()&&e.unhighlight(),e.verifyFeedback(!1,!0)}),J.addEvent(this.feedbackForm,"click",function(){e.isHighlighted()&&e.unhighlight()}),this.verifyFeedback(!1,!0)},i.prototype.setValues=function(e){var t,n,r,i,s;e=e||{};if(!e.feedback){r=this.minChars||0,this.maxChars?n=this.maxChars:this.maxWords?n=this.maxWords*4:r?n=r+80:n=80,t=J.randomString(J.randomInt(r,n),"aA_1");if(this.minWords){i=this.minWords-t.split(" ").length;if(i>0)for(s=0;s")),e.verify!==!1&&(n=this.verifyFeedback(e.markAttempt,e.updateUI)),n===!1&&(e.updateUI||e.highlight)&&this.highlight(),e.feedbackOnly||(t={timeBegin:this.timeInputBegin,feedback:t,attempts:this.attempts,valid:n},e.markAttempt&&(t.isCorrect=n)),t!==""&&(e.send&&n||e.sendAnyway)&&(this.sendValues({values:t}),e.updateUI&&(this.submitButton.setAttribute("value",this.getText("sent")),this.submitButton.disabled=!0,this.textareaElement.disabled=!0)),e.reset&&this.reset(),t},i.prototype.sendValues=function(t){var n;return t=t||{feedbackOnly:!0},n=t.values||this.getValues(t),this.setMsg?("string"==typeof n&&(n={feedback:n}),e.set(n,t.to||"SERVER")):e.say("feedback",t.to||"SERVER",n),n},i.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("Feedback.highlight: border must be string or undefined. Found: "+e);if(!this.isAppended()||this.highlighted===!0)return;this.textareaElement.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},i.prototype.unhighlight=function(){if(!this.isAppended()||this.highlighted!==!0)return;this.textareaElement.style.border="",this.highlighted=!1,this.emit("unhighlighted")},i.prototype.reset=function(){this.attempts=[],this.timeInputBegin=null,this._feedback=null,this.textareaElement&&(this.textareaElement.value=""),this.isHighlighted()&&this.unhighlight()},i.prototype.disable=function(){if(!this.textareaElement||this.textareaElement.disabled)return;this.disabled=!0,this.submitElement&&(this.submitElement.disabled=!0),this.textareaElement.disabled=!0,this.emit("disabled")},i.prototype.enable=function(){if(!this.textareaElement||!this.textareaElement.disabled)return;this.disabled=!1,this.submitElement&&(this.submitElement.disabled=!1),this.textareaElement.disabled=!1,this.emit("enabled")},i.prototype.showCounters=function(){if(!this.charCounter){if(this.minChars||this.maxChars)this.charCounter=W.append("span",this.feedbackForm,{className:"feedback-char-count badge",innerHTML:this.maxChars})}else this.charCounter.style.display="";if(!this.wordCounter){if(this.minWords||this.maxWords)this.wordCounter=W.append("span",this.feedbackForm,{className:"feedback-char-count badge",innerHTML:this.maxWords}),this.charCounter&&(this.wordCounter.style["margin-left"]="10px")}else this.wordCounter.style.display=""},i.prototype.hideCounters=function(){this.charCounter&&(this.charCounter.style.display="none"),this.wordCounter&&(this.wordCounter.style.display="none")}}(node),function(e){"use strict";function t(e){this.dropdown}function n(t){var n,r,i,s,o,u,a;i=[],t=t||e.game.plot.stager.sequence;for(n=0;nYou can work quickly, your first feeling is generally best."},i.prototype.init=function(e){e=e||{};if(e.choices){if(!J.isArray(e.choices)||e.choices.length<2)throw new Error("GroupMalleability.init: choices must be an array of length > 1 or undefined. Found: "+e.choices);this.choices=e.choices}if(e.header){if(!J.isArray(e.header)||e.header.length!==this.choices.length)throw new Error("GroupMalleability.init: header must be an array of length equal to the number of choices or undefined. Found: "+e.header);this.header=e.header}if(e.mainText){if("string"!=typeof e.mainText&&e.mainText!==!1)throw new Error("GroupMalleability.init: mainText must be string, false, or undefined. Found: "+e.mainText);this.mainText=e.mainText}else e.mainText!==!1&&(this.mainText=this.getText("mainText"));this.requiredMark=e.requiredMark,this.displayRequired=e.displayRequired},i.prototype.append=function(){this.ctg=e.widgets.add("ChoiceTableGroup",this.panelDiv,{id:this.id||"groupmalleability_choicetable",items:t.map(function(e,t){return["GM_"+(t+1),e]}),choices:this.choices,mainText:this.mainText,title:!1,panel:!1,requiredChoice:this.required,header:this.header,displayRequired:this.displayRequired,requiredMark:this.requiredMark})},i.prototype.getValues=function(e){return e=e||{},this.ctg.getValues(e)},i.prototype.setValues=function(e){return e=e||{},this.ctg.setValues(e)},i.prototype.enable=function(e){return this.ctg.enable(e)},i.prototype.disable=function(e){return this.ctg.disable(e)},i.prototype.highlight=function(e){return this.ctg.highlight(e)},i.prototype.unhighlight=function(e){return this.ctg.unhighlight(e)}}(node),function(e){"use strict";function t(t){var n=this;this.options=t,this.availableLanguages={en:{name:"English",nativeName:"English",shortName:"en"}},this.currentLanguage=null,this.buttonListLength=null,this.displayForm=null,this.optionsLabel={},this.optionsDisplay={},this.loadingDiv=null,this.languagesLoaded=!1,this.usingButtons=!0,this.updatePlayer="ondone",this.setUriPrefix=!0,this.notifyServer=!0,this.onLangCallback=function(t){function u(e){return function(){n.setLanguage(e,n.updatePlayer==="onselect")}}var r,i,s,o;while(n.displayForm.firstChild)n.displayForm.removeChild(n.displayForm.firstChild);o=0,n.availableLanguages=t.data;if(n.usingButtons)for(r in t.data)t.data.hasOwnProperty(r)&&(i=W.get("label",{id:r+"Label","for":r+"RadioButton"}),s=W.get("input",{id:r+"RadioButton",type:"radio",name:"languageButton",value:t.data[r].name}),s.onclick=u(r),i.appendChild(s),i.appendChild(document.createTextNode(t.data[r].nativeName)),++o!==1&&W.add("br",n.displayForm),i.className="unselected",n.displayForm.appendChild(i),n.optionsLabel[r]=i,n.optionsDisplay[r]=s);else{n.displaySelection=W.get("select","selectLanguage");for(r in t.data)i=document.createTextNode(t.data[r].nativeName),s=W.get("option",{id:r+"Option",value:r}),s.appendChild(i),n.displaySelection.appendChild(s),n.optionsLabel[r]=i,n.optionsDisplay[r]=s;n.displayForm.appendChild(n.displaySelection),n.displayForm.onchange=function(){n.setLanguage(n.displaySelection.value,n.updatePlayer==="onselect")}}n.loadingDiv.style.display="none",n.languagesLoaded=!0,n.setLanguage(e.player.lang.shortName||"en",!1),n.onLangCallbackExtension&&(n.onLangCallbackExtension(t),n.onLangCallbackExtension=null)},this.onLangCallbackExtension=null}e.widgets.register("LanguageSelector",t),t.version="0.6.3",t.description="Display information about the current language and allows users to change it.",t.title="Select Language",t.className="languageselector",t.texts.loading="Loading...",t.prototype.init=function(t){J.mixout(t,this.options),this.options=t,"undefined"!=typeof this.options.usingButtons&&(this.usingButtons=!!this.options.usingButtons);if("undefined"!=typeof this.options.notifyServer)if(!1===this.options.notifyServer)this.options.notifyServer="never";else{if("string"!=typeof this.options.notifyServer)throw new Error("LanguageSelector.init: options.notifyServer must be "+this.options.notifyServer);if("never"!==this.options.notifyServer&&"onselect"!==this.options.notifyServer&&"ondone"!==this.options.notifyServer)throw new Error('LanguageSelector.init: invalid value for notifyServer: "'+this.options.notifyServer+'". Valid '+'values: "never","onselect", "ondone".');this.notifyServer=this.options.notifyServer}"undefined"!=typeof this.options.setUriPrefix&&(this.setUriPrefix=!!this.options.setUriPrefix),e.on.lang(this.onLangCallback),this.displayForm=W.get("form","radioButtonForm"),this.loadingDiv=W.add("div",this.displayForm),this.loadingDiv.innerHTML=this.getText("loading"),this.loadLanguages()},t.prototype.append=function(){this.bodyDiv.appendChild(this.displayForm)},t.prototype.setLanguage=function(t,n){this.usingButtons&&this.currentLanguage!==null&&this.currentLanguage!==this.availableLanguages[t]&&(this.optionsDisplay[this.currentLanguage].checked="unchecked",this.optionsLabel[this.currentLanguage].className="unselected"),this.currentLanguage=t,this.usingButtons?(this.optionsDisplay[this.currentLanguage].checked="checked",this.optionsLabel[this.currentLanguage].className="selected"):this.displaySelection.value=this.currentLanguage,n!==!1&&e.setLanguage(this.availableLanguages[this.currentLanguage],this.setUriPrefix,this.notifyServer)},t.prototype.updateAvalaibleLanguages=function(t){t&&t.callback&&(this.onLangCallbackExtension=t.callback),e.socket.send(e.msg.create({target:"LANG",to:"SERVER",action:"get"}))},t.prototype.loadLanguages=function(e){this.languagesLoaded?e&&e.callback&&e.callback():this.updateAvalaibleLanguages(e)},t.prototype.listeners=function(){var t;t=this,e.events.step.on("REALLY_DONE",function(){t.updatePlayer==="ondone"&&e.setLanguage(t.availableLanguages[t.currentLanguage],t.setUriPrefix,t.notifyServer)})}}(node),function(e){"use strict";function t(){this.spanCurrency=null,this.spanMoney=null,this.currency="ECU",this.money=0,this.precision=2,this.showCurrency=!0,this.classnameCurrency="moneytalkscurrency",this.classnameMoney="moneytalksmoney"}e.widgets.register("MoneyTalks",t),t.version="0.5.0",t.description="Displays the earnings of a player.",t.title="Earnings",t.className="moneytalks",t.prototype.init=function(e){e=e||{},"string"==typeof e.currency&&(this.currency=e.currency),"undefined"!=typeof e.showCurrency&&(this.showCurrency=!!e.showCurrency),"number"==typeof e.money&&(this.money=e.money),"number"==typeof e.precision&&(this.precision=e.precision),"string"==typeof e.MoneyClassName&&(this.classnameMoney=e.MoneyClassName),"string"==typeof e.currencyClassName&&(this.classnameCurrency=e.currencyClassName)},t.prototype.append=function(){this.spanMoney||(this.spanMoney=document.createElement("span")),this.spanCurrency||(this.spanCurrency=document.createElement("span")),this.showCurrency||(this.spanCurrency.style.display="none"),this.spanMoney.className=this.classnameMoney,this.spanCurrency.className=this.classnameCurrency,this.spanCurrency.innerHTML=this.currency,this.spanMoney.innerHTML=this.money,this.bodyDiv.appendChild(this.spanMoney),this.bodyDiv.appendChild(this.spanCurrency)},t.prototype.listeners=function(){var t=this;e.on("MONEYTALKS",function(e,n){t.update(e,n)})},t.prototype.update=function(t,n){var r;r=J.isNumber(t);if(r===!1){e.err("MoneyTalks.update: invalid amount: "+t);return}return n&&(this.money=0),this.money+=r,this.spanMoney.innerHTML=this.money.toFixed(this.precision),this.money},t.prototype.getValues=function(){return this.money}}(node),function(e){"use strict";function t(e){this.methods={},this.method="I-PANAS-SF",this.mainText=null,this.gauge=null,this.addMethod("I-PANAS-SF",r)}function n(e,t){if(!t)throw new Error("MoodGauge.init: method "+e+"did not create element gauge.");if("function"!=typeof t.getValues)throw new Error("MoodGauge.init: method "+e+": gauge missing function getValues.");if("function"!=typeof t.enable)throw new Error("MoodGauge.init: method "+e+": gauge missing function enable.");if("function"!=typeof t.disable)throw new Error("MoodGauge.init: method "+e+": gauge missing function disable.");if("function"!=typeof t.append)throw new Error("MoodGauge.init: method "+e+": gauge missing function append.")}function r(t){var n,r,i,s,o,u,a,f,l;i=t.choices||["1","2","3","4","5"],r=t.emotions||["Upset","Hostile","Alert","Ashamed","Inspired","Nervous","Determined","Attentive","Afraid","Active"],l=r.length,s=t.left||"never",o=t.right||"always",n=new Array(l),f=-1;for(;++f'+r[f]+": "+s,n[f]={id:r[f],left:u,right:o,sameCellWidth:"200px"};return a=e.widgets.get("ChoiceTableGroup",{id:t.id||"ipnassf",items:n,mainText:this.mainText||this.getText("mainText"),requiredChoice:!0,storeRef:!1,header:t.header,choices:i}),a}e.widgets.register("MoodGauge",t),t.version="0.5.0",t.description="Displays an interface to measure mood and emotions.",t.className="moodgauge",t.texts.mainText="Thinking about yourself and how you normally feel, to what extent do you generally feel: ",t.prototype.init=function(e){var t;if("undefined"!=typeof e.method){if("string"!=typeof e.method)throw new TypeError("MoodGauge.init: method must be string or undefined: "+e.method);if(!this.methods[e.method])throw new Error("MoodGauge.init: method is invalid: "+e.method);this.method=e.method}if(e.mainText){if("string"!=typeof e.mainText)throw new TypeError("MoodGauge.init: mainText must be string or undefined. Found: "+e.mainText);this.mainText=e.mainText}t=this.methods[this.method].call(this,e),n(this.method,t),this.gauge=t,this.on("enabled",function(){t.enable()}),this.on("disabled",function(){t.disable()}),this.on("highlighted",function(){t.highlight()}),this.on("unhighlighted",function(){t.unhighlight()})},t.prototype.append=function(){e.widgets.append(this.gauge,this.bodyDiv,{panel:!1})},t.prototype.addMethod=function(e,t){if("string"!=typeof e)throw new Error("MoodGauge.addMethod: name must be string: "+e);if("function"!=typeof t)throw new Error("MoodGauge.addMethod: cb must be function: "+t);if(this.methods[e])throw new Error("MoodGauge.addMethod: name already existing: "+e);this.methods[e]=t},t.prototype.getValues=function(e){return this.gauge.getValues(e)},t.prototype.setValues=function(e){return this.gauge.setValues(e)}}(node),function(e){"use strict";function t(e){function t(e){var t,n,i,s;return t="/images/"+(e.content.success?"success-icon.png":"delete-icon.png"),n=document.createElement("img"),n.src=t,"object"==typeof e.content.text&&(e.content.text=r(e.content.text)),s=document.createTextNode(e.content.text),i=document.createElement("span"),i.className="requirement",i.appendChild(n),i.appendChild(s),i}this.requirements=[],this.stillChecking=0,this.withTimeout=e.withTimeout||!0,this.timeoutTime=e.timeoutTime||1e4,this.timeoutId=null,this.summary=null,this.summaryUpdate=null,this.summaryResults=null,this.dots=null,this.hasFailed=!1,this.results=[],this.completed={},this.sayResults=e.sayResults||!1,this.sayResultsLabel=e.sayResultLabel||"requirements",this.addToResults=e.addToResults||null,this.onComplete=null,this.onSuccess=null,this.onFailure=null,this.callbacksExecuted=!1,this.list=new W.List({render:{pipeline:t,returnAt:"first"}})}function n(e,t,n){var r,i,s;i=function(n,r,i){if(e.completed[t])throw new Error("Requirements.checkRequirements: test already completed: "+t);e.completed[t]=!0,e.updateStillChecking(-1),n||(e.hasFailed=!0),"string"==typeof r&&(r=[r]);if(r){if(!J.isArray(r))throw new Error("Requirements.checkRequirements: errors must be array or undefined. Found: "+r);e.displayResults(r)}e.results.push({name:t,success:n,errors:r,data:i}),e.isCheckingFinished()&&e.checkingFinished()},r=e.requirements[n];if("function"==typeof r)s=r(i);else{if("object"!=typeof r)throw new TypeError("Requirements.checkRequirements: invalid requirement: "+t+".");s=r.cb(i,r.params||{})}s&&i(s.success,s.errors,s.data)}function r(e){var t;return e.msg?t=e.msg:e.message?t=e.message:e.description?t=t.description:t=e.toString(),t}e.widgets.register("Requirements",t),t.version="0.7.2",t.description="Checks a set of requirements and display the results",t.title="Requirements",t.className="requirements",t.texts.errStr="One or more function is taking too long. This is likely to be due to a compatibility issue with your browser or to bad network connectivity.",t.texts.testPassed="All tests passed.",t.dependencies={List:{}},t.prototype.init=function(e){if("object"!=typeof e)throw new TypeError("Requirements.init: conf must be object. Found: "+e);if(e.requirements){if(!J.isArray(e.requirements))throw new TypeError("Requirements.init: conf.requirements must be array or undefined. Found: "+e.requirements);this.requirements=e.requirements}if("undefined"!=typeof e.onComplete){if(null!==e.onComplete&&"function"!=typeof e.onComplete)throw new TypeError("Requirements.init: conf.onComplete must be function, null or undefined. Found: "+e.onComplete);this.onComplete=e.onComplete}if("undefined"!=typeof e.onSuccess){if(null!==e.onSuccess&&"function"!=typeof e.onSuccess)throw new TypeError("Requirements.init: conf.onSuccess must be function, null or undefined. Found: "+e.onSuccess);this.onSuccess=e.onSuccess}if("undefined"!=typeof e.onFailure){if(null!==e.onFailure&&"function"!=typeof e.onFailure)throw new TypeError("Requirements.init: conf.onFailure must be function, null or undefined. Found: "+e.onFailure);this.onFailure=e.onFailure}if(e.maxExecTime){if(null!==e.maxExecTime&&"number"!=typeof e.maxExecTime)throw new TypeError("Requirements.init: conf.onMaxExecTime must be number, null or undefined. Found: "+e.maxExecTime);this.withTimeout=!!e.maxExecTime,this.timeoutTime=e.maxExecTime}},t.prototype.addRequirements=function(){var e,t;e=-1,t=arguments.length;for(;++e0&&e.displayResults([e.getText("errStr")]),e.timeoutId=null,e.hasFailed=!0,e.checkingFinished()},this.timeoutTime)},t.prototype.clearTimeout=function(){this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)},t.prototype.updateStillChecking=function(e,t){var n,r;this.stillChecking=t?e:this.stillChecking+e,n=this.requirements.length,r=n-this.stillChecking,this.summaryUpdate.innerHTML=" ("+r+" / "+n+")"},t.prototype.isCheckingFinished=function(){return this.stillChecking<=0},t.prototype.checkingFinished=function(t){var n;if(this.callbacksExecuted&&!t)return;this.callbacksExecuted=!0,this.timeoutId&&clearTimeout(this.timeoutId),this.dots.stop(),this.sayResults&&(n={success:!this.hasFailed,results:this.results},this.addToResults&&J.mixin(n,this.addToResults()),e.say(this.sayResultsLabel,"SERVER",n)),this.onComplete&&this.onComplete(),this.hasFailed?this.onFailure&&this.onFailure():this.onSuccess&&this.onSuccess()},t.prototype.displayResults=function(e){var t,n;if(!this.list)throw new Error("Requirements.displayResults: list not found. Have you called .append() first?");if(!J.isArray(e))throw new TypeError("Requirements.displayResults: results must be array. Found: "+e);if(!this.hasFailed&&this.stillChecking<=0)this.list.addDT({success:!0,text:this.getText("testPassed")});else{t=-1,n=e.length;for(;++tand',a=e+s,a+=i.currencyAfter?t+o:o+t,a+=u+n+s,a+(i.currencyAfter?r+o:o+r)}function r(t){var r,i,s,o,u,a,f,l,c,h,p,d;a=t.values||[2,1.6,3.85,.1],t.scale&&(a=a.map(function(e){return e*t.scale})),f=a[0].toFixed(2),l=a[1].toFixed(2),c=a[2].toFixed(2),h=a[3].toFixed(2),o=10,r=new Array(o);for(s=0;s 0 or undefined. Found: "+t.boxValue)}else this.boxValue=.01;this.currency=t.currency||"USD",this.revealProbBomb="undefined"==typeof t.revealProbBomb?!0:!!t.revealProbBomb;if("undefined"!=typeof t.totBoxes){if(!J.isInt(t.totBoxes,0,1e4,!1,!0))throw new TypeError("Bomb.init: maxBoxes must be an integer > 0 and <= 10000 or undefined. Found: "+t.totBoxes);this.totBoxes=t.totBoxes}else this.totBoxes=100;if("undefined"!=typeof t.maxBoxes){if(!J.isInt(t.maxBoxes,0,this.totBoxes))throw new TypeError("Bomb.init: maxBoxes must be a positive integer <= "+this.totBoxes+" or undefined. Found: "+t.maxBoxes);this.maxBoxes=t.maxBoxes}else this.maxBoxes=r===1?this.totBoxes-1:this.totBoxes;if("undefined"!=typeof t.boxesInRow){if(!J.isInt(t.boxesInRow,0))throw new TypeError("Bomb.init: boxesInRow must be a positive integer or undefined. Found: "+t.boxesInRow);this.boxesInRow=t.boxesInRow>this.totBoxes?this.totBoxes:t.boxesInRow}else this.boxesInRow=this.totBoxes<10?this.totBoxes:10;this.withPrize="undefined"==typeof t.withPrize?!0:!!t.withPrize,this.onopen=null;if(t.onopen){if("function"!=typeof t.onopen)throw new TypeError("Bomb: onopen must be function or undefined. Found: "+t.onopen);this.onopen=t.onopen}return i=Math.random()>=r?-1:Math.ceil(Math.random()*this.totBoxes),{setValues:function(e){f.setValues(e)},getValues:function(e){var t,r,i,s;return e=e||{},r=f.getValues(),"undefined"!=typeof h?(i=h,s=!0):(i=parseInt(f.slider.value,10),s=!1),t={value:i,isCorrect:s,totalMove:r.totalMove,isWinner:c,time:r.time,reward:0},!t.isCorrect&&("undefined"==typeof e.highlight||e.highlight)&&f.highlight(),c===!0&&(t.reward=h*n.boxValue),t},highlight:function(){f.highlight()},unhighlight:function(){f.unhighlight()},append:function(){var t;W.add("div",n.bodyDiv,{innerHTML:n.mainText||n.getText("bomb_mainText",r),className:"bomb-maintext"}),f=e.widgets.add("Slider",n.bodyDiv,{min:0,max:n.maxBoxes,hint:n.getText("bomb_sliderHint"),title:!1,initialValue:0,displayValue:!1,displayNoChange:!1,displayRequired:n.displayRequired,requiredMark:n.requiredMark,type:"flat",required:!0,panel:!1,onmove:function(e){var t,r,i,o;n._unhighlight(),e>0?(l.style.display="",l.disabled=!1,a.innerHTML=""):(l.style.display="none",a.innerHTML=n.getText("bomb_warn"),l.disabled=!0);for(t=0;tt?r.style.background="#1be139":r.style.background="#000000";W.gid("bomb_numBoxes").innerText=e,n.withPrize&&(i=n.currency,o=n.boxValue,W.gid("bomb_boxValue").innerText=o+i,W.gid("bomb_totalWin").innerText=Number(e*o).toFixed(2)+i)},storeRef:!1,width:"100%"}),t=Math.ceil(n.totBoxes/n.boxesInRow),W.add("div",n.bodyDiv,{innerHTML:u(t,n.boxesInRow,n.totBoxes)}),o=W.add("div",n.bodyDiv,{className:"risk-info"}),W.add("p",o,{innerHTML:n.getText("bomb_numBoxes")+' 0'}),n.withPrize&&(W.add("p",o,{innerHTML:n.getText("bomb_boxValue")+' '+n.boxValue+""}),W.add("p",o,{innerHTML:n.getText("bomb_totalWin")+' 0'})),a=W.add("p",o,{id:"bomb_result"}),l=W.add("button",n.bodyDiv,{className:"btn btn-lg btn-danger",innerHTML:n.getText("bomb_openButton")}),l.style.display="none",l.onclick=function(){var t;h=parseInt(f.slider.value,10),i>-1?(W.gid(s(i-1)).style.background="#fa0404",c=hn){i=i+'';break}i=i+'
'}return i+="",i}function u(e,t,n){var r,i,s,u;i='';for(r=0;rn&&(u=n-r*t-1),i+=o(r,t,u);return i+="

",i}e.widgets.register("RiskGauge",t),t.version="0.9.0",t.description="Displays an interface to measure risk preferences with different methods.",t.className="riskgauge",t.texts={holt_laury_mainText:"Below you find a series of hypothetical lotteries, each contains two lotteries with different probabalities of winning. In each row, select the lottery you would rather take part in.",bomb_mainText:function(e,t){var n;return n='

',n+="Below there are "+e.totBoxes+" black boxes. ",n+="Every box contains a prize of "+e.boxValue+" "+e.currency+", but ",t===1?n+="one random box contains a bomb.":e.revealProbBomb?n+="with probability "+t+" one random box contains a bomb.":n+="one random box might contain a bomb.",n+=" You must decide how many boxes you want to open.",n+="

",e.withPrize&&(n+='

',n+="You will receive a reward equal to the sum of all the prizes in every opened box. However, if you open the box with the bomb, you get nothing.

"),n+='

',n+="How many boxes do you want to open ",n+="between 1 and "+e.maxBoxes+"?

",n},bomb_sliderHint:'Move the slider to choose the number of boxes to open, then click "Open Boxes"',bomb_boxValue:"Prize per box: ",bomb_numBoxes:"Number of boxes: ",bomb_totalWin:"Total reward: ",bomb_openButton:"Open Boxes",bomb_warn:"Open at least one box.",bomb_won:"You won! You did not open the box with the bomb.",bomb_lost:"You lost! You opened the box with the bomb."},t.texts.mainText=t.texts.holt_laury_mainText,t.prototype.init=function(t){var n,r;if("undefined"!=typeof t.method){if("string"!=typeof t.method)throw new TypeError("RiskGauge.init: method must be string or undefined: "+t.method);if(!this.methods[t.method])throw new Error("RiskGauge.init: method is invalid: "+t.method);this.method=t.method}if(t.mainText){if("string"!=typeof t.mainText)throw new TypeError("RiskGauge.init: mainText must be string or undefined. Found: "+t.mainText);this.mainText=t.mainText}n=this.methods[this.method].call(this,t),r=this,n.isHidden=function(){return r.isHidden()},n.isCollapsed=function(){return r.isCollapsed()};if(!e.widgets.isWidget(n))throw new Error("RiskGauge.init: method "+this.method+" created invalid gauge: missing default widget "+"methods.");this.gauge=n,this.on("enabled",function(){n.enable&&n.enable()}),this.on("disabled",function(){n.disable&&n.disable()}),this.on("highlighted",function(){n.highlight&&n.highlight()}),this.on("unhighlighted",function(){n.unhighlight&&n.unhighlight()}),this.displayRequired=t.displayRequired,this.requiredMark=t.requiredMark},t.prototype.append=function(){e.widgets.append(this.gauge,this.bodyDiv,{panel:!1})},t.prototype.addMethod=function(e,t){if("string"!=typeof e)throw new TypeError("RiskGauge.addMethod: name must be string: "+e);if("function"!=typeof t)throw new TypeError("RiskGauge.addMethod: cb must be function: "+t);if(this.methods[e])throw new Error("RiskGauge.addMethod: name already existing: "+e);this.methods[e]=t},t.prototype.getValues=function(e){return this.gauge.getValues(e)},t.prototype.setValues=function(e){return this.gauge.setValues(e)}}(node),function(e){"use strict";function i(){this.sdo=null,this.scale="SDO7s",this.choices=n,this.header=r,this.mainText=null}e.widgets.register("SDO",i),i.version="0.4.0",i.description="Displays an interface to measure Social Dominance Orientation (S.D.O.).",i.className="SDO";var t={SDO7:["Some groups of people must be kept in their place.","It's probably a good thing that certain groups are at the top and other groups are at the bottom.","An ideal society requires some groups to be on top and others to be on the bottom.","Some groups of people are simply inferior to other groups.","Groups at the bottom are just as deserving as groups at the top.","No one group should dominate in society.","Groups at the bottom should not have to stay in their place.","Group dominance is a poor principle.","We should not push for group equality.","We shouldn't try to guarantee that every group has the same quality of life.","It is unjust to try to make groups equal.","Group equality should not be our primary goal.","We should work to give all groups an equal chance to succeed.","We should do what we can to equalize conditions for different groups.","No matter how much effort it takes, we ought to strive to ensure that all groups have the same chance in life.","Group equality should be our ideal."],SDO16:["Some groups of people are simply inferior to other groups.","In getting what you want, it is sometimes necessary to use force against other groups.","It's OK if some groups have more of a chance in life than others.","To get ahead in life, it is sometimes necessary to step on other groups.","If certain groups stayed in their place, we would have fewer problems.","It's probably a good thing that certain groups are at the top and other groups are at the bottom.","Inferior groups should stay in their place.","Sometimes other groups must be kept in their place.","It would be good if groups could be equal.","Group equality should be our ideal.","All groups should be given an equal chance in life.","We should do what we can to equalize conditions for different groups.","Increased social equality is beneficial to society.","We would have fewer problems if we treated people more equally.","We should strive to make incomes as equal as possible.","No group should dominate in society."]};t.SDO7s=[t.SDO7[2],t.SDO7[3],t.SDO7[5],t.SDO7[6],t.SDO7[11],t.SDO7[10],t.SDO7[13],t.SDO7[12]];var n=[1,2,3,4,5,6,7],r=["Strongly Oppose","Somewhat Oppose","Slightly Oppose","Neutral","Slightly Favor","Somewhat Favor","Strongly Favor"];i.texts={mainText:"Show how much you favor or oppose each idea below by selecting a number from 1 to 7 on the scale below. You can work quickly, your first feeling is generally best."},i.dependencies={},i.prototype.init=function(e){e=e||{};if(e.scale){if(e.scale!=="SDO16"&&e.scale!=="SDO7"&&e.scale!=="SDO7s")throw new Error("SDO.init: scale must be SDO16, SDO7, SDO7s or undefined. Found: "+e.scale);this.scale=e.scale}if(e.choices){if(!J.isArray(e.choices)||e.choices.length<2)throw new Error("SDO.init: choices must be an array of length > 1 or undefined. Found: "+e.choices);this.choices=e.choices}if(e.header){if(!J.isArray(e.header)||e.header.length!==this.choices.length)throw new Error("SDO.init: header must be an array of length equal to the number of choices or undefined. Found: "+e.header);this.header=e.header}if(e.mainText){if("string"!=typeof e.mainText&&e.mainText!==!1)throw new Error("SDO.init: mainText must be string, false, or undefined. Found: "+e.mainText);this.mainText=e.mainText}this.requiredMark=e.requiredMark,this.displayRequired=e.displayRequired},i.prototype.append=function(){this.sdo=e.widgets.add("ChoiceTableGroup",this.panelDiv,{id:this.id||"SDO_choicetable",items:this.getItems(this.scale),choices:this.choices,mainText:this.mainText||this.getText("mainText"),title:!1,panel:!1,requiredChoice:this.required,header:this.header,displayRequired:this.displayRequired,requiredMark:this.requiredMark})},i.prototype.getItems=function(){var e=this.scale;return t[e].map(function(t,n){return[e+"_"+(n+1),t]})},i.prototype.getValues=function(e){return e=e||{},this.sdo.getValues(e)},i.prototype.setValues=function(e){return e=e||{},this.sdo.setValues(e)},i.prototype.enable=function(e){return this.sdo.enable(e)},i.prototype.disable=function(e){return this.sdo.disable(e)},i.prototype.highlight=function(e){return this.sdo.highlight(e)},i.prototype.unhighlight=function(e){return this.sdo.unhighlight(e)}}(node),function(e){"use strict";function t(){var e;e=this,this.slider=null,this.rangeFill=null,this.scale=1,this.currentValue=50,this.initialValue=50,this.step=1,this.mainText=null,this.required=null,this.requiredChoice=null,this.hint=null,this.min=0,this.max=100,this.correctValue=null,this.displayValue=!0,this.valueSpan=null,this.displayNoChange=!0,this.noChangeBtn=null,this.noChangeCb=null,this.errorBox=null,this.totalMove=0,this.nClicks=0,this.type="volume",this.hoverColor="#2076ea",this.left=null,this.right=null;var t=null;this.listener=function(n,r,i){var s;if(!n&&t)return;e.isHighlighted()&&e.unhighlight(),s=function(){var i,s;i=(e.slider.value-e.min)*e.scale,e.type==="volume"?(i>99&&(i=99),e.rangeFill.style.width=i+"%"):e.rangeFill.style.width="99%",e.displayValue&&(e.valueSpan.innerHTML=e.getText("currentValue",e.slider.value)),e.displayNoChange&&n!==!0&&e.noChangeCheckbox.checked&&(e.noChangeCheckbox.checked=!1,J.removeClass(e.noChangeBtn,"italic")),r||(s=e.slider.value-e.currentValue,e.totalMove+=Math.abs(s),e.onmove&&e.onmove.call(e,e.slider.value,s)),e.currentValue=e.slider.value,t=null},i?s():t=setTimeout(s,0)},this.onmove=null,this.timeFrom="step",this.knobHiddenFirst=!1,this._tmpColor}e.widgets.register("Slider",t),t.version="0.7.0",t.description="Creates a configurable slider",t.className="slider",t.texts={currentValue:function(e,t){return"Value: "+t},noChange:"No change",error:"Movement required. If you agree with the current value, move the slider away and then back to this position.",autoHint:function(e){var t="";return e.knobHiddenFirst&&(t+="The slider knob will be shown after the first click. "),e.required&&(t+="Movement required."),t||!1}},t.prototype.init=function(e){var t,n;n="Slider.init: ";if("undefined"!=typeof e.min){t=J.isInt(e.min);if("number"!=typeof t)throw new TypeError(n+"min must be an integer or "+"undefined. Found: "+e.min);this.min=t}if("undefined"!=typeof e.max){t=J.isInt(e.max);if("number"!=typeof t)throw new TypeError(n+"max must be an integer or "+"undefined. Found: "+e.max);this.max=t}this.scale=100/(this.max-this.min),t=e.initialValue;if("undefined"!=typeof t){if(t==="random")t=J.randomInt(this.min-1,this.max);else{t=J.isInt(t,this.min,this.max,!0,!0);if("number"!=typeof t)throw new TypeError(n+"initialValue must be an "+"integer >= "+this.min+" and =< "+this.max+" or undefined. Found: "+e.initialValue)}this.initialValue=this.currentValue=t}"undefined"!=typeof e.hideKnob&&(this.knobHiddenFirst=!!e.hideKnob);if("undefined"!=typeof e.step){t=J.isInt(e.step);if("number"!=typeof t)throw new TypeError(n+"step must be an integer or "+"undefined. Found: "+e.step);this.step=t}"undefined"!=typeof e.displayValue&&(this.displayValue=!!e.displayValue),"undefined"!=typeof e.displayNoChange&&(this.displayNoChange=!!e.displayNoChange);if("undefined"!=typeof e.noChangeCb){if("function"!=typeof e.noChangeCb)throw new TypeError(n+"noChangeCb must be function or "+"undefined. Found: "+e.noChangeCb);this.noChangeCb=e.noChangeCb}if(e.type){if(e.type!=="volume"&&e.type!=="flat")throw new TypeError(n+'type must be "volume", "flat", or '+"undefined. Found: "+e.type);this.type=e.type}t=e.requiredChoice,"undefined"!=typeof t?console.log("***Slider.init: requiredChoice is deprecated. Use required instead.***"):"undefined"!=typeof e.required&&(t=e.required),"undefined"!=typeof t&&(this.requiredChoice=this.required=!!t);if(e.mainText){if("string"!=typeof e.mainText)throw new TypeError(n+"mainText must be string or "+"undefined. Found: "+e.mainText);this.mainText=e.mainText}if("undefined"!=typeof e.hint){if(!1!==e.hint&&"string"!=typeof e.hint)throw new TypeError(n+"hint must be a string, false, or "+"undefined. Found: "+e.hint);this.hint=e.hint}else this.hint=this.getText("autoHint");this.required&&this.hint!==!1&&e.displayRequired!==!1&&(this.hint+=" "+this.requiredMark);if(e.onmove){if("function"!=typeof e.onmove)throw new TypeError(n+"onmove must be a function or "+"undefined. Found: "+e.onmove);this.onmove=e.onmove}if(e.width){if("string"!=typeof e.width)throw new TypeError(n+"width must be string or "+"undefined. Found: "+e.width);this.sliderWidth=e.width}if(e.hoverColor){if("string"!=typeof e.hoverColor)throw new TypeError(n+"hoverColor must be string or "+"undefined. Found: "+e.hoverColor);this.hoverColor=e.hoverColor}if("undefined"!=typeof e.correctValue){if(!1===J.isNumber(e.correctValue,this.min,this.max,!0,!0))throw new Error(n+"correctValue must be a number between "+this.min+" and "+this.max+". Found: "+e.correctValue);this.correctValue=e.correctValue}t=e.left;if("undefined"!=typeof t){if("string"!=typeof t&&"number"!=typeof t)throw new TypeError(n+"left must be string, number or "+"undefined. Found: "+t);this.left=""+t}t=e.right;if("undefined"!=typeof t){if("string"!=typeof t&&"number"!=typeof t)throw new TypeError(n+"right must be string, number or "+"undefined. Found: "+t);this.right=""+t}},t.prototype.append=function(){var e,t,n=this;this.mainText&&(this.spanMainText=W.append("span",this.bodyDiv,{className:"slider-maintext",innerHTML:this.mainText})),this.hint&&W.append("span",this.bodyDiv,{className:"slider-hint",innerHTML:this.hint}),e=W.add("div",this.bodyDiv,{className:"container-slider"}),this.left&&(t=W.add("span",e),t.innerHTML=this.left,t.style.position="relative",t.style.top="-20px",t.style.float="left"),this.rangeFill=W.add("div",e,{className:"fill-slider"}),t={className:"volume-slider",name:"rangeslider",type:"range",min:this.min,max:this.max,step:this.step},this.knobHiddenFirst&&(t.style={opacity:0}),this.slider=W.add("input",e,t),this.slider.onmousedown=function(){n.knobHiddenFirst&&n.isKnobHidden()&&(n.showKnob(),n.listener(!0,!1,!0)),n.nClicks++},this.slider.ontouchstart=this.slider.onmousedown,this.slider.onmouseover=function(){if(n.slider.disabled)return;n._tmpColor=n.rangeFill.style.background||"black",n.rangeFill.style.background=n.hoverColor},this.slider.onmouseout=function(){if(n.slider.disabled)return;n.rangeFill.style.background=n._tmpColor},this.sliderWidth&&(this.slider.style.width=this.sliderWidth),this.right&&(t=W.add("span",e),t.innerHTML=this.right,t.style.position="relative",t.style.top="-20px",t.style.float="right"),this.displayNoChange&&(this.noChangeBtn=W.add("button",this.bodyDiv,{className:"btn btn-danger btn-sm slider-display-nochange",innerHTML:this.getText("noChange")+" "}),this.noChangeCheckbox=W.add("input",this.noChangeBtn,{type:"checkbox"}),this.noChangeBtn.onclick=function(e){var t,r;t=n.noChangeCheckbox,r=e.target&&e.target.type==="checkbox",n.noChange?(J.removeClass(n.noChangeBtn,"italic"),n.noChange=!1,t.checked=!1,n.enableSlider()):(J.addClass(n.noChangeBtn,"italic"),n.noChange=!0,t.checked=!0,n.disableSlider()),n.noChangeCb&&n.noChangeCb(n,n.noChange)}),this.displayValue&&(this.valueSpan=W.add("span",this.bodyDiv,{className:"slider-display-value"})),this.errorBox=W.append("div",this.bodyDiv,{className:"errbox"}),this.slider.value=this.initialValue,this.slider.oninput=this.listener,this.slider.oninput(!1,!0)},t.prototype.getValues=function(t){var n,r;return t=t||{},n=!0,"undefined"==typeof t.highlight&&(t.highlight=!0),this.isChoiceDone()||(t.highlight&&(this.highlight(),this.setError(this.getText("error"))),n=!1),r=this.noChangeCheckbox&&this.noChangeCheckbox.checked,{value:this.currentValue,noChange:!!r,initialValue:this.initialValue,totalMove:this.totalMove,nClicks:this.nClicks,isCorrect:n,time:e.timer.getTimeSince(this.timeFrom)}},t.prototype.setValues=function(e){var t;"undefined"==typeof e?e={}:"number"==typeof e&&(e={value:e}),e.correct&&this.correctValue!==null?t=this.correctValue:"number"!=typeof e.value?(t=J.randomInt(0,101)-1,this.required&&this.totalMove===0&&t===this.slider.value&&t++):t=e.value,this.slider.value=t,this.slider.oninput(!1,!1,!0)},t.prototype.disableSlider=function(e){W.addClass(this.rangeFill,"disabled"),W.addClass(this.slider,"disabled"),this._tmpColor=this.rangeFill.style.background||"black",this.rangeFill.style.background="grey",this.slider.disabled=!0,e!==!1&&this.hideKnob()},t.prototype.enableSlider=function(e){W.removeClass(this.rangeFill,"disabled"),W.removeClass(this.slider,"disabled"),this.rangeFill.style.background=this._tmpColor,this.slider.disabled=!1,e!==!1&&this.knobHiddenFirst&&this.nClicks!==0&&this.showKnob()},t.prototype.hideKnob=function(){this.slider.style.opacity=0},t.prototype.showKnob=function(){this.slider.style.opacity=1},t.prototype.isKnobHidden=function(){return this.slider.style.opacity==0},t.prototype.disable=function(){if(this.disabled===!0)return;this.disabled=!0,this.disableSlider(),this.noChangeBtn&&(this.noChangeBtn.disabled=!0,this.noChangeCheckbox.disabled=!0),this.emit("disabled")},t.prototype.enable=function(){if(this.disabled===!1)return;this.disabled=!1,this.enableSlider(),this.noChangeBtn&&(this.noChangeBtn.disabled=!1,this.noChangeCheckbox.disabled=!1),this.emit("enabled")},t.prototype.setError=function(e){this.errorBox.innerHTML=e||"",e?this.highlight():this.unhighlight()},t.prototype.isChoiceDone=function(){var e,t;return e=this.currentValue,t=this.noChangeCheckbox&&this.noChangeCheckbox.checked,!(this.required&&this.totalMove===0&&!t||null!==this.correctValue&&this.correctValue!==e)}}(node),function(e){"use strict";function t(){this.methods={},this.method="Slider",this.mainText=null,this.gauge=null,this.addMethod("Slider",n)}function n(t){var n,r,i,s,o,u,a;r=t.sliders||[[[85,85],[85,76],[85,68],[85,59],[85,50],[85,41],[85,33],[85,24],[85,15]],[[85,15],[87,19],[89,24],[91,28],[93,33],[94,37],[96,41],[98,46],[100,50]],[[50,100],[54,98],[59,96],[63,94],[68,93],[72,91],[76,89],[81,87],[85,85]],[[50,100],[54,89],[59,79],[63,68],[68,58],[72,47],[76,36],[81,26],[85,15]],[[100,50],[94,56],[88,63],[81,69],[75,75],[69,81],[63,88],[56,94],[50,100]],[[100,50],[98,54],[96,59],[94,63],[93,68],[91,72],[89,76],[87,81],[85,85]]],this.sliders=r,a=t.renderer||function(e,t,n){e.innerHTML=t[0]+"
"+t[1]},u=r.length,n=new Array(u),o=-1;for(;++oextra bonus. Choose the preferred bonus amounts (in cents) for you and the other participant in each row.
We will select one row at random and add the bonus to your and the other participant's payment. Your choice will remain anonymous.",left:"Your Bonus:
Other's Bonus:"},t.prototype.init=function(t){var n,r;if("undefined"!=typeof t.method){if("string"!=typeof t.method)throw new TypeError("SVOGauge.init: method must be string or undefined. Found: "+t.method);if(!this.methods[t.method])throw new Error("SVOGauge.init: method is invalid: "+t.method);this.method=t.method}if("undefined"!=typeof t.mainText){if(t.mainText!==!1&&"string"!=typeof t.mainText)throw new TypeError("SVOGauge.init: mainText must be string false, or undefined. Found: "+t.mainText);this.mainText=t.mainText}n=this.methods[this.method].call(this,t),r=this,n.isHidden=function(){return r.isHidden()},n.isCollapsed=function(){return r.isCollapsed()};if(!e.widgets.isWidget(n))throw new Error("SVOGauge.init: method "+this.method+" created invalid gauge: missing default widget "+"methods.");this.gauge=n,this.on("enabled",function(){n.enable()}),this.on("disabled",function(){n.disable()}),this.on("highlighted",function(){n.highlight()}),this.on("unhighlighted",function(){n.unhighlight()}),this.displayRequired=t.displayRequired,this.requiredMark=t.requiredMark},t.prototype.append=function(){e.widgets.append(this.gauge,this.bodyDiv)},t.prototype.addMethod=function(e,t){if("string"!=typeof e)throw new Error("SVOGauge.addMethod: name must be string: "+e);if("function"!=typeof t)throw new Error("SVOGauge.addMethod: cb must be function: "+t);if(this.methods[e])throw new Error("SVOGauge.addMethod: name already existing: "+e);this.methods[e]=t},t.prototype.getValues=function(e){return e=e||{},"undefined"==typeof e.processChoice&&(e.processChoice=function(e){return e===null?null:this.choices[e]}),this.gauge.getValues(e)},t.prototype.setValues=function(e){return this.gauge.setValues(e)}}(node),function(e){"use strict";function t(){this.options=null,this.displayMode=null,this.stager=null,this.gamePlot=null,this.curStage=null,this.totStage=null,this.curRound=null,this.totRound=null,this.stageOffset=null,this.totStageOffset=null,this.oldStageId=null,this.separator=" / ",this.layout=null}function n(e,t){l(this,e,"COUNT_UP_STAGES",t),c(this,"stagediv",this.visualRound.getText("stage"))}function r(e,t){l(this,e,"COUNT_DOWN_STAGES",t),c(this,"stagediv",e.getText("stageLeft"))}function i(e,t){l(this,e,"COUNT_UP_STEPS",t),c(this,"stepdiv",this.visualRound.getText("step"))}function s(e,t){l(this,e,"COUNT_DOWN_STEPS",t),c(this,"stepdiv",this.visualRound.getText("stepLeft"))}function o(e,t){l(this,e,"COUNT_UP_ROUNDS",t),c(this,"rounddiv",e.getText("round"))}function u(e,t){l(this,e,"COUNT_DOWN_ROUNDS",t),c(this,"rounddiv",e.getText("roundLeft"))}function a(e,t,n){this.visualRound=e,this.displayModes=t,this.name=t.join("&"),this.options=n||{},this.displayDiv=null,this.init(n)}function f(e,t,n){return t==="vertical"||t==="multimode_vertical"||t==="all_vertical"?(e.displayDiv.style.float="none",e.titleDiv.style.float="none",e.titleDiv.style["margin-right"]="0px",e.contentDiv.style.float="none",!0):t==="horizontal"?(e.displayDiv.style.float="none",e.titleDiv.style.float="left",e.titleDiv.style["margin-right"]="6px",e.contentDiv.style.float="right",!0):t==="multimode_horizontal"?(e.displayDiv.style.float="left",e.titleDiv.style.float="none",e.titleDiv.style["margin-right"]="0px",e.contentDiv.style.float="none",n||(e.displayDiv.style["margin-right"]="10px"),!0):t==="all_horizontal"?(e.displayDiv.style.float="left",e.titleDiv.style.float="left",e.titleDiv.style["margin-right"]="6px",e.contentDiv.style.float="right",n||(e.displayDiv.style["margin-right"]="10px"),!0):!1}function l(e,t,n,r){r=r||{},e.visualRound=t,e.name=n,r.toTotal&&(e.name+="_TO_TOTAL"),e.options=r,e.displayDiv=null,e.titleDiv=null,e.contentDiv=null,e.current=null,e.textDiv=null,e.total=null}function c(e,t,n){e.displayDiv=W.get("div",{className:t}),e.titleDiv=W.add("div",e.displayDiv,{className:"title",innerHTML:n}),e.contentDiv=W.add("div",e.displayDiv,{className:"content"}),e.current=W.append("span",e.contentDiv,{className:"number"}),e.options.toTotal&&(e.textDiv=W.append("span",e.contentDiv,{className:"text",innerHTML:e.visualRound.separator}),e.total=W.append("span",e.contentDiv,{className:"number"})),e.updateDisplay()}e.widgets.register("VisualRound",t),t.version="0.9.1",t.description="Displays current/total/left round/stage/step. ",t.className="visualround",t.texts={round:"Round",step:"Step",stage:"Stage",roundLeft:"Rounds Left",stepLeft:"Steps Left",stageLeft:"Stages Left"},t.dependencies={GamePlot:{}},t.prototype.init=function(t){t=t||{},J.mixout(t,this.options),this.options=t,this.stageOffset=this.options.stageOffset||0,this.totStageOffset="undefined"==typeof this.options.totStageOffset?this.stageOffset:this.options.totStageOffset,this.options.flexibleMode&&(this.curStage=this.options.curStage||1,this.curStage-=this.options.stageOffset||0,this.curStep=this.options.curStep||1,this.curRound=this.options.curRound||1,this.totStage=this.options.totStage,this.totRound=this.options.totRound,this.totStep=this.options.totStep,this.oldStageId=this.options.oldStageId),this.gamePlot||(this.gamePlot=e.game.plot),this.stager||(this.stager=this.gamePlot.stager),this.updateInformation(),this.options.displayMode?this.setDisplayMode(this.options.displayMode):this.setDisplayMode(["COUNT_UP_ROUNDS_TO_TOTAL_IFNOT1","COUNT_UP_STAGES_TO_TOTAL"]),"undefined"!=typeof t.separator&&(this.separator=t.separator),"undefined"!=typeof t.layout&&(this.layout=t.layout);if("undefined"!=typeof t.preprocess){if("function"!=typeof t.preprocess)throw new TypeError("VisualRound.init: preprocess must function or undefined. Found: "+t.preprocess);this.preprocess=t.preprocess}this.updateDisplay()},t.prototype.append=function(){this.activate(this.displayMode),this.updateDisplay()},t.prototype.updateDisplay=function(){this.displayMode&&this.displayMode.updateDisplay()},t.prototype.setDisplayMode=function(e){var t,f,l;if("string"==typeof e)e=[e];else if(!J.isArray(e))throw new TypeError("VisualRound.setDisplayMode: displayMode must be array or string. Found: "+e);f=e.length;if(f===0)throw new Error("VisualRound.setDisplayMode: displayMode is empty");if(this.displayMode){if(e.join("&")===this.displayMode.name)return;this.deactivate(this.displayMode)}l=[],t=-1;for(;++tt.stage?i=s:i=1,i}function i(e){var t,n,r;t=e.split(" "),e=s(t[0]),r=t.length,r>1&&(e+=" "+s(t[1]));if(r>2)for(n=2;n'+e.getText(i)+""+r),W.add("span",e.div,{innerHTML:r,className:"visualstage-"+i})}function a(e,t){var n;n=t.indexOf(e[0]);if(n===-1)return"unknown item: "+e[0];t.splice(n,1),n=t.indexOf(e[1]);if(n===-1)return"unknown item: "+e[1];t.splice(n,1),n=t.indexOf(e[2]);if(n===-1)return"unknown item: "+e[2];t.splice(n,1);if(t.length)return"duplicated entry: "+t[0];return}var t=W.Table;e.widgets.register("VisualStage",n),n.version="0.11.0",n.description="Displays the name of the current, previous and next step of the game.",n.className="visualstage",n.texts={miss:"",current:"Stage: ",previous:"Prev: ",next:"Next: "},n.dependencies={Table:{}},n.prototype.init=function(e){var t;if("undefined"!=typeof e.displayMode){if(e.displayMode!=="inline"&&e.displayMode!=="table")throw new TypeError('VisualStage.init: displayMode must be "inline", "table" or undefined. Found: '+e.displayMode);this.displayMode=e.displayMode}"undefined"!=typeof e.addRound&&(this.addRound=!!e.addRound),"undefined"!=typeof e.previous&&(this.showPrevious=!!e.previous),"undefined"!=typeof e.next&&(this.showNext=!!e.next),"undefined"!=typeof e.current&&(this.showCurrent=!!e.current);if("undefined"!=typeof e.order){if(!J.isArray(e.order)||e.order.length!==3)throw new TypeError("VisualStage.init: order must be an array of length 3 or undefined. Found: "+e.order);t=a(e.order,this.order.slice(0));if(t)throw new TypeError("VisualStage.init: order contains errors: "+e.order);this.order=e.order}else this.displayMode==="inline"&&(this.order=["previous","current","next"]);if("undefined"!=typeof e.preprocess){if("function"!=typeof e.preprocess)throw new TypeError("VisualStage.init: preprocess must be function or undefined. Found: "+e.preprocess);this.preprocess=e.preprocess}"undefined"!=typeof e.capitalize&&(this.capitalize=!!e.capitalize),"undefined"!=typeof e.replaceUnderscore&&(this.replaceUnderscore=!!e.replaceUnderscore)},n.prototype.append=function(){this.displayMode==="table"?(this.table=new t,this.bodyDiv.appendChild(this.table.table)):this.div=W.append("div",this.bodyDiv),this.updateDisplay()},n.prototype.listeners=function(){var t=this;e.on("STEP_CALLBACK_EXECUTED",function(){t.updateDisplay()})},n.prototype.updateDisplay=function(){var t,n,r,i,s,a,f,l;f={},t=e.game.getCurrentGameStage(),t&&(this.showCurrent&&(i=this.getStepName(t,t,"current"),f.current=i),this.showNext&&(n=e.game.plot.next(t),n&&(s=this.getStepName(n,t,"next"),f.next=s)),this.showPrevious&&(r=e.game.plot.previous(t),r&&(a=this.getStepName(r,t,"previous"),f.previous=a))),this.displayMode==="table"?(this.table.clear(!0),o(this,0,f),o(this,1,f),o(this,2,f),l=this.table.selexec("y","=",0),l.addClass("strong"),this.table.parse()):(this.div.innerHTML="",u(this,0,f),u(this,1,f),u(this,2,f))},n.prototype.getStepName=function(t,n,s){var o,u,a,f;return o=e.game.plot.getProperty(t,"name"),"function"==typeof o?(a=o,o=null):"object"==typeof o&&o!==null&&(a=o.preprocess,f=o.addRound,o=o.name),o||(o=e.game.plot.getStep(t),o?(o=o.id,this.replaceUnderscore&&(o=o.replace(/_/g," ")),this.capitalize&&(o=i(o))):o=this.getText("miss")),a||(a=this.preprocess),"undefined"==typeof f&&(f=this.addRound),u=r(t,n,s),a&&(o=a.call(e.game,o,s,u)),f&&u&&(o+=" "+u),o}}(node),function(e){"use strict";function t(){this.gameTimer=null,this.mainBox=null,this.waitBox=null,this.activeBox=null,this.isInitialized=!1,this.options={},this.internalTimer=null}function n(e){this.boxDiv=null,this.titleDiv=null,this.bodyDiv=null,this.timeLeft=null,this.boxDiv=W.get("div"),this.titleDiv=W.add("div",this.boxDiv),this.bodyDiv=W.add("div",this.boxDiv),this.init(e)}function r(t){t.internalTimer?(t.gameTimer.isDestroyed()||e.timer.destroyTimer(t.gameTimer),t.internalTimer=null):t.gameTimer.removeHook("VisualTimer_"+t.wid)}e.widgets.register("VisualTimer",t),t.version="0.9.3",t.description="Display a configurable timer for the game. Can trigger events. Only for countdown smaller than 1h.",t.title="Time Left",t.className="visualtimer",t.dependencies={GameTimer:{}},t.prototype.init=function(t){var r;t=t||{};if("object"!=typeof t)throw new TypeError("VisualTimer.init: opts must be object or undefined. Found: "+t);r={};if("undefined"!=typeof t.gameTimer){if(this.gameTimer)throw new Error("GameTimer.init: opts.gameTimer cannot be set if a gameTimer is already existing: "+this.name);if("object"!=typeof t.gameTimer)throw new TypeError("VisualTimer.init: opts.gameTimer must be object or undefined. Found: "+t.gameTimer);this.gameTimer=t.gameTimer}else this.isInitialized||(this.internalTimer=!0,this.gameTimer=e.timer.createTimer({name:t.name||"VisualTimer_"+J.randomInt(1e7)}));if(t.hooks){if(!this.internalTimer)throw new Error("VisualTimer.init: cannot add hooks on external gameTimer.");J.isArray(t.hooks)||(r.hooks=[t.hooks])}else r.hooks=[];this.isInitialized||r.hooks.push({name:"VisualTimer_"+this.wid,hook:this.updateDisplay,ctx:this}),"undefined"!=typeof t.milliseconds&&(r.milliseconds=e.timer.parseInput("milliseconds",t.milliseconds)),"undefined"!=typeof t.update?r.update=e.timer.parseInput("update",t.update):r.update=1e3,"undefined"!=typeof t.timeup&&(r.timeup=t.timeup),this.gameTimer.init(r),this.options=r,"undefined"!=typeof t.stopOnDone?this.options.stopOnDone=!!t.stopOnDone:"undefined"==typeof this.options.stopOnDone&&(this.options.stopOnDone=!0),"undefined"!=typeof t.startOnPlaying?this.options.startOnPlaying=!!t.startOnPlaying:"undefined"==typeof this.options.startOnPlaying&&(this.options.startOnPlaying=!0),this.options.mainBoxOptions||(this.options.mainBoxOptions={}),this.options.waitBoxOptions||(this.options.waitBoxOptions={}),J.mixout(this.options.mainBoxOptions,{classNameBody:t.className,hideTitle:!0}),J.mixout(this.options.waitBoxOptions,{title:"Max. wait timer",classNameTitle:"waitTimerTitle",classNameBody:"waitTimerBody",hideBox:!0}),this.mainBox?this.mainBox.init(this.options.mainBoxOptions):this.mainBox=new n(this.options.mainBoxOptions),this.waitBox?this.waitBox.init(this.options.waitBoxOptions):this.waitBox=new n(this.options.waitBoxOptions),this.activeBox=this.options.activeBox||this.mainBox,this.isInitialized=!0},t.prototype.append=function(){this.bodyDiv.appendChild(this.mainBox.boxDiv),this.bodyDiv.appendChild(this.waitBox.boxDiv),this.activeBox=this.mainBox,this.updateDisplay()},t.prototype.clear=function(e){var t;return e=e||{},t=this.options,r(this),this.gameTimer=null,this.activeBox=null,this.isInitialized=!1,this.init(e),t},t.prototype.updateDisplay=function(){var e,t,n;if(!this.gameTimer.milliseconds||this.gameTimer.milliseconds===0){this.activeBox.bodyDiv.innerHTML="00:00";return}e=this.gameTimer.milliseconds-this.gameTimer.timePassed,e=J.parseMilliseconds(e),t=e[2]<10?"0"+e[2]:e[2],n=e[3]<10?"0"+e[3]:e[3],this.activeBox.bodyDiv.innerHTML=t+":"+n},t.prototype.start=function(){this.updateDisplay(),this.gameTimer.start()},t.prototype.restart=function(e){this.stop(),"number"==typeof e&&(e={milliseconds:e}),this.init(e),this.start()},t.prototype.stop=function(){this.gameTimer.isStopped()||(this.activeBox.timeLeft=this.gameTimer.timeLeft,this.gameTimer.stop())},t.prototype.switchActiveBoxTo=function(e){this.activeBox.timeLeft=this.gameTimer.timeLeft||0,this.activeBox=e,this.updateDisplay()},t.prototype.startWaiting=function(e){"undefined"==typeof e&&(e={}),"undefined"==typeof e.milliseconds&&(e.milliseconds=this.gameTimer.timeLeft),"undefined"==typeof e.mainBoxOptions&&(e.mainBoxOptions={}),"undefined"==typeof e.waitBoxOptions&&(e.waitBoxOptions={}),e.mainBoxOptions.classNameBody="strike",e.mainBoxOptions.timeLeft=this.gameTimer.timeLeft||0,e.activeBox=this.waitBox,e.waitBoxOptions.hideBox=!1,this.restart(e)},t.prototype.startTiming=function(e){"undefined"==typeof e&&(e={}),"undefined"==typeof e.mainBoxOptions&&(e.mainBoxOptions={}),"undefined"==typeof e.waitBoxOptions&&(e.waitBoxOptions={}),e.activeBox=this.mainBox,e.waitBoxOptions.timeLeft=this.gameTimer.timeLeft||0,e.waitBoxOptions.hideBox=!0,e.mainBoxOptions.classNameBody="",this.restart(e)},t.prototype.resume=function(){this.gameTimer.resume()},t.prototype.setToZero=function(){this.stop(),this.activeBox.bodyDiv.innerHTML="00:00",this.activeBox.setClassNameBody("strike")},t.prototype.isTimeup=function(){return this.gameTimer.isTimeup()},t.prototype.doTimeUp=function(){this.gameTimer.doTimeUp()},t.prototype.listeners=function(){var t=this;if(!this.internalTimer)return;e.on("PLAYING",function(){var e;t.options.startOnPlaying&&(e=t.gameTimer.getStepOptions(),e?(e.update=t.update,e.timeup=undefined,t.startTiming(e)):t.gameTimer.isRunning()||t.setToZero())}),e.on("REALLY_DONE",function(){t.options.stopOnDone&&(t.gameTimer.isStopped()||t.stop())}),this.on("destroyed",function(){r(t),t.bodyDiv.removeChild(t.mainBox.boxDiv),t.bodyDiv.removeChild(t.waitBox.boxDiv)})},n.prototype.init=function(e){e&&(e.hideTitle?this.hideTitle():this.unhideTitle(),e.hideBody?this.hideBody():this.unhideBody(),e.hideBox?this.hideBox():this.unhideBox()),this.setTitle(e.title||""),this.setClassNameTitle(e.classNameTitle||""),this.setClassNameBody(e.classNameBody||""),e.timeLeft&&(this.timeLeft=e.timeLeft)},n.prototype.hideBox=function(){this.boxDiv.style.display="none"},n.prototype.unhideBox=function(){this.boxDiv.style.display=""},n.prototype.hideTitle=function(){this.titleDiv.style.display="none"},n.prototype.unhideTitle=function(){this.titleDiv.style.display=""},n.prototype.hideBody=function(){this.bodyDiv.style.display="none"},n.prototype.unhideBody=function(){this.bodyDiv.style.display=""},n.prototype.setTitle=function(e){this.titleDiv.innerHTML=e},n.prototype.setClassNameTitle=function(e){this.titleDiv.className=e},n.prototype.setClassNameBody=function(e){this.bodyDiv.className=e}}(node),function(e){"use strict";function t(){this.connected=0,this.poolSize=0,this.nGames=undefined,this.groupSize=0,this.waitTime=null,this.executionMode=null,this.startDate=null,this.timeoutId=null,this.execModeDiv=null,this.playerCount=null,this.startDateDiv=null,this.msgDiv=null,this.timerDiv=null,this.timer=null,this.dots=null,this.onTimeout=null,this.disconnectIfNotSelected=null,this.userCanDispatch=null,this.playBtn=null,this.userCanSelectTreat=null,this.selectedTreatment=null,this.addDefaultTreatments=null,this.treatmentTiles=null}function n(t){var n,r;return n=document.getElementById("play_btn_group"),n?n:(n=document.createElement("div"),n.id="play_btn_group",n.role="group",n["aria-label"]="Play Buttons",n.className="btn-group",r=document.createElement("input"),r.className="btn btn-primary btn-lg",r.value=t.getText("playBot"),r.id="play_btn",r.type="button",r.onclick=function(){t.playBtn.value=t.getText("connectingBots"),t.playBtn.disabled=!0,e.say("DISPATCH","SERVER",t.selectedTreatment),setTimeout(function(){t.playBtn.value=t.getText("playBot"),t.playBtn.disabled=!1},5e3)},n.appendChild(r),t.playBtn=r,t.bodyDiv.appendChild(document.createElement("br")),t.bodyDiv.appendChild(n),n)}function r(e,t){var r;r=n(e);var i=document.createElement("div");i.role="group",i["aria-label"]="Select Treatment",i.className="btn-group";var s=document.createElement("button");s.className="btn btn-default btn-lg dropdown-toggle",s["data-toggle"]="dropdown",s["aria-haspopup"]="true",s["aria-expanded"]="false",s.innerHTML=e.getText("selectTreatment");var o=document.createElement("span");o.className="caret",s.appendChild(o);var u=document.createElement("ul");u.className="dropdown-menu",u.style["text-align"]="left";var a,f,l,c,h,p,d;if(t.availableTreatments){a=document.createElement("li"),a.innerHTML=e.getText("gameTreatments"),a.className="dropdown-header",u.appendChild(a);for(l in t.availableTreatments)t.availableTreatments.hasOwnProperty(l)&&(a=document.createElement("li"),a.id=l,f=document.createElement("a"),f.href="#",f.innerHTML=""+l+": "+t.availableTreatments[l],a.appendChild(f),l==="treatment_latin_square"?p=a:l==="treatment_rotate"?c=a:l==="treatment_random"?h=a:l==="treatment_weighted_random"?d=a:u.appendChild(a));e.addDefaultTreatments!==!1&&(a=document.createElement("li"),a.role="separator",a.className="divider",u.appendChild(a),a=document.createElement("li"),a.innerHTML=e.getText("defaultTreatments"),a.className="dropdown-header",u.appendChild(a),u.appendChild(c),u.appendChild(h),u.appendChild(p),u.appendChild(d))}i.appendChild(s),i.appendChild(u),r.appendChild(i),s.onclick=function(){u.style.display===""?u.style.display="block":u.style.display=""},u.onclick=function(t){var n;n=t.target,u.style.display="",n=n.parentNode.id,n||(n=t.target.parentNode.parentNode.id);if(!n)return;s.innerHTML=n+" ",s.appendChild(o),e.selectedTreatment=n},e.treatmentBtn=s}function i(t,n){var r,i,s,o,u,a,f,l,c,h,p;p=W.add("div",t.bodyDiv),p.style.display="flex",p.style["flex-wrap"]="wrap",p.style["column-gap"]="20px",p.style["justify-content"]="space-between",p.style.margin="50px 100px 30px 150px",p.style["text-align"]="center",p.className="waitroom-listContainer",a=0;if(n.availableTreatments){for(s in n.availableTreatments)n.availableTreatments.hasOwnProperty(s)&&(r=document.createElement("div"),r.id=s,r.style.flex="200px",r.style["margin-top"]="10px",r.className="treatment waitroom-list",i=document.createElement("span"),t.treatmentTileCb?u=t.treatmentTileCb(s,n.availableTreatments[s],++a,t):(o=s,s.length>16&&(o=''+s.substr(0,13)+"..."),u=""+o+"
"+''+n.availableTreatments[s]+""),i.innerHTML=u,r.appendChild(i),r.onclick=function(){var n;n=this.id,t.selectedTreatment=n,e.say("DISPATCH","SERVER",t.selectedTreatment)},s=s.substring(10),s==="latin_square"?c=r:s==="rotate"?f=r:s==="random"?l=r:s==="weighted_random"?h=r:p.appendChild(r));t.addDefaultTreatments!==!1&&(p.appendChild(f),p.appendChild(l),p.appendChild(c),p.appendChild(h))}}e.widgets.register("WaitingRoom",t),t.version="1.4.0",t.description="Displays a waiting room for clients.",t.title="Waiting Room",t.className="waitingroom",t.dependencies={VisualTimer:{}},t.sounds={dispatch:"/sounds/doorbell.ogg"},t.texts={blinkTitle:"GAME STARTS!",waitingForConf:"Waiting to receive data",executionMode:function(e){return e.executionMode==="WAIT_FOR_N_PLAYERS"?"Waiting for All Players to Connect: ":e.executionMode==="WAIT_FOR_DISPATCH"?"Task will start soon. Please be patient.":"Task will start at:
"+e.startDate},disconnect:'You have been disconnected. Please try again later.

',waitedTooLong:"Waiting for too long. Please look for a HIT called Trouble Ticket and file a new trouble ticket reporting your experience.",notEnoughPlayers:'

Thank you for your patience.
Unfortunately, there are not enough participants in your group to start the experiment.
',roomClosed:' The waiting room is CLOSED. You have been disconnected. Please try again later.

',tooManyPlayers:function(e,t){var n;return n="There are more players in this waiting room than playslots in the game. ",e.poolSize===1?n+="Each player will play individually.":n+="Only "+t.nGames+" players will be selected "+"to play the game.",n},notSelectedClosed:'

Unfortunately, you were not selected to join the game this time. Thank you for your participation.



',notSelectedOpen:'

Unfortunately, you were not selected to join the game this time, but you may join the next one.Ok, I got it.



Thank you for your participation.

',exitCode:function(e,t){return"
You have been disconnected. "+("undefined"!=typeof t.exit?"Please report this exit code: "+t.exit:"")+"
"},playBot:function(e){return e.poolSize===e.groupSize&&e.groupSize===1?"Play":e.groupSize===2?"Play With Bot":"Play With Bots"},connectingBots:function(e){return console.log(e.poolSize,e.groupSize),e.poolSize===e.groupSize&&e.groupSize===1?"Starting, Please Wait...":e.groupSize===2?"Connecting Bot, Please Wait...":"Connecting Bot/s, Please Wait..."},selectTreatment:"Select Treatment ",gameTreatments:"Game:",defaultTreatments:"Defaults:"},t.prototype.init=function(t){var s,o;o=this;if("object"!=typeof t)throw new TypeError("WaitingRoom.init: conf must be object. Found: "+t);if(!t.executionMode)return;this.executionMode=t.executionMode;if(t.onTimeout){if("function"!=typeof t.onTimeout)throw new TypeError("WaitingRoom.init: conf.onTimeout must be function, null or undefined. Found: "+t.onTimeout);this.onTimeout=t.onTimeout}if(t.waitTime){if(null!==t.waitTime&&"number"!=typeof t.waitTime)throw new TypeError("WaitingRoom.init: conf.waitTime must be number, null or undefined. Found: "+t.waitTime);this.waitTime=t.waitTime}t.startDate&&(this.startDate=(new Date(t.startDate)).toString());if(t.poolSize){if(t.poolSize&&"number"!=typeof t.poolSize)throw new TypeError("WaitingRoom.init: conf.poolSize must be number or undefined. Found: "+t.poolSize);this.poolSize=t.poolSize}if(t.groupSize){if(t.groupSize&&"number"!=typeof t.groupSize)throw new TypeError("WaitingRoom.init: conf.groupSize must be number or undefined. Found: "+t.groupSize);this.groupSize=t.groupSize}if(t.nGames){if(t.nGames&&"number"!=typeof t.nGames)throw new TypeError("WaitingRoom.init: conf.nGames must be number or undefined. Found: "+t.nGames);this.nGames=t.nGames}if(t.connected){if(t.connected&&"number"!=typeof t.connected)throw new TypeError("WaitingRoom.init: conf.connected must be number or undefined. Found: "+t.connected);this.connected=t.connected}if(t.disconnectIfNotSelected){if("boolean"!=typeof t.disconnectIfNotSelected)throw new TypeError("WaitingRoom.init: conf.disconnectIfNotSelected must be boolean or undefined. Found: "+t.disconnectIfNotSelected);this.disconnectIfNotSelected=t.disconnectIfNotSelected}else this.disconnectIfNotSelected=!1;t.userCanDispatch?this.userCanDispatch=!0:this.userCanDispatch=!1,t.userCanSelectTreat?this.userCanSelectTreat=!0:this.userCanSelectTreat=!1,"undefined"!=typeof t.addDefaultTreatments?this.addDefaultTreatments=!!t.addDefaultTreatments:this.addDefaultTreatments=!0;if(t.queryStringTreatVar){s=J.getQueryString(t.queryStringTreatVar);if(s){if(!!t.availableTreatments[s]){e.say("DISPATCH","SERVER",s);return}alert("Unknown treatment: "+s)}}t.treatmentTileCb&&(this.treatmentTileCb=t.treatmentTileCb),"undefined"!=typeof t.treatmentTiles&&(this.treatmentTiles=t.treatmentTiles),this.displayExecMode(),this.userCanDispatch&&(this.userCanSelectTreat?this.treatmentTiles?i(this,t):r(this,t):n(this)),this.on("destroyed",function(){o.dots&&o.dots.stop(),e.deregisterSetup("waitroom")})},t.prototype.startTimer=function(){var t=this;if(this.timer)return;if(!this.waitTime)return;this.timerDiv||(this.timerDiv=document.createElement("div"),this.timerDiv.id="timer-div"),this.timerDiv.appendChild(document.createTextNode("Maximum Waiting Time: ")),this.timer=e.widgets.append("VisualTimer",this.timerDiv,{milliseconds:this.waitTime,timeup:function(){t.bodyDiv.innerHTML=t.getText("waitedTooLong")},update:1e3}),this.timer.setTitle(),this.timer.panelDiv.className="ng_widget visualtimer",this.bodyDiv.appendChild(this.timerDiv),this.timer.start()},t.prototype.clearTimeout=function(){this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)},t.prototype.updateState=function(e){if(!e)return;"number"==typeof e.connected&&(this.connected=e.connected),"number"==typeof e.poolSize&&(this.poolSize=e.poolSize),"number"==typeof e.groupSize&&(this.groupSize=e.groupSize)},t.prototype.updateDisplay=function(){var t,n;if(!this.execModeDiv){e.warn("WaitingRoom: cannot update display, inteface not ready");return}this.connected>this.poolSize?(n=Math.floor(this.connected/this.groupSize),"undefined"!=typeof this.nGames&&(n=n>this.nGames?this.nGames:n),t=n*this.groupSize,this.playerCount.innerHTML=''+this.connected+""+" / "+this.poolSize,this.playerCountTooHigh.style.display="",this.playerCountTooHigh.innerHTML=this.getText("tooManyPlayers",{nGames:t})):(this.playerCount.innerHTML=this.connected+" / "+this.poolSize,this.playerCountTooHigh.style.display="none")},t.prototype.displayExecMode=function(){this.bodyDiv.innerHTML="",this.execModeDiv=document.createElement("div"),this.execModeDiv.id="exec-mode-div",this.execModeDiv.innerHTML=this.getText("executionMode"),this.playerCount=document.createElement("p"),this.playerCount.id="player-count",this.execModeDiv.appendChild(this.playerCount),this.playerCountTooHigh=document.createElement("div"),this.playerCountTooHigh.style.display="none",this.execModeDiv.appendChild(this.playerCountTooHigh),this.startDateDiv=document.createElement("div"),this.startDateDiv.style.display="none",this.execModeDiv.appendChild(this.startDateDiv),this.dots=W.getLoadingDots(),this.execModeDiv.appendChild(this.dots.span),this.bodyDiv.appendChild(this.execModeDiv),this.msgDiv=document.createElement("div"),this.bodyDiv.appendChild(this.msgDiv),this.waitTime&&this.startTimer()},t.prototype.append=function(){this.bodyDiv.innerHTML=this.getText("waitingForConf")},t.prototype.listeners=function(){var t;t=this,e.registerSetup("waitroom",function(n){if(!n)return;if("object"!=typeof n){e.warn("waiting room widget: invalid setup object: "+n);return}return n.executionMode?t.init(n):(t.setSounds(n.sounds),t.setTexts(n.texts)),n}),e.on.data("PLAYERSCONNECTED",function(e){if(!e.data)return;t.connected=e.data,t.updateDisplay()}),e.on.data("DISPATCH",function(e){var n,r;e=e||{},n=e.data||{},t.dots&&t.dots.stop(),n.action==="allPlayersConnected"?t.alertPlayer():(r=t.getText("exitCode",n),n.action==="notEnoughPlayers"?(t.bodyDiv.innerHTML=t.getText(n.action),t.onTimeout&&t.onTimeout(e.data),t.disconnect(t.bodyDiv.innerHTML+r)):n.action==="notSelected"?!1===n.shouldDispatchMoreGames||t.disconnectIfNotSelected?(t.bodyDiv.innerHTML=t.getText("notSelectedClosed"),t.disconnect(t.bodyDiv.innerHTML+r)):t.msgDiv.innerHTML=t.getText("notSelectedOpen"):n.action==="disconnect"&&t.disconnect(t.bodyDiv.innerHTML+r))}),e.on.data("TIME",function(){e.info("waiting room: TIME IS UP!"),t.stopTimer()}),e.on.data("WAITTIME",function(e){t.updateState(e.data),t.updateDisplay()}),e.on("SOCKET_DISCONNECT",function(){t.stopTimer(),t.bodyDiv.innerHTML=t.getText("disconnect")}),e.on.data("ROOM_CLOSED",function(){t.disconnect(t.getText("roomClosed"))})},t.prototype.stopTimer=function(){this.timer&&(e.info("waiting room: PAUSING TIMER"),this.timer.stop())},t.prototype.disconnect=function(t){t&&this.setText("disconnect",t),e.socket.disconnect(),this.stopTimer()},t.prototype.alertPlayer=function(){var t,n,r,i;r=this.getText("blinkTitle"),i=this.getSound("dispatch"),i&&J.playSound(i);if(!r)return;document.hasFocus&&document.hasFocus()?J.blinkTitle(r,{repeatFor:1}):(t=J.blinkTitle(r,{stopOnFocus:!0,stopOnClick:window}),n=function(){var e;t(),e=W.getFrame(),e&&e.removeEventListener("mouseover",n,!1)},e.events.ng.once("FRAME_GENERATED",function(e){e.addEventListener("mouseover",n,!1)}))}}(node) \ No newline at end of file diff --git a/lib/Widget.js b/lib/Widget.js index f70f217..2a0ddee 100644 --- a/lib/Widget.js +++ b/lib/Widget.js @@ -1,6 +1,6 @@ /** * # Widget - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Prototype of a widget class @@ -156,6 +156,7 @@ if (!this.isHighlighted()) return; this.highlighted = false; this.bodyDiv.style.border = ''; + if (this.setError) this.setError(); this.emit('unhighlighted'); }; @@ -339,10 +340,26 @@ * @see Widget.hide * @see Widget.toggle */ - Widget.prototype.show = function(display) { + Widget.prototype.show = function(opts) { if (this.panelDiv && this.panelDiv.style.display === 'none') { - this.panelDiv.style.display = display || ''; + // Backward compatible. + opts = opts || {}; + if ('string' === typeof opts) opts = { display: opts }; + this.panelDiv.style.display = opts.display || ''; this.hidden = false; + + W.adjustFrameHeight(); + if (opts.scroll !== false) { + // Scroll into the slider. + if ('function' === typeof this.bodyDiv.scrollIntoView) { + this.bodyDiv.scrollIntoView({ behavior: 'smooth' }); + } + else if (window.scrollTo) { + // Scroll to bottom of page. + window.scrollTo(0, document.body.scrollHeight); + } + } + this.emit('shown'); } }; @@ -535,7 +552,7 @@ // Bootstrap 5. options = { className: 'card-footer' }; } - else if ('object' !== typeof options) { + else if ('object' !== typeof options && 'function') { throw new TypeError('Widget.setFooter: options must ' + 'be object or undefined. Found: ' + options); @@ -552,6 +569,9 @@ else if ('string' === typeof footer) { this.footerDiv.innerHTML = footer; } + else if ('function' === typeof footer) { + footer.call(this, this.footerDiv); + } else { throw new TypeError(J.funcName(this.constructor) + '.setFooter: footer must be string, ' + @@ -901,6 +921,28 @@ throw new Error(errMsg); }; + /** + * ### Widget.next + * + * Updates the widget with the next visualization within the same step + * + * @param {boolean} FALSE if there is no next visualization. + * + * @see Widget.prev + */ + Widget.prototype.next = function() { return false; }; + + /** + * ### Widget.prev + * + * Updates the widget with the previous visualization within the same step + * + * @param {boolean} FALSE if there is no prev visualization. + * + * @see Widget.next + */ + Widget.prototype.prev = function() { return false; }; + // ## Helper methods. /** @@ -926,11 +968,12 @@ */ 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 ('undefined' === typeof res) { + throw new Error(method + ': name not found: ' + name); + } + if ('function' === typeof res) { res = res(that, param); if ('string' !== typeof res && res !== false) { diff --git a/lib/Widgets.js b/lib/Widgets.js index 9238e3c..f17de69 100644 --- a/lib/Widgets.js +++ b/lib/Widgets.js @@ -31,18 +31,18 @@ * Container of appended widget instances * * @see Widgets.append - * @see Widgets.lastAppended + * @see Widgets.last */ this.instances = []; /** - * ### Widgets.lastAppended + * ### Widgets.last|lastAppended * * Reference to lastAppended widget * * @see Widgets.append */ - this.lastAppended = null; + this.last = this.lastAppended = null; /** * ### Widgets.docked @@ -72,6 +72,15 @@ */ this.collapseTarget = null; + /** + * ### Widgets.decorators + * + * Map of decorators callbacks for widgets + * + * @see Widgets.decorator + */ + this.decorators = {}; + that = this; node.registerSetup('widgets', function(conf) { var name, root, collapseTarget; @@ -227,7 +236,7 @@ * 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 + * @param {object} opts Optional. Configuration options, will be * mixed out with attributes in the `defaults` property * of the widget prototype. * @@ -236,116 +245,124 @@ * @see Widgets.append * @see Widgets.instances */ - Widgets.prototype.get = function(widgetName, options) { - var WidgetPrototype, widget, changes, tmp; + Widgets.prototype.get = function(widgetName, opts) { + var WidgetProto, widget, changes, tmp, err; + + err = 'Widgets.get'; if ('string' !== typeof widgetName) { - throw new TypeError('Widgets.get: widgetName must be string.' + + throw new TypeError(err + ': 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); + + err += widgetName + ': '; + + if (!opts) { + opts = {}; } - if (options.storeRef === false) { - if (options.docked === true) { - throw new TypeError('Widgets.get: ' + widgetName + - 'options.storeRef cannot be false ' + - 'if options.docked is true.'); - } + else if ('object' !== typeof opts) { + throw new TypeError(err + ' opts must be object or undefined. ' + + 'Found: ' + opts); } - WidgetPrototype = J.getNestedValue(widgetName, this.widgets); + WidgetProto = J.getNestedValue(widgetName, this.widgets); - if (!WidgetPrototype) { - throw new Error('Widgets.get: ' + widgetName + ' not found'); - } + if (!WidgetProto) throw new Error(err + ' not found'); + + node.info('creating widget ' + widgetName + ' v.' + + WidgetProto.version); - node.info('creating widget ' + widgetName + - ' v.' + WidgetPrototype.version); + // Merge shared options (if any). + tmp = this.decorators['*']; + if (tmp) tmp(opts); + tmp = this.decorators[widgetName]; + if (tmp) tmp(opts); - if (!this.checkDependencies(WidgetPrototype)) { - throw new Error('Widgets.get: ' + widgetName + ' has unmet ' + - 'dependencies'); + if (opts.storeRef === false) { + if (opts.docked === true || WidgetProto.docked) { + node.warn(err + ' storeRef=false ignored, widget is docked'); + } + } + + if (!this.checkDependencies(WidgetProto)) { + throw new Error(err + ' has unmet dependencies'); } // Create widget. - widget = new WidgetPrototype(options); + widget = new WidgetProto(opts); // Set ID. - tmp = options.id; + tmp = opts.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) { + if ('undefined' !== typeof opts.idPrefix) { + if ('string' === typeof opts.idPrefix && + 'number' !== typeof opts.idPrefix) { - tmp = options.idPrefix + tmp; + tmp = opts.idPrefix + tmp; } else { - throw new TypeError('Widgets.get: options.idPrefix ' + + throw new TypeError('Widgets.get: opts.idPrefix ' + 'must be string, number or ' + 'undefined. Found: ' + - options.idPrefix); + opts.idPrefix); } } widget.id = tmp; } else { - throw new TypeError('Widgets.get: options.id must be ' + + throw new TypeError('Widgets.get: opts.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) { + else if (opts.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; + // Set prototype values or opts values. + if ('undefined' !== typeof opts.title) { + widget.title = opts.title; } - else { - widget.title = ' '; + else if ('undefined' !== typeof WidgetProto.title) { + widget.title = WidgetProto.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(' '); + + widget.panel = 'undefined' === typeof opts.panel ? + WidgetProto.panel : opts.panel; + widget.footer = 'undefined' === typeof opts.footer ? + WidgetProto.footer : opts.footer; + widget.className = WidgetProto.className; + if (J.isArray(opts.className)) { + widget.className += ' ' + opts.className.join(' '); } - else if ('string' === typeof options.className) { - widget.className += ' ' + options.className; + else if ('string' === typeof opts.className) { + widget.className += ' ' + opts.className; } - else if ('undefined' !== typeof options.className) { + else if ('undefined' !== typeof opts.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; + opts.className); + } + widget.context = 'undefined' === typeof opts.context ? + WidgetProto.context : opts.context; + widget.sounds = 'undefined' === typeof opts.sounds ? + WidgetProto.sounds : opts.sounds; + widget.texts = 'undefined' === typeof opts.texts ? + WidgetProto.texts : opts.texts; + + widget.docked = 'undefined' === typeof opts.docked ? + WidgetProto.docked : opts.docked; + + widget.collapsible = opts.collapsible || false; + widget.closable = opts.closable || false; widget.collapseTarget = - options.collapseTarget || this.collapseTarget || null; - widget.info = options.info || false; + opts.collapseTarget || this.collapseTarget || null; + widget.info = opts.info || false; widget.hooks = { hidden: [], @@ -360,18 +377,26 @@ }; // By default destroy widget on exit step. - widget.destroyOnExit = options.destroyOnExit !== false; + widget.destroyOnExit = opts.destroyOnExit !== false; // Required widgets require action from user, otherwise they will // block node.done(). - if (options.required || - options.requiredChoice || - 'undefined' !== typeof options.correctChoice) { + if (opts.required === false) { + widget.required = false; + } + else if (opts.required || opts.requiredChoice || + ('undefined' !== typeof opts.correctChoice && + opts.correctChoice !== false)) { // Flag required is undefined, if not set to false explicitely. widget.required = true; } + // Display required mark (in some widgets). + widget.displayRequired = opts.displayRequired === false ? false : true; + widget.requiredMark = 'undefined' !== typeof opts.requiredMark ? + opts.requiredMark : '✳️'; // * + // Fixed properties. // Widget Name. @@ -385,44 +410,49 @@ 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; + // Option already checked. + if (widget.docked) widget._docked = true; + + // Bootstrap 5 by default. + if (opts.bootstrap5 !== false) widget._bootstrap5 = true; + + if (opts.disabled) widget._disabled = true; + if (opts.highlighted) widget._highlighted = true; + if (opts.collapsed) widget._collapsed = true; + if (opts.hidden) widget._hidden = true; + // Call init. - widget.init(options); + widget.init(opts); // Call listeners. - if (options.listeners !== false) { + if (opts.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) { + // if ('undefined' === typeof opts.listeners) { // ee = node.getCurrentEventEmitter(); // } - // else if ('string' === typeof options.listeners) { - // if (options.listeners !== 'game' && - // options.listeners !== 'stage' && - // options.listeners !== 'step') { + // else if ('string' === typeof opts.listeners) { + // if (opts.listeners !== 'game' && + // opts.listeners !== 'stage' && + // opts.listeners !== 'step') { // // throw new Error('Widget.get: widget ' + widgetName + // ' has invalid value for option ' + - // 'listeners: ' + options.listeners); + // 'listeners: ' + opts.listeners); // } - // ee = node.events[options.listeners]; + // ee = node.events[opts.listeners]; // } // else { // throw new Error('Widget.get: widget ' + widgetName + - // ' options.listeners must be false, string ' + - // 'or undefined. Found: ' + options.listeners); + // ' opts.listeners must be false, string ' + + // 'or undefined. Found: ' + opts.listeners); // } // Start recording changes. @@ -481,11 +511,11 @@ } } // Remove from lastAppended. - if (node.widgets.lastAppended && - node.widgets.lastAppended.wid === this.wid) { + if (node.widgets.last && + node.widgets.last.wid === this.wid) { - node.warn('node.widgets.lastAppended destroyed.'); - node.widgets.lastAppended = null; + node.warn('node.widgets.last destroyed.'); + node.widgets.lastAppended = node.widgets.last = null; } } @@ -500,14 +530,14 @@ }; // Store widget instance (e.g., used for destruction). - if (options.storeRef !== false) this.instances.push(widget); + if (opts.storeRef !== false) this.instances.push(widget); else widget.storeRef = false; return widget; }; /** - * ### Widgets.append + * ### Widgets.append|add * * Appends a widget to the specified root element * @@ -531,6 +561,7 @@ * * @see Widgets.get */ + Widgets.prototype.add = Widgets.prototype.append = function(w, root, options) { var tmp; @@ -578,10 +609,10 @@ // Add panelDiv (with or without panel). tmp = options.panel === false ? true : w.panel === false; - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5 tmp = { - className: tmp ? [ 'ng_widget', 'no-panel', w.className ] : + className: tmp ? [ 'ng_widget', w.className ] : [ 'ng_widget', 'card', w.className ] }; } @@ -606,7 +637,7 @@ // Optionally add title (and div). if (options.title !== false && w.title) { - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5. tmp = options.panel === false ? 'no-panel-heading' : 'card-header'; @@ -621,7 +652,7 @@ } // Add body (with or without panel). - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5. tmp = options.panel !== false ? 'card-body' : 'no-panel-body'; } @@ -634,15 +665,15 @@ // Optionally add footer. if (w.footer) { - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5. tmp = options.panel === false ? 'no-panel-heading' : 'card-footer'; } else { - // Bootstrap 3. - tmp = options.panel === false ? - 'no-panel-heading' : 'panel-heading'; + // Bootstrap 3. + tmp = options.panel === false ? + 'no-panel-heading' : 'panel-heading'; } w.setFooter(w.footer); @@ -674,17 +705,11 @@ } // Store reference of last appended widget (.get method set storeRef). - if (w.storeRef !== false) this.lastAppended = w; + if (w.storeRef !== false) this.lastAppended = this.last = w; return w; }; - Widgets.prototype.add = function(w, root, options) { - console.log('***Widgets.add is deprecated. Use ' + - 'Widgets.append instead.***'); - return this.append(w, root, options); - }; - /** * ### Widgets.isWidget * @@ -727,7 +752,7 @@ for ( ; ++i < len ; ) { this.instances[0].destroy(); } - this.lastAppended = null; + this.lastAppended = this.last = null; if (this.instances.length) { node.warn('node.widgets.destroyAll: some widgets could ' + 'not be destroyed.'); @@ -854,6 +879,23 @@ return res; }; + /** + * ### Widgets.decorator + * + * Adds a callback to decorate options for all widgets + * + * @param {string} widget optional The name of the widget for which + * the options are decorated; '*' means valid for all widgets. + * @param {function} cb The callback function decorating the options. + */ + Widgets.prototype.decorator = function(widget, cb) { + if ('function' !== typeof cb) { + throw new TypeError('Widgets.decorator: cb must be function. ' + + 'Found: ' + cb); + } + this.decorators[widget] = cb + }; + // ## Helper functions // ### checkDepErrMsg diff --git a/package.json b/package.json index b967bd9..df883a4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nodegame-widgets", "description": "Collections of useful and reusable javascript / HTML snippets for nodeGame", - "version": "7.0.3", + "version": "8.0.0", "keywords": [ "nodegame", "window", "widgets", "behavioral", "game", "survey", "questionnaire" ], "author": "Stefano Balietti ", "licenses": "MIT", diff --git a/widgets/BackButton.js b/widgets/BackButton.js index cf30c79..b4e84f8 100644 --- a/widgets/BackButton.js +++ b/widgets/BackButton.js @@ -1,6 +1,6 @@ /** * # BackButton - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a button that if pressed goes to the previous step @@ -15,20 +15,14 @@ // ## Meta-data - BackButton.version = '0.4.0'; + BackButton.version = '0.5.0'; BackButton.description = 'Creates a button that if ' + 'pressed goes to the previous step.'; - BackButton.title = false; + BackButton.panel = false; BackButton.className = 'backbutton'; BackButton.texts.back = 'Back'; - // ## Dependencies - - BackButton.dependencies = { - JSUS: {} - }; - /** * ## BackButton constructor * @@ -53,8 +47,9 @@ this.button = options.button; } else if ('undefined' === typeof options.button) { - this.button = document.createElement('input'); - this.button.type = 'button'; + // this.button = document.createElement('input'); + this.button = document.createElement('button'); + // this.button.type = 'button'; } else { throw new TypeError('BackButton constructor: options.button must ' + @@ -65,6 +60,14 @@ this.button.onclick = function() { var res; that.disable(); + if (that.onclick && false === that.onclick()) return; + if (node.game.isWidgetStep()) { + // Widget has a next visualization in the same step. + if (node.widgets.last.prev() !== false) { + that.enable(); + return; + } + } res = node.game.stepBack(that.stepOptions); if (res === false) that.enable(); }; @@ -93,6 +96,18 @@ // ## @api: private. noZeroStep: true }; + + + /** + * #### BackButton.onclick + * + * A callback function executed when the button is clicked + * + * If the function returns FALSE, the procedure is aborted. + * + * Default: TRUE + */ + this.onclick = null; } // ## BackButton methods @@ -137,28 +152,30 @@ } this.button.id = tmp; - if ('undefined' === typeof opts.className) { + if ('undefined' === typeof opts.classNameBtn) { tmp = 'btn btn-lg btn-secondary'; } - else if (opts.className === false) { + else if (opts.classNameBtn === false) { tmp = ''; } - else if ('string' === typeof opts.className) { - tmp = opts.className; + else if ('string' === typeof opts.classNameBtn) { + tmp = opts.classNameBtn; } - else if (J.isArray(opts.className)) { - tmp = opts.className.join(' '); + else if (J.isArray(opts.classNameBtn)) { + tmp = opts.classNameBtn.join(' '); } else { - throw new TypeError('BackButton.init: opts.className must ' + + throw new TypeError('BackButton.init: classNameBtn must ' + 'be string, array, or undefined. Found: ' + - opts.className); + opts.classNameBtn); } this.button.className = tmp; // Button text. - this.button.value = 'string' === typeof opts.text ? - opts.text : this.getText('back'); + // this.button.value = 'string' === typeof opts.text ? + // opts.text : this.getText('back'); + this.button.innerHTML = 'string' === typeof opts.text ? + opts.text : this.getText('back'); this.stepOptions.acrossStages = 'undefined' === typeof opts.acrossStages ? @@ -166,6 +183,8 @@ this.stepOptions.acrossRounds = 'undefined' === typeof opts.acrossRounds ? true : !!opts.acrossRounds; + + setOnClick(this, opts.onclick); }; BackButton.prototype.append = function() { @@ -188,19 +207,32 @@ step = node.game.getPreviousStep(1, that.stepOptions); prop = node.game.getProperty('backbutton'); - if (!step || prop === false || - (prop && prop.enableOnPlaying === false)) { + if (prop !== true && + (!step || prop === false || + (prop && prop.enableOnPlaying === false))) { // It might be disabled already, but we do it again. that.disable(); } else { // It might be enabled already, but we do it again. - if (step) that.enable(); + if (prop === true || step) that.enable(); + } + + // if ('string' === typeof prop) that.button.value = prop; + // else if (prop && prop.text) that.button.value = prop.text; + if ('string' === typeof prop) that.button.innerHTML = prop; + else if (prop && prop.text) that.button.innerHTML = prop.text; + + if (prop) { + setOnClick(that, prop.onclick, true); + if (prop.enable) that.enable(); } + }); - if ('string' === typeof prop) that.button.value = prop; - else if (prop && prop.text) that.button.value = prop.text; + // Catch those events. + node.events.game.on('WIDGET_NEXT', function() { + that.enable(); }); }; @@ -210,7 +242,10 @@ * Disables the back button */ BackButton.prototype.disable = function() { - this.button.disabled = 'disabled'; + if (this.disabled) return; + this.disabled = true; + this.button.disabled = true; + this.emit('disabled'); }; /** @@ -219,7 +254,26 @@ * Enables the back button */ BackButton.prototype.enable = function() { + if (!this.disabled) return; + this.disabled = false; this.button.disabled = false; + this.emit('enabled'); }; + // ## Helper functions. + + // Checks and sets the onclick function. + function setOnClick(that, onclick, step) { + var str; + if ('undefined' !== typeof onclick) { + if ('function' !== typeof onclick && onclick !== null) { + str = 'BackButton.init'; + if (step) str += ' (step property)'; + throw new TypeError(str + ': onclick must be function, null,' + + ' or undefined. Found: ' + onclick); + } + that.onclick = onclick; + } + } + })(node); diff --git a/widgets/BoxSelector.js b/widgets/BoxSelector.js index fc3f99b..61fc86d 100644 --- a/widgets/BoxSelector.js +++ b/widgets/BoxSelector.js @@ -1,6 +1,6 @@ /** * # BoxSelector - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a simple box that opens a menu of items to choose from @@ -22,15 +22,8 @@ 'of items to choose from.'; BoxSelector.panel = false; - BoxSelector.title = false; BoxSelector.className = 'boxselector'; - // ## Dependencies - - BoxSelector.dependencies = { - JSUS: {} - }; - /** * ## BoxSelector constructor * diff --git a/widgets/Chat.js b/widgets/Chat.js index d5c2823..0db9232 100644 --- a/widgets/Chat.js +++ b/widgets/Chat.js @@ -1,6 +1,6 @@ /** * # Chat - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a simple configurable chat @@ -62,17 +62,10 @@ Chat.description = 'Offers a uni-/bi-directional communication interface ' + 'between players, or between players and the server.'; - Chat.title = 'Chat'; Chat.className = 'chat'; Chat.panel = false; - // ## Dependencies - - Chat.dependencies = { - JSUS: {} - }; - /** * ## Chat constructor * diff --git a/widgets/ChernoffFaces.js b/widgets/ChernoffFaces.js index 49bf548..2bdc585 100644 --- a/widgets/ChernoffFaces.js +++ b/widgets/ChernoffFaces.js @@ -1,6 +1,6 @@ /** * # ChernoffFaces - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays multidimensional data in the shape of a Chernoff Face. @@ -21,12 +21,10 @@ ChernoffFaces.description = 'Display parametric data in the form of a Chernoff Face.'; - ChernoffFaces.title = 'ChernoffFaces'; ChernoffFaces.className = 'chernofffaces'; // ## Dependencies ChernoffFaces.dependencies = { - JSUS: {}, Table: {}, Canvas: {}, SliderControls: {} diff --git a/widgets/ChernoffFacesSimple.js b/widgets/ChernoffFacesSimple.js index 2786b08..ce5508c 100644 --- a/widgets/ChernoffFacesSimple.js +++ b/widgets/ChernoffFacesSimple.js @@ -31,7 +31,6 @@ // ## Dependencies ChernoffFaces.dependencies = { - JSUS: {}, Table: {}, Canvas: {}, 'Controls.Slider': {} diff --git a/widgets/ChoiceManager.js b/widgets/ChoiceManager.js index 1f46025..98878b9 100644 --- a/widgets/ChoiceManager.js +++ b/widgets/ChoiceManager.js @@ -1,6 +1,6 @@ /** * # ChoiceManager - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates and manages a set of selectable choices forms (e.g., ChoiceTable). @@ -15,16 +15,19 @@ // ## Meta-data - ChoiceManager.version = '1.4.1'; + ChoiceManager.version = '1.9.0'; ChoiceManager.description = 'Groups together and manages a set of ' + 'survey forms (e.g., ChoiceTable).'; - ChoiceManager.title = false; ChoiceManager.className = 'choicemanager'; // ## Dependencies - ChoiceManager.dependencies = {}; + ChoiceManager.dependencies = { + BackButton: {}, DoneButton: {} + }; + + var C = 'ChoiceManager.'; /** * ## ChoiceManager constructor @@ -32,6 +35,7 @@ * Creates a new instance of ChoiceManager */ function ChoiceManager() { + /** * ### ChoiceManager.dl * @@ -103,7 +107,6 @@ */ this.groupOrder = null; - // TODO: rename in sharedOptions. /** * ### ChoiceManager.formsOptions * @@ -118,13 +121,12 @@ storeRef: false }; - /** * ### ChoiceManager.simplify * - * If TRUE, it returns getValues() returns forms.values + * If TRUE, method `ChoiceManager.getValues()` returns only forms.values * - * @see ChoiceManager.getValue + * @see ChoiceManager.getValues */ this.simplify = null; @@ -147,9 +149,109 @@ /** * ### ChoiceManager.required * - * TRUE if widget should be checked upon node.done. + * If TRUE, the widget is checked upon node.done. */ this.required = null; + + /** + * ### ChoiceManager.oneByOne + * + * If, TRUE the widget displays only one form at the time + * + * Calling node.done will display the next form. + */ + this.oneByOne = null; + + /** + * ### ChoiceManager.oneByOneCounter + * + * Index the currently displayed form if oneByOne is TRUE + */ + this.oneByOneCounter = 0; + + /** + * ### ChoiceManager.oneByOneResults + * + * Contains partial results from forms if OneByOne is true + */ + this.oneByOneResults = {}; + + /** + * ### ChoiceManager.conditionals + * + * Contains conditions to display or hide forms based on other forms + */ + this.conditionals = {}; + + /** + * ### ChoiceManager.doneBtn + * + * Button to go to the next visualization/step + */ + this.doneBtn = null; + + /** + * ### ChoiceManager.backBtn + * + * Button to go to the previous visualization/step + */ + this.backBtn = null; + + /** + * ### ChoiceManager.honeypot + * + * Array of unused input forms to detect bots. + */ + this.honeypot = null; + + /** + * ### ChoiceManager.qCounter + * + * Adds question number starting from the integer. + * + * If FALSE, no question number is added. + */ + this.qCounter = 1; + + /** + * ### ChoiceManager.qCounterSymbol + * + * The symbol used to count the questions. + */ + this.qCounterSymbol = 'Q'; + + /** + * ### ChoiceManager.qCounterCb + * + * The callback creating the question counter. + */ + this.qCounterCb = function(w, mainText, form, idx) { + return '' + + w.qCounterSymbol + w.qCounter++ + ' ' + mainText; + }; + + /** + * ### ChoiceManager.autoId + * + * If TRUE, id forms are auto-assigned if undefined + */ + this.autoId = true; + + /** + * ### ChoiceManager.delayOnNext + * + * The number of milliseconds the _next_ form is initially disabled + * + * Next and back buttons are also disabled in the process. + * + * Set to falsy to prevent this default behavior. + * + * @see ChoiceManager.next + * @see ChoiceManager.doneBtn + * @see ChoiceManager.backBtn + */ + this.delayOnNext = 350; + } // ## ChoiceManager methods @@ -186,7 +288,6 @@ else tmp = !!options.shuffleForms; this.shuffleForms = tmp; - // Set the group, if any. if ('string' === typeof options.group || 'number' === typeof options.group) { @@ -194,7 +295,7 @@ this.group = options.group; } else if ('undefined' !== typeof options.group) { - throw new TypeError('ChoiceManager.init: options.group must ' + + throw new TypeError(C + 'init: options.group must ' + 'be string, number or undefined. Found: ' + options.group); } @@ -205,7 +306,7 @@ this.groupOrder = options.groupOrder; } else if ('undefined' !== typeof options.group) { - throw new TypeError('ChoiceManager.init: options.groupOrder must ' + + throw new TypeError(C + 'init: options.groupOrder must ' + 'be number or undefined. Found: ' + options.groupOrder); } @@ -215,7 +316,7 @@ this.mainText = options.mainText; } else if ('undefined' !== typeof options.mainText) { - throw new TypeError('ChoiceManager.init: options.mainText must ' + + throw new TypeError(C + 'init: options.mainText must ' + 'be string or undefined. Found: ' + options.mainText); } @@ -223,15 +324,11 @@ // formsOptions. if ('undefined' !== typeof options.formsOptions) { if ('object' !== typeof options.formsOptions) { - throw new TypeError('ChoiceManager.init: options.formsOptions' + + throw new TypeError(C + 'init: options.formsOptions' + ' must be object or undefined. Found: ' + options.formsOptions); } - if (options.formsOptions.hasOwnProperty('name')) { - throw new Error('ChoiceManager.init: options.formsOptions ' + - 'cannot contain property name. Found: ' + - options.formsOptions); - } + this.formsOptions = J.mixin(this.formsOptions, options.formsOptions); } @@ -246,9 +343,49 @@ // If TRUE, it returns getValues returns forms.values. this.simplify = !!options.simplify; + // If TRUE, forms are displayed one by one. + this.oneByOne = !!options.oneByOne; + + // If truthy, a next button is added at the bottom. If object, it + // is passed as conf object to DoneButton. + this.doneBtn = options.doneBtn; + + // If truthy, a back button is added at the bottom. If object, it + // is passed as conf object to BackButton. + this.backBtn = options.backBtn; + + // If truthy a useless form is added to detect bots. + this.honeypot = options.honeypot; + + if ('undefined' !== typeof options.qCounter) { + this.qCounter = options.qCounter; + } + + if ('undefined' !== typeof options.qCounterSymbol) { + this.qCounterSymbol = options.qCounterSymbol; + } + + if ('undefined' !== typeof options.qCounterCb) { + this.qCounterCb = options.qCounterCb; + } + + if ('undefined' !== typeof options.autoId) { + this.autoId = options.autoId; + } + + tmp = options.delayOnNext; + if ('undefined' !== typeof tmp) { + if (J.isNumber(tmp, 0)) { + throw new TypeError('ChoiceManager.init: delayOnNext must ' + + 'be a positive number or undefined. Found: ' + tmp); + } + this.delayOnNext = tmp; + } + // After all configuration options are evaluated, add forms. if ('undefined' !== typeof options.forms) this.setForms(options.forms); + }; /** @@ -280,11 +417,11 @@ * @see ChoiceManager.buildTableAndForms */ ChoiceManager.prototype.setForms = function(forms) { - var form, formsById, i, len, parsedForms, name; + var i, formIdx, len, parsedForms; if ('function' === typeof forms) { parsedForms = forms.call(node.game); if (!J.isArray(parsedForms)) { - throw new TypeError('ChoiceManager.setForms: forms is a ' + + throw new TypeError(C + 'setForms: forms is a ' + 'callback, but did not returned an ' + 'array. Found: ' + parsedForms); } @@ -293,89 +430,37 @@ parsedForms = forms; } else { - throw new TypeError('ChoiceManager.setForms: forms must be array ' + + throw new TypeError(C + 'setForms: forms must be array ' + 'or function. Found: ' + forms); } len = parsedForms.length; if (!len) { - throw new Error('ChoiceManager.setForms: forms is an empty array.'); + throw new Error(C + 'setForms: forms is an empty array.'); } // Manual clone forms. - formsById = {}; - forms = new Array(len); - i = -1; - for ( ; ++i < len ; ) { - form = parsedForms[i]; - if (!node.widgets.isWidget(form)) { - // TODO: smart checking form name. Maybe in Stager already? - name = form.name || 'ChoiceTable'; - // Add defaults. - J.mixout(form, this.formsOptions); - form = node.widgets.get(name, form); - } - - if (form.id) { - if (formsById[form.id]) { - throw new Error('ChoiceManager.setForms: duplicated ' + - 'form id: ' + form.id); - } + this.formsById = {}; + this.forms = new Array(len); - } - else { - form.id = form.className + '_' + i; - } - forms[i] = form; - formsById[form.id] = forms[i]; - - if (form.required || form.requiredChoice || form.correctChoice) { - // False is set manually, otherwise undefined. - if (this.required === false) { - throw new Error('ChoiceManager.setForms: required is ' + - 'false, but form "' + form.id + - '" has required truthy'); - } - this.required = true; - } - } - // Assigned verified forms. - this.forms = forms; - this.formsById = formsById; - - // Save the order in which the choices will be added. + // Shuffle, if needed. this.order = J.seq(0, len-1); if (this.shuffleForms) this.order = J.shuffle(this.order); - }; - - /** - * ### ChoiceManager.buildDl - * - * Builds the list of all forms - * - * Must be called after forms have been set already. - * - * @see ChoiceManager.setForms - * @see ChoiceManager.order - */ - ChoiceManager.prototype.buildDl = function() { - var i, len, dt; - var form; - i = -1, len = this.forms.length; + i = -1; for ( ; ++i < len ; ) { - dt = document.createElement('dt'); - dt.className = 'question'; - form = this.forms[this.order[i]]; - node.widgets.append(form, dt); - this.dl.appendChild(dt); + formIdx = this.order[i]; + this.addForm(parsedForms[formIdx], false, i); + // Save the order in which the choices will be added. } }; ChoiceManager.prototype.append = function() { + var div, opts; + // Id must be unique. if (W.getElementById(this.id)) { - throw new Error('ChoiceManager.append: id is not ' + + throw new Error(C + 'append: id is not ' + 'unique: ' + this.id); } @@ -389,9 +474,8 @@ } // Dl. - this.dl = document.createElement('dl'); - this.buildDl(); - // Append Dl. + this.dl = buildDL(this); + // Append it. this.bodyDiv.appendChild(this.dl); // Creates a free-text textarea, possibly with placeholder text. @@ -405,6 +489,27 @@ // Append textarea. this.bodyDiv.appendChild(this.textarea); } + + if (this.backBtn || this.doneBtn) { + div = W.append('div', this.bodyDiv); + div.className = 'choicemanager-buttons'; + + if (this.backBtn) { + opts = this.backBtn; + if ('string' === typeof opts) opts = { text: opts }; + else opts = J.mixin({ text: 'Back' }, opts); + this.backBtn = node.widgets.append('BackButton', div, opts); + } + + if (this.doneBtn) { + opts = this.doneBtn; + if ('string' === typeof opts) opts = { text: opts }; + opts = J.mixin({ text: 'Next' }, opts); + this.doneBtn = node.widgets.append('DoneButton', div, opts); + } + } + + if (this.honeypot) this.addHoneypot(this.honeypot); }; /** @@ -443,6 +548,105 @@ this.emit('disabled'); }; + /** + * ### ChoiceManager.addForm + * + * Adds a new form at the bottom. + */ + ChoiceManager.prototype.addForm = function(form, scrollIntoView, idx) { + var name; + + if ('undefined' === typeof idx) idx = this.forms.length; + if ('undefined' === typeof scrollIntoView) scrollIntoView = true; + + if (!node.widgets.isWidget(form)) { + + // Add defaults. + J.mixout(form, this.formsOptions); + + + if (!form.id && this.autoId) { + name = this.autoId === true ? + node.game.getStepId() : this.autoId; + form.id = name + '_' + (idx + 1); + } + + // By default correctChoice means required. + // However, it is possible to add required = false and correctChoice + // truthy, for instance if there is a solution to display. + if ((form.required !== false && form.requiredChoice !== false) && + (form.required || form.requiredChoice || + ('undefined' !== typeof form.correctChoice && + form.correctChoice !== false))) { + + // False is set manually, otherwise undefined. + if (this.required === false) { + throw new Error(C + 'setForms: required is ' + + 'false, but form "' + form.id + + '" has required truthy'); + } + this.required = true; + } + + // Display forms one by one. + if (this.oneByOne && this.oneByOneCounter !== idx) { + form.hidden = true; + } + + if (form.conditional) { + this.conditionals[form.id] = form.conditional; + } + + if (this._bootstrap5 && 'undefined' === typeof form.bootstrap5) { + form.bootstrap5 = true; + } + + if (this.qCounter !== false) { + if (form.mainText && !form.qCounterAdded) { + form.mainText = + this.qCounterCb(this, form.mainText, form, idx); + form.qCounterAdded = true; + } + } + + // TODO: smart checking form name. Maybe in Stager already? + name = form.name || 'ChoiceTable'; + + form = node.widgets.get(name, form); + + } + + if (form.id) { + if (this.formsById[form.id]) { + throw new Error(C + 'setForms: duplicated form id: ' + form.id); + } + + } + else { + form.id = form.className + '_' + idx; + } + this.forms[idx] = form; + this.formsById[form.id] = form; + + if (this.dl) { + + // Add the last added form to the order array. + this.order.push(this.order.length); + + appendDT(this.dl, form); + W.adjustFrameHeight(); + if (!scrollIntoView) return; + // Scroll into the slider. + if ('function' === typeof form.bodyDiv.scrollIntoView) { + form.bodyDiv.scrollIntoView({ behavior: 'smooth' }); + } + else if (window.scrollTo) { + // Scroll to bottom of page. + window.scrollTo(0, document.body.scrollHeight); + } + } + }; + /** * ### ChoiceManager.enable * @@ -539,7 +743,7 @@ */ ChoiceManager.prototype.highlight = function(border) { if (border && 'string' !== typeof border) { - throw new TypeError('ChoiceManager.highlight: border must be ' + + throw new TypeError(C + 'highlight: border must be ' + 'string or undefined. Found: ' + border); } if (!this.dl || this.highlighted === true) return; @@ -592,13 +796,16 @@ * to find the correct answer. Default: TRUE. * - highlight: If TRUE, forms that do not have a correct value * will be highlighted. Default: TRUE. + * - simplify: If TRUE, forms are not nested under `.forms`, but + * available at the first level. Duplicated keys will be overwritten. + * TODO: rename "flatten." * * @return {object} Object containing the choice and paradata * * @see ChoiceManager.verifyChoice */ ChoiceManager.prototype.getValues = function(opts) { - var obj, i, len, form, lastErrored, res; + var obj, i, len, form, lastErrored, res, toCheck; obj = { order: this.order, forms: {}, @@ -609,39 +816,72 @@ if ('undefined' === typeof opts.markAttempt) opts.markAttempt = true; if ('undefined' === typeof opts.highlight) opts.highlight = true; if (opts.markAttempt) obj.isCorrect = true; - i = -1, len = this.forms.length; - for ( ; ++i < len ; ) { - form = this.forms[i]; - // If it is hidden or disabled we do not do validation. - if (form.isHidden() || form.isDisabled()) { - res = form.getValues({ - markAttempt: false, - highlight: false - }); - if (res) obj.forms[form.id] = res; - } - else { - // ContentBox does not return a value. - res = form.getValues(opts); - if (!res) continue; - obj.forms[form.id] = res; - // Backward compatible (requiredChoice). - if ((form.required || form.requiredChoice) && - (obj.forms[form.id].choice === null || - (form.selectMultiple && - !obj.forms[form.id].choice.length))) { - - obj.missValues.push(form.id); - lastErrored = form; - } - if (opts.markAttempt && - obj.forms[form.id].isCorrect === false) { - // obj.isCorrect = false; - lastErrored = form; + len = this.forms.length; + + + // TODO: we could save the results when #next() is called or + // have an option to get the values of current form or a specific form. + // The code below is a old and created before #next() was created. + // Only one form displayed. + // if (this.oneByOne) { + // + // // Evaluate one-by-one and store partial results. + // if (this.oneByOneCounter < (len-1)) { + // form = this.forms[this.oneByOneCounter]; + // res = form.getValues(opts); + // if (res) { + // this.oneByOneResults[form.id] = res; + // lastErrored = checkFormResult(res, form, opts); + // + // if (!lastErrored) { + // this.forms[this.oneByOneCounter].hide(); + // this.oneByOneCounter++; + // this.forms[this.oneByOneCounter].show(); + // W.adjustFrameHeight(); + // // Prevent stepping. + // obj.isCorrect = false; + // } + // } + // } + // // All one-by-one pages executed. + // else { + // // Copy all partial results in the obj returning the + // obj.forms = this.oneByOneResults; + // } + // + // } + // All forms on the page. + // else { + i = -1; + for ( ; ++i < len ; ) { + form = this.forms[i]; + + // Not one-by-one because there could be many hidden. + // If it is hidden or disabled we do not do validation. + + if (this.oneByOne) toCheck = form._shown && form.required; + else toCheck = !(form.isDisabled() || form.isHidden()); + + if (toCheck) { + // ContentBox does not return a value. + res = form.getValues(opts); + if (!res) continue; + obj.forms[form.id] = res; + + res = checkFormResult(res, form, opts, obj); + if (res) lastErrored = res; + } + else { + res = form.getValues({ + markAttempt: false, + highlight: false + }); + if (res) obj.forms[form.id] = res; } } - } + // } + if (lastErrored) { if (opts.highlight && 'function' === typeof lastErrored.bodyDiv.scrollIntoView) { @@ -658,12 +898,20 @@ if (this.textarea) obj.freetext = this.textarea.value; // Simplify everything, if requested. - if (opts.simplify || this.simplify) { + if (opts.simplify === true || this.simplify) { res = obj; obj = obj.forms; if (res.isCorrect === false) obj.isCorrect = false; if (res.freetext) obj.freetext = res.freetext; } + + if (this.honeypot) { + obj.honeypotHit = 0; + obj.honeypot = this.honeypot.map(function(h) { + if (h.value) obj.honeypotHit++; + return h.value || false; + }); + } return obj; }; @@ -679,7 +927,7 @@ ChoiceManager.prototype.setValues = function(opts) { var i, len; if (!this.forms || !this.forms.length) { - throw new Error('ChoiceManager.setValues: no forms found.'); + throw new Error(C + 'setValues: no forms found.'); } opts = opts || {}; i = -1, len = this.forms.length; @@ -691,8 +939,291 @@ if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); }; + /** + * ### ChoiceManager.addHoneypot + * + * Adds a hidden tag with nested that bots should fill + * + * The inputs created are added under ChoiceManager.honeypot + * + * @param {object} opts Optional. Options to configure the honeypot. + * - id: id of the tag + * - action: action attribute of the tag + * - forms: array of forms to add to the tag. Format: + * - id: id of input and "for" attribute of the label + * - label: text of the label + * - placeholder: placeholder for the input + * - type: type of input (default 'text') + */ + ChoiceManager.prototype.addHoneypot = function(opts) { + var h, forms, that; + if (!this.isAppended()) { + node.warn(C + 'addHoneypot: not appended yet'); + return; + } + if ('object' !== typeof opts) opts = {}; + h = W.add('form', this.panelDiv, { + id: opts.id || (this.id + 'form'), + action: opts.action || ('/' + this.id + 'receive') + }); + + h.style.opacity = 0; + h.style.position = 'absolute'; + h.style.top = 0; + h.style.left = 0; + h.style.height = 0; + h.style.width = 0; + h.style['z-index'] = -1; + + if (!opts.forms) { + forms = [ + { id: 'name', label: 'Your name', + placeholder: 'Enter your name' }, + { id: 'email', label: 'Your email', + placeholder: 'Type your email', type: 'email' } + ]; + } + else { + forms = opts.forms; + } + + // Change from options to array linking to honeypot inputs. + this.honeypot = []; + + that = this; + forms.forEach(function(f) { + var hh; + W.add('label', h, { 'for': f.id }); + hh = W.add('input', h, { + id: f.id, + type: f.type || 'text', + placeholder: f.placeholder, + required: true, + autocomplete: 'off' + }); + that.honeypot.push(hh); + }); + }; + + /** + * ### ChoiceManager.next + * + * Sets values for forms in manager as specified by the options + * + * @return {boolean} FALSE, if there is not another visualization. + */ + ChoiceManager.prototype.next = function() { + var form, conditional, failsafe, that; + if (!this.oneByOne) return false; + if (!this.forms || !this.forms.length) { + throw new Error(C + 'next: no forms found.'); + } + form = this.forms[this.oneByOneCounter]; + if (!form) return false; + + if (form.next()) return true; + if (this.oneByOneCounter >= (this.forms.length-1)) return false; + + form.hide(); + if (this.backBtn) this.backBtn.disable(); + if (this.doneBtn) this.doneBtn.disable(); + + failsafe = 500; + while (form && !conditional && this.oneByOneCounter < failsafe) { + form = this.forms[++this.oneByOneCounter]; + if (!form) return false; + conditional = checkConditional(this, form.id); + } + + // TODO: make this property a reserved keyword. + form._shown = true; + + // Delay the activation of the form to prevent accidental clicking. + if (this.delayOnNext) form.disable(); + + if ('undefined' !== typeof $) { + $(form.panelDiv).fadeIn(); + form.hidden = false; // for nodeGame. + } + else { + form.show(); + } + window.scrollTo(0,0); + + if (this.delayOnNext) { + that = this; + setTimeout(function() { + if (node.game.isPaused()) return; + form.enable(); + if (that.backBtn) that.backBtn.enable(); + if (that.doneBtn) that.doneBtn.enable(); + }, this.delayOnNext); + } + + W.adjustFrameHeight(); + + node.emit('WIDGET_NEXT', this); + + return true; + }; + + ChoiceManager.prototype.prev = function() { + var form, conditional, failsafe; + if (!this.oneByOne) return false; + if (!this.forms || !this.forms.length) { + throw new Error(C + 'prev: no forms found.'); + } + form = this.forms[this.oneByOneCounter]; + if (!form) return false; + if (form.prev()) return true; + if (this.oneByOneCounter <= 0) return false; + form.hide(); + + failsafe = 500; + while (form && !conditional && this.oneByOneCounter < failsafe) { + form = this.forms[--this.oneByOneCounter]; + if (!form) return false; + conditional = checkConditional(this, form.id); + } + + if ('undefined' !== typeof $) { + $(form.panelDiv).fadeIn(); + form.hidden = false; // for nodeGame. + } + else { + form.show(); + } + window.scrollTo(0,0); + + W.adjustFrameHeight(); + node.emit('WIDGET_PREV', this); + + return true; + }; + + // TODO: better to have .getForms({ hidden: false }); or similar + ChoiceManager.prototype.getVisibleForms = function() { + if (this.oneByOne) return [this.forms[this.oneByOneCounter]]; + return this.forms.map(function(f) { if (!f.isHidden()) return f; }); + }; + // ## Helper methods. + /** + * ### checkFormResult + * + * Checks if the values returned by a form are valid + * + * @param {object} res The values returned by a form + * @param {object} form The form object + * @param {object} opts Configuration options changing the checking behavior + * @param {object} out Optional The object returned by + * `ChoiceManager.getValues()` + * + * @return {bool} TRUE, if conditions for display are met + * + * @see ChoiceManager.getValues + */ + function checkFormResult(res, form, opts, out) { + var err; + // Backward compatible (requiredChoice). + if ((form.required || form.requiredChoice) && + (res.choice === null || + (form.selectMultiple && !res.choice.length))) { + + if (out) out.missValues.push(form.id); + err = form; + } + if (opts.markAttempt && res.isCorrect === false) { + // out.isCorrect = false; + err = form; + } + + return err; + } + + /** + * ### checkConditional + * + * Checks if the conditions for the display of a form are met + * + * @param {ChoiceManager} w This widget instance + * @param {string} form The id of the conditional to check + * + * @return {bool} TRUE, if conditions for display are met + * + * @see ChoiceManager.conditionals + */ + function checkConditional(w, id) { + var f, c, form; + f = w.conditionals[id]; + if (f) { + if ('function' === typeof f) { + return f.call(w, w.formsById); + } + for (c in f) { + if (f.hasOwnProperty(c)) { + form = w.formsById[c]; + if (!form) continue; + // No multiple choice allowed. + if (J.isArray(f[c])) { + if (!J.inArray(form.currentChoice, f[c])) return false; + } + else if (form.currentChoice !== f[c]) { + return false; + } + } + } + } + return true; + } + + /** + * ### buildDL + * + * Builds the list of all forms + * + * Must be called after forms have been set already. + * + * @param {ChoiceManager} w This widget instance + * + * @return {HTMLElement} The
HTML element + * + * @see ChoiceManager.setForms + * @see ChoiceManager.order + * @see appendDT + */ + function buildDL(w) { + var i, len, form, dl; + dl = document.createElement('dl'); + i = -1, len = w.forms.length; + for ( ; ++i < len ; ) { + // If shuffled, w.forms already follows the shuffled order. + form = w.forms[i]; + // form = w.forms[w.order[i]]; + appendDT(dl, form); + } + return dl; + } + + /** + * ### appendDT + * + * Creates a
, adds a widget to it, and
to a
+ * + * @param {HTMLElement} dl The
HTML element + * @param {object} form The widget settings to create a new form + * + * @see buildDL + */ + function appendDT(dl, form) { + var dt; + dt = document.createElement('dt'); + dt.className = 'question'; + node.widgets.add(form, dt); + dl.appendChild(dt); + } + // In progress. // const createOnClick = (choice, question) => { // return function(value, removed, td) { diff --git a/widgets/ChoiceTable.js b/widgets/ChoiceTable.js index c970dc6..1e8c2fb 100644 --- a/widgets/ChoiceTable.js +++ b/widgets/ChoiceTable.js @@ -1,6 +1,6 @@ /** * # ChoiceTable - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2024 Stefano Balietti * MIT Licensed * * Creates a configurable table where each cell is a selectable choice @@ -17,11 +17,10 @@ // ## Meta-data - ChoiceTable.version = '1.8.1'; + ChoiceTable.version = '1.11.0'; ChoiceTable.description = 'Creates a configurable table where ' + 'each cell is a selectable choice.'; - ChoiceTable.title = 'Make your choice'; ChoiceTable.className = 'choicetable'; ChoiceTable.texts = { @@ -29,7 +28,9 @@ autoHint: function(w) { var res; if (!w.requiredChoice && !w.selectMultiple) return false; - if (!w.selectMultiple) return '*'; + if (!w.selectMultiple) { + return w.displayRequired ? w.requiredMark : false; + } res = '('; if (!w.requiredChoice) { if ('number' === typeof w.selectMultiple) { @@ -54,9 +55,12 @@ } } res += ')'; - if (w.requiredChoice) res += ' *'; + if (w.requiredChoice && w.displayRequired) { + res += ' ' + w.requiredMark; + } return res; }, + error: function(w, value) { if (value !== null && ('number' === typeof w.correctChoice || @@ -65,18 +69,16 @@ return 'Not correct, try again.'; } return 'Selection required.'; - } - // correct: 'Correct.' - }; + }, - ChoiceTable.separator = '::'; + other: 'Other', - // ## Dependencies + customInput: 'Please specify.' - ChoiceTable.dependencies = { - JSUS: {} }; + ChoiceTable.separator = '::'; + /** * ## ChoiceTable constructor * @@ -120,21 +122,26 @@ * @see ChoiceTable.onclick */ this.listener = function(e) { - var name, value, td, tr; - var i, len, removed; + var value, td, ci; + var i, len, removed, otherSel; e = e || window.event; td = e.target || e.srcElement; // See if it is a clickable choice. - if ('undefined' === typeof that.choicesIds[td.id]) { + ci = that.choicesIds; + if ('undefined' === typeof ci[td.id]) { // It might be a nested element, try the parent. td = td.parentNode; if (!td) return; - if ('undefined' === typeof that.choicesIds[td.id]) { + if ('undefined' === typeof ci[td.id]) { td = td.parentNode; - if (!td || 'undefined' === typeof that.choicesIds[td.id]) { - return; + if (!td) return; + if ('undefined' === typeof ci[td.id]) { + td = td.parentNode; + if (!td || 'undefined' === typeof ci[td.id]) { + return; + } } } } @@ -166,8 +173,27 @@ // One more click. that.numberOfClicks++; + removed = that.isChoiceCurrent(value); + len = that.choices.length; + + if (that.customInput) { + // Is "Other" currently selected? + otherSel = value === (len - 1); + + if (otherSel && !removed && + // Fixed Select multiple (not all max choices selected). + ('number' !== typeof that.selectMultiple || + (that.selectMultiple > that.currentChoice.length)) + ) { + that.customInput.show(); + } + else if (!that.selectMultiple || otherSel) { + that.customInput.hide(); + } + } + // Click on an already selected choice. - if (that.isChoiceCurrent(value)) { + if (removed) { that.unsetCurrentChoice(value); J.removeClass(td, 'selected'); @@ -184,7 +210,6 @@ else { that.selected = null; } - removed = true; } // Click on a new choice. else { @@ -227,6 +252,10 @@ value = parseInt(value, 10); that.onclick.call(that, value, removed, td); } + + that.lastClicked = value; + + if (that.doneOnClick) node.done(); }; /** @@ -340,6 +369,15 @@ */ this.rightCell = null; + /** + * ### ChoiceTable.header + * + * Header to be displayed above the table + * + * @experimental + */ + this.header = null; + /** * ### ChoiceTable.errorBox * @@ -438,6 +476,28 @@ */ this.currentChoice = null; + /** + * ### ChoiceTable.defaultChoice + * + * Choice/s initially selected when the widget is inited + * + * @see ChoiceTable.selectMultiple + * + * @see ChoiceTable.selected + */ + this.defaultChoice = null; + + /** + * ### ChoiceTable._initDefaultChoice + * + * Flags that default choices still need to be added + * + * @see ChoiceTable.defaultChoice + * + * @api private + */ + this._initDefaultChoice = null; + /** * ### ChoiceTable.selectMultiple * @@ -557,9 +617,88 @@ /** * ### ChoiceTable.sameWidthCells * - * If TRUE, cells have same width regardless of content + * If truthy, it forces cells to have same width regardless of content + * + * - If TRUE, it automatically computes the equal size of the cells + * (options `left` and `right` affect computation). + * - If string, it is the value of width for all cells + * + * Only applies in horizontal mode. */ this.sameWidthCells = true; + + /** + * ### ChoiceTable.other + * + * If TRUE, adds an "Other" choice as last choice + * + * Accepted values: + * - true: adds "Other" choice as last choice. + * - 'CustomInput': adds "Other" choice AND a CustomInput widget below + * the choicetable (initially hidden). + * - object: as previous, but it also allows for custom options for the + * custom input + * + * @see ChoiceTable.customInput + */ + this.other = null; + + /** + * ### ChoiceTable.customInput + * + * The customInput widget + * + * @see ChoiceTable.other + */ + this.customInput = null; + + /** + * ### ChoiceTable.lastClicked + * + * The idx of the last selected choice + */ + this.lastClicked = null; + + /** + * ### ChoiceTable.doneOnClick + * + * If TRUE, node.done() will be invoked after the first click + */ + this.doneOnClick = null; + + /** + * ### ChoiceTable.solution + * + * Additional information to be displayed after a selection is confirmed + * + * If no answer is provided and the next method is triggered, the + * solution is displayed only if solutionNoChoice is TRUE + * + * @see ChoiceTable.solutionNoChoice + * @see ChoiceTable.next + */ + this.solution = null; + + /** + * ### ChoiceTable.solutionDisplayed + * + * TRUE, if the solution is currently displayed + */ + this.solutionDisplayed = false; + + /** + * ### ChoiceTable.solutionNoChoice + * + * TRUE, he solution is displayed upon trigger even with no choice + */ + this.solutionNoChoice = false; + + /** + * ### ChoiceTable.solutionDiv + * + * The
element containing the solution + */ + this.solutionDiv = null; } // ## ChoiceTable methods @@ -604,7 +743,7 @@ * @param {object} opts Configuration options */ ChoiceTable.prototype.init = function(opts) { - var tmp, that; + var tmp, that, i; that = this; if (!this.id) { @@ -734,24 +873,44 @@ } // Set the mainText, if any. - if ('string' === typeof opts.mainText) { - this.mainText = opts.mainText; + tmp = opts.mainText + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp) { + throw new TypeError('ChoiceTable.init: opts.mainText cb ' + + 'must return a string. Found: ' + + tmp); + } } - else if ('undefined' !== typeof opts.mainText) { + if ('string' === typeof tmp) { + this.mainText = tmp; + } + else if ('undefined' !== typeof tmp) { throw new TypeError('ChoiceTable.init: opts.mainText must ' + - 'be string or undefined. Found: ' + - opts.mainText); + 'be function, string or undefined. Found: ' + + tmp); } // Set the hint, if any. - if ('string' === typeof opts.hint || false === opts.hint) { - this.hint = opts.hint; - if (this.requiredChoice) this.hint += ' *'; + tmp = opts.hint; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp && false !== tmp) { + throw new TypeError('ChoiceTable.init: opts.hint cb must ' + + 'return string or false. Found: ' + + tmp); + } } - else if ('undefined' !== typeof opts.hint) { + if ('string' === typeof tmp || false === tmp) { + this.hint = tmp; + if (this.requiredChoice && tmp !== false && this.displayRequired) { + this.hint += ' ' + this.requiredMark; + } + } + else if ('undefined' !== typeof tmp) { throw new TypeError('ChoiceTable.init: opts.hint must ' + 'be a string, false, or undefined. Found: ' + - opts.hint); + tmp); } else { // Returns undefined if there are no constraints. @@ -791,10 +950,18 @@ 'separator option. Found: ' + this.separator); } - if ('string' === typeof opts.left || - 'number' === typeof opts.left) { - - this.left = '' + opts.left; + // left. + tmp = opts.left; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp && 'undefined' !== typeof tmp) { + throw new TypeError('ChoiceTable.init: opts.left cb must ' + + 'return string or undefined. Found: ' + + tmp); + } + } + if ('string' === typeof tmp || 'number' === typeof tmp) { + this.left = '' + tmp; } else if (J.isNode(opts.left) || J.isElement(opts.left)) { @@ -802,19 +969,24 @@ this.left = opts.left; } else if ('undefined' !== typeof opts.left) { - throw new TypeError('ChoiceTable.init: opts.left must ' + - 'be string, number, an HTML Element or ' + - 'undefined. Found: ' + opts.left); + throw new TypeError('ChoiceTable.init: opts.left must be string, ' + + 'number, function, an HTML Element or ' + + 'undefined. Found: ' + tmp); + } + + tmp = opts.right; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp && 'undefined' !== typeof tmp) { + throw new TypeError('ChoiceTable.init: opts.right cb must ' + + 'return string or undefined. Found: ' + + tmp); + } } - - if ('string' === typeof opts.right || - 'number' === typeof opts.right) { - - this.right = '' + opts.right; + if ('string' === typeof tmp || 'number' === typeof tmp) { + this.right = '' + tmp; } - else if (J.isNode(opts.right) || - J.isElement(opts.right)) { - + else if (J.isNode(opts.right) || J.isElement(opts.right)) { this.right = opts.right; } else if ('undefined' !== typeof opts.right) { @@ -876,11 +1048,14 @@ // Add the correct choices. - if ('undefined' !== typeof opts.choicesSetSize) { - if (!J.isInt(opts.choicesSetSize, 0)) { + tmp = opts.choicesSetSize; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + } + if ('undefined' !== typeof tmp) { + if (!J.isInt(tmp, 0)) { throw new Error('ChoiceTable.init: choicesSetSize must be ' + - 'undefined or an integer > 0. Found: ' + - opts.choicesSetSize); + 'undefined or an integer > 0. Found: ' + tmp); } if (this.left || this.right) { @@ -889,47 +1064,132 @@ 'right options are set.'); } - this.choicesSetSize = opts.choicesSetSize; + this.choicesSetSize = tmp; + } + + // Add other. + if ('undefined' !== typeof opts.sameWidthCells) { + this.sameWidthCells = opts.sameWidthCells; + } + + // Add other. + if ('undefined' !== typeof opts.other) { + this.other = opts.other; } // Add the choices. - if ('undefined' !== typeof opts.choices) { - this.setChoices(opts.choices); + tmp = opts.choices; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if (!J.isArray(tmp) || !tmp.length) { + throw new TypeError('ChoiceTable.init: opts.choices cb must ' + + 'return a non-empty array. Found: ' + tmp); + } + } + if ('undefined' !== typeof tmp) { + this.setChoices(tmp); } // Add the correct choices. - if ('undefined' !== typeof opts.correctChoice) { + tmp = opts.correctChoice; + if ('undefined' !== typeof tmp) { if (this.requiredChoice) { - throw new Error('ChoiceTable.init: cannot specify both ' + - 'opts requiredChoice and correctChoice'); + this.requiredChoice = null; + this.required = null; + node.warn('ChoiceTable.init: requiredChoice and ' + + 'correctChoice are both set; requiredChoice ignored.' + ); + } + if ('function' === typeof tmp) { + tmp = tmp.call(this); + // No checks. } this.setCorrectChoice(opts.correctChoice); } // Add the correct choices. - if ('undefined' !== typeof opts.disabledChoices) { + tmp = opts.disabledChoices; + if ('undefined' !== typeof tmp) { + if ('function' === typeof tmp) { + tmp = tmp.call(this); + } if (!J.isArray(opts.disabledChoices)) { - throw new Error('ChoiceTable.init: disabledChoices must be ' + - 'undefined or array. Found: ' + - opts.disabledChoices); + throw new TypeError('ChoiceTable.init: disabledChoices ' + + 'must be undefined or array. Found: ' + + tmp); } // TODO: check if values of disabled choices are correct? // Do we have the choices now, or can they be added later? - tmp = opts.disabledChoices.length; if (tmp) { (function() { - for (var i = 0; i < tmp; i++) { - that.disableChoice(opts.disabledChoices[i]); + for (i = 0; i < tmp.length; i++) { + that.disableChoice(tmp[i]); } })(); } } - if ('undefined' === typeof opts.sameWidthCells) { - this.sameWidthCells = !!opts.sameWidthCells; + if ('undefined' !== typeof opts.doneOnClick) { + this.doneOnClick = !!opts.doneOnClick; + } + + tmp = opts.solution; + if ('undefined' !== typeof tmp) { + if ('string' !== typeof tmp && 'function' !== typeof tmp) { + throw new TypeError('ChoiceTable.init: solution must be ' + + 'string or undefined. Found: ' + tmp); + } + this.solution = tmp; + } + + tmp = opts.defaultChoice; + if ('undefined' !== typeof tmp) { + this.defaultChoice = tmp; + initDefaultChoice(this); + } + + if (opts.header) { + tmp = opts.header; + // One td will colspan all choices. + if ('string' === typeof tmp) { + tmp = [ tmp ]; + } + else if (!J.isArray(tmp) || + (tmp.length !== 1 && tmp.length !== opts.choices.length)) { + + throw new Error('ChoiceTableGroup.init: header ' + + 'must be string, array (size ' + + opts.choices.length + + '), or undefined. Found: ' + tmp); + } + + this.header = tmp; + } + + }; + + /** + * ### ChoiceTable.clickChoice + * + * Clicks on a choice + * + * @param {string|number} idx The idx of the choice to click on + */ + ChoiceTable.prototype.clickChoice = function(idx) { + if (!this.choicesCells) { + throw new Error('ChoiceTable.clickChoice: choicesCells not ' + + 'initialized.'); + } + if (J.isInt(idx) === false) { + throw new TypeError('ChoiceTable.clickChoice: idx must be ' + + 'integer. Found: ' + idx); + } + if (!this.choicesCells[idx]) { + throw new Error('ChoiceTable.clickChoice: idx not found: ' + idx); } + this.choicesCells[idx].click(); }; /** @@ -937,10 +1197,13 @@ * * Marks a choice as disabled (will not be clickable) * - * @param {string|number} value The value of the choice to disable` + * @param {string|number} idx The idx of the choice to disable */ - ChoiceTable.prototype.disableChoice = function(value) { - this.disabledChoices[value] = true; + ChoiceTable.prototype.disableChoice = function(idx) { + if (!this.disabledChoices[idx]) { + this.disabledChoices[idx] = true; + J.addClass(this.choicesCells[idx], 'disabled'); + } }; /** @@ -948,10 +1211,13 @@ * * Enables a choice (will be clickable again if previously disabled) * - * @param {string|number} value The value of the choice to disable` + * @param {string|number} idx The value of the choice to disable */ - ChoiceTable.prototype.enableChoice = function(value) { - this.disabledChoices[value] = null; + ChoiceTable.prototype.enableChoice = function(idx) { + if (this.disabledChoices[idx]) { + this.disabledChoices[idx] = null; + J.removeClass(this.choicesCells[idx], 'disabled'); + } }; /** @@ -971,7 +1237,7 @@ * @see ChoiceTable.buildTableAndChoices */ ChoiceTable.prototype.setChoices = function(choices) { - var len; + var len, idxOther; if (!J.isArray(choices)) { throw new TypeError('ChoiceTable.setChoices: choices ' + 'must be array'); @@ -979,6 +1245,11 @@ if (!choices.length) { throw new Error('ChoiceTable.setChoices: choices array is empty'); } + // Check and drop previous "other" choices. + if (this.other) { + idxOther = choices.indexOf(this.getText('other')); + if (idxOther >= 0) choices.splice(idxOther, 1); + } this.choices = choices; len = choices.length; @@ -986,6 +1257,48 @@ this.order = J.seq(0, len-1); if (this.shuffleChoices) this.order = J.shuffle(this.order); + // Loop through all choices and see if there is any fixed position. + // TODO: we could add validation here. + (function(w) { + var i, c, fixedPos, idxOrder, allFixedPos = [], allFixedLen; + // See if there is any fixed-choice. + for (i = -1 ; ++i < len ; ) { + fixedPos = undefined; + idxOrder = w.order[i]; + c = choices[idxOrder]; + if (J.isArray(c)) { + // Third position after id and text is fixedPos. + fixedPos = c[2]; + } + else if ('object' === typeof choices[i]) { + fixedPos = c.fixedPos; + } + if ('undefined' !== typeof fixedPos) { + allFixedPos.push({ fixed: fixedPos, pos: i, idx: idxOrder}); + } + } + // All fixed position collected, we need to sort them from + // lowest to highest, then we can do the placing. + allFixedLen = allFixedPos.length; + if (allFixedLen) { + if (allFixedLen > 1) { + allFixedPos.sort(function(a, b) {return a.fixed < b.fixed}); + } + for (i = -1 ; ++i < allFixedLen ; ) { + c = allFixedPos[i]; + // Remove from old position and place it in new one. + w.order.splice(c.pos, 1); + w.order.splice(c.fixed, 0, c.idx); + } + } + })(this) + + // Add 'Other' field at the end. + if (this.other) { + this.choices[len] = this.getText('other'); + this.order[len] = len + } + // Build the table and choices at once (faster). if (this.table) this.buildTableAndChoices(); // Or just build choices. @@ -1006,12 +1319,13 @@ * @see ChoiceTable.renderSpecial */ ChoiceTable.prototype.buildChoices = function() { - var i, len; - i = -1, len = this.choices.length; + var len, pos, idx; + pos = -1, len = this.choices.length; // Pre-allocate the choicesCells array. this.choicesCells = new Array(len); - for ( ; ++i < len ; ) { - this.renderChoice(this.choices[this.order[i]], i); + for ( ; ++pos < len ; ) { + idx = this.order[pos]; + this.renderChoice(this.choices[idx], idx, pos); } if (this.left) this.renderSpecial('left', this.left); if (this.right) this.renderSpecial('right', this.right); @@ -1033,10 +1347,33 @@ ChoiceTable.prototype.buildTable = (function() { function makeSet(i, len, H, doSets) { - var tr, counter; + var tr, td, counter, pos; counter = 0; // Start adding tr/s and tds based on the orientation. if (H) { + + if (this.header) { + tr = W.add('tr', this.table); + + // Add empty left header cell, if needed. + if (this.left) W.add('td', tr, { className: 'header' }); + + for ( ; ++i < this.header.length ; ) { + td = W.add('td', tr, { + innerHTML: this.header[i], + className: 'header' + }); + } + + // Only one element, header spans throughout. + if (i === 1) td.setAttribute('colspan', this.choices.length); + + // Add empty right header cell, if needed. + if (this.right) W.add('td', tr, { className: 'header' }); + + i = -1; + } + tr = createTR(this, 'main'); // Add horizontal choices title. if (this.leftCell) tr.appendChild(this.leftCell); @@ -1052,7 +1389,8 @@ } } // Clickable cell. - tr.appendChild(this.choicesCells[i]); + pos = this.order[i]; + tr.appendChild(this.choicesCells[pos]); // Stop if we reached set size (still need to add the right). if (doSets && ++counter >= this.choicesSetSize) break; } @@ -1096,7 +1434,7 @@ * @see ChoiceTable.orientation */ ChoiceTable.prototype.buildTableAndChoices = function() { - var i, len, tr, td, H; + var i, idx, len, tr, td, H; len = this.choices.length; // Pre-allocate the choicesCells array. @@ -1125,7 +1463,8 @@ } } // Clickable cell. - td = this.renderChoice(this.choices[this.order[i]], i); + idx = this.order[i]; + td = this.renderChoice(this.choices[idx], idx, i); tr.appendChild(td); } if (this.right) { @@ -1196,7 +1535,7 @@ * text to display as choice, or an object with properties value and * display. If a renderer function is defined there are no restriction * on the format of choice. - * @param {number} idx The position of the choice within the choice array + * @param {number} idx The position of the choice within the choices array * * @return {HTMLElement} td The newly created cell of the table * @@ -1204,17 +1543,23 @@ * @see ChoiceTable.separator * @see ChoiceTable.choicesCells */ - ChoiceTable.prototype.renderChoice = function(choice, idx) { + ChoiceTable.prototype.renderChoice = function(choice, idx, pos) { var td, shortValue, value, width; td = document.createElement('td'); if (this.tabbable) J.makeTabbable(td); // Forces equal width. if (this.sameWidthCells && this.orientation === 'H') { - width = this.left ? 70 : 100; - if (this.right) width = width - 30; - width = width / (this.choicesSetSize || this.choices.length); - td.style.width = width.toFixed(2) + '%'; + if (this.sameWidthCells === true) { + width = this.left ? 70 : 100; + if (this.right) width = width - 20; + width = width / (this.choicesSetSize || this.choices.length); + width = width.toFixed(2) + '%'; + } + else { + width = this.sameWidthCells; + } + td.style.width = width; } // Use custom renderer. @@ -1233,7 +1578,8 @@ choice = choice.display; } - value = this.shuffleChoices ? this.order[idx] : idx; + // value = this.shuffleChoices ? this.order[idx] : idx; + value = idx; if ('string' === typeof choice || 'number' === typeof choice) { td.innerHTML = choice; @@ -1262,7 +1608,7 @@ } // All fine, updates global variables. - this.choicesValues[value] = idx; + this.choicesValues[value] = pos; this.choicesCells[idx] = td; this.choicesIds[td.id] = td; @@ -1324,7 +1670,7 @@ ChoiceTable.prototype.append = function() { var tmp; // Id must be unique. - if (W.getElementById(this.id)) { + if (W.gid(this.id)) { throw new Error('ChoiceTable.append: id is not ' + 'unique: ' + this.id); } @@ -1364,6 +1710,11 @@ this.errorBox = W.append('div', this.bodyDiv, { className: 'errbox' }); + this.setCustomInput(this.other, this.bodyDiv); + + if (this.solution) { + this.solutionDiv = W.append('div', this.bodyDiv); + } // Creates a free-text textarea, possibly with placeholder text. if (this.freeText) { @@ -1377,6 +1728,33 @@ // Append textarea. this.bodyDiv.appendChild(this.textarea); } + + // Inits default choices, if necessary. + if (this._initDefaultChoice) initDefaultChoice(this); + }; + + /** + * ### ChoiceTable.setCustomInput + * + * Set Custom Input widget. + * + */ + ChoiceTable.prototype.setCustomInput = function(other, root) { + var opts; + if (other === null || 'boolean' === typeof other) return; + opts = { + id: 'other' + this.id, + mainText: this.getText('customInput'), + requiredChoice: this.requiredChoice, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark + }; + // other is the string 'CustomInput' or a conf object. + if ('object' === typeof other) J.mixin(opts, other); + // Force initially hidden. + opts.hidden = true; + this.customInput = node.widgets.append('CustomInput', root, opts); + }; /** @@ -1431,6 +1809,7 @@ // Remove listener to make cells clickable with the keyboard. if (this.tabbable) J.makeClickable(this.table, false); } + if (this.customInput) this.customInput.disable(); this.emit('disabled'); }; @@ -1451,6 +1830,7 @@ this.table.addEventListener('click', this.listener); // Add listener to make cells clickable with the keyboard. if (this.tabbable) J.makeClickable(this.table); + if (this.customInput) this.customInput.enable(); this.emit('enabled'); }; @@ -1475,9 +1855,24 @@ * @see ChoiceTable.attempts * @see ChoiceTable.setCorrectChoice */ - ChoiceTable.prototype.verifyChoice = function(markAttempt) { + ChoiceTable.prototype.verifyChoice = function(markAttempt) { var i, len, j, lenJ, c, clone, found; - var correctChoice; + var correctChoice, ci, ciCorrect; + + // Mark attempt by default. + markAttempt = 'undefined' === typeof markAttempt ? true : markAttempt; + if (markAttempt) this.attempts.push(this.currentChoice); + + // Custom input to check. + ci = this.customInput && !this.customInput.isHidden(); + if (ci) { + ciCorrect = this.customInput.getValues({ + markAttempt: markAttempt + }).isCorrect; + if (ciCorrect === false) return false; + // Set it to null so it is returned correctly, later below. + if ('undefined' === typeof ciCorrect) ciCorrect = null; + } // Check the number of choices. if (this.requiredChoice !== null) { @@ -1485,40 +1880,40 @@ else return this.currentChoice.length >= this.requiredChoice; } - // If no correct choice is set return null. - if ('undefined' === typeof this.correctChoice) return null; - // Mark attempt by default. - markAttempt = 'undefined' === typeof markAttempt ? true : markAttempt; - if (markAttempt) this.attempts.push(this.currentChoice); - if (!this.selectMultiple) { - return this.currentChoice === this.correctChoice; - } - else { - // Make it an array (can be a string). - correctChoice = J.isArray(this.correctChoice) ? - this.correctChoice : [this.correctChoice]; + correctChoice = this.correctChoice; + // If no correct choice is set return null or ciCorrect (true|null). + if (null === correctChoice) return ci ? ciCorrect : null; - len = correctChoice.length; - lenJ = this.currentChoice.length; - // Quick check. - if (len !== lenJ) return false; - // Check every item. - i = -1; - clone = this.currentChoice.slice(0); - for ( ; ++i < len ; ) { - found = false; - c = correctChoice[i]; - j = -1; - for ( ; ++j < lenJ ; ) { - if (clone[j] === c) { - found = true; - break; - } + // Only one choice allowed, ci is correct, + // otherwise we would have returned already. + if (!this.selectMultiple) return this.currentChoice === correctChoice; + + // Multiple selections allowed. + + // Make it an array (can be a string). + if (!J.isArray(correctChoice)) correctChoice = [correctChoice]; + + len = correctChoice.length; + lenJ = this.currentChoice.length; + // Quick check. + if (len !== lenJ) return false; + // Check every item. + i = -1; + clone = this.currentChoice.slice(0); + for ( ; ++i < len ; ) { + found = false; + c = correctChoice[i]; + j = -1; + for ( ; ++j < lenJ ; ) { + if (clone[j] === c) { + found = true; + break; } - if (!found) return false; } - return true; + if (!found) return false; } + return true; + }; /** @@ -1624,18 +2019,26 @@ * * Highlights the choice table * - * @param {string} The style for the table's border. + * @param {string|obj} opts Optional. If string is the 'border' + * option for backward compatibilityThe style for the table's border. * Default '3px solid red' * * @see ChoiceTable.highlighted */ - ChoiceTable.prototype.highlight = function(border) { + ChoiceTable.prototype.highlight = function(opts) { + var border, ci; + opts = opts || {}; + // Backward compatible. + if ('string' === typeof opts) opts = { border: opts }; + border = opts.border; if (border && 'string' !== typeof border) { throw new TypeError('ChoiceTable.highlight: border must be ' + 'string or undefined. Found: ' + border); } if (!this.table || this.highlighted) return; this.table.style.border = border || '3px solid red'; + ci = this.customInput; + if (opts.customInput !== false && ci && !ci.isHidden()) ci.highlight(); this.highlighted = true; this.emit('highlighted', border); }; @@ -1647,9 +2050,15 @@ * * @see ChoiceTable.highlighted */ - ChoiceTable.prototype.unhighlight = function() { + ChoiceTable.prototype.unhighlight = function(opts) { + var ci; + opts = opts || {}; if (!this.table || this.highlighted !== true) return; this.table.style.border = ''; + ci = this.customInput; + if (opts.customInput !== false && ci && !ci.isHidden()) { + ci.unhighlight(); + } this.highlighted = false; this.setError(); this.emit('unhighlighted'); @@ -1683,7 +2092,10 @@ * @see ChoiceTable.reset */ ChoiceTable.prototype.getValues = function(opts) { - var obj, resetOpts, i, len; + var obj, resetOpts, i, len, ci, ciCorrect; + var that; + + that = this; opts = opts || {}; obj = { id: this.id, @@ -1701,20 +2113,21 @@ // Option getValue backward compatible. if (opts.addValue !== false && opts.getValue !== false) { if (!this.selectMultiple) { - obj.value = getValueFromChoice(this.choices[obj.choice]); + obj.value = getValueFromChoice(that,this.choices[obj.choice]); } else { len = obj.choice.length; obj.value = new Array(len); if (len === 1) { obj.value[0] = - getValueFromChoice(this.choices[obj.choice[0]]); + getValueFromChoice(that,this.choices[obj.choice[0]]); } else { i = -1; for ( ; ++i < len ; ) { obj.value[i] = - getValueFromChoice(this.choices[obj.choice[i]]); + getValueFromChoice(that, + this.choices[obj.choice[i]]); } if (opts.sortValue !== false) obj.value.sort(); } @@ -1727,18 +2140,43 @@ if (this.groupOrder === 0 || this.groupOrder) { obj.groupOrder = this.groupOrder; } - if (null !== this.correctChoice || null !== this.requiredChoice) { + + ci = this.customInput; + if (this.required !== false && + (null !== this.correctChoice || null !== this.requiredChoice || + (ci && !ci.isHidden()))) { + obj.isCorrect = this.verifyChoice(opts.markAttempt); obj.attempts = this.attempts; - if (!obj.isCorrect && opts.highlight) this.highlight(); + if (!obj.isCorrect && opts.highlight) this.highlight({ + // If errored, it is already highlighted + customInput: false + }); } + if (this.textarea) obj.freetext = this.textarea.value; + if (obj.isCorrect === false) { - this.setError(this.getText('error', obj.value)); + // If there is an error on CI, we just highlight CI. + // However, there could be an error also on the choice table, + // e.g., not enough options selected. It will be catched + // at next click. + // TODO: change verifyChoice to say where the error is coming from. + if (ci) { + ciCorrect = ci.getValues({ + markAttempt: false + }).isCorrect; + } + if (ci && !ciCorrect && !ci.isHidden()) { + this.unhighlight({ customInput: false }); + } + else { + this.setError(this.getText('error', obj.value)); + } } else if (opts.reset) { - resetOpts = 'object' !== typeof opts.reset ? {} : opts.reset; - this.reset(resetOpts); + resetOpts = 'object' !== typeof opts.reset ? {} : opts.reset; + this.reset(resetOpts); } return obj; }; @@ -1802,6 +2240,7 @@ // Set values, random or pre-set. i = -1; + // Pre-set. if ('undefined' !== typeof options.values) { if (!J.isArray(options.values)) tmp = [ options.values ]; len = tmp.length; @@ -1860,6 +2299,9 @@ // Make a random comment. if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); + if (this.customInput && !this.customInput.isHidden()) { + this.customInput.setValues(); + } }; /** @@ -1900,6 +2342,7 @@ if (this.isHighlighted()) this.unhighlight(); if (options.shuffleChoices) this.shuffle(); + if (this.customInput) this.customInput.reset(); }; /** @@ -1914,8 +2357,15 @@ var parentTR; H = this.orientation === 'H'; - order = J.shuffle(this.order); - i = -1, len = order.length; + len = this.order.length; + if (this.other) { + order = J.shuffle(this.order.slice(0,-1)); + order.push(this.order[len - 1]); + } + else { + order = J.shuffle(this.order); + } + i = -1; choicesValues = {}; choicesCells = new Array(len); @@ -1948,6 +2398,65 @@ this.choicesValues = choicesValues; }; + /** + * ### ChoiceManager.setValues + * + * Sets values for forms in manager as specified by the options + * + * @param {object} options Optional. Options specifying how to set + * the values. If no parameter is specified, random values will + * be set. + */ + ChoiceTable.prototype.next = function() { + var sol; + sol = this.solution; + // No solution or solution already displayed. + if (!sol || this.solutionDisplayed) return false; + // Solution, but no answer provided. + if (sol) { + if (!this.isChoiceDone() && !this.solutionNoChoice) return false; + this.solutionDisplayed = true; + if ('function' === typeof sol) { + sol = this.solution(this.verifyChoice(false), this); + } + this.solutionDiv.innerHTML = sol; + } + this.disable(); + W.adjustFrameHeight(); + node.emit('WIDGET_NEXT', this); + return true; + }; + + ChoiceTable.prototype.prev = function() { + return false; + if (!this.solutionDisplayed) return false; + this.solutionDisplayed = false; + this.solutionDiv.innerHTML = ''; + this.enable(); + W.adjustFrameHeight(); + node.emit('WIDGET_PREV', this); + return true; + }; + + ChoiceTable.prototype.isChoiceDone = function(complete) { + var cho, mul, len, ci; + ci = this.customInput; + cho = this.currentChoice; + mul = this.selectMultiple; + // Selected "Other, Specify" + if (ci && this.isChoiceCurrent(this.choices.length-1)) return false; + // Single choice. + if ((!complete || !mul) && null !== cho) return true; + // Multiple choices. + if (J.isArray(cho)) len = cho.length; + if (mul === true && len === this.choices.length) return true; + if ('number' === typeof mul && len === mul) return true; + // Not done. + return false; + }; + + + // ## Helper methods. /** @@ -2012,6 +2521,7 @@ * The value is either the text displayed or short value specified * by the choice. * + * @param {ChoiceTable} that This instance * @param {mixed} choice * @param {boolean} display TRUE to return the display value instead * one. Default: FALSE. @@ -2022,7 +2532,10 @@ * @see ChoiceTable.getValues * @see ChoiceTable.renderChoice */ - function getValueFromChoice(choice, display) { + function getValueFromChoice(that, choice, display) { + if (choice === that.getText('other') && that.customInput) { + return that.customInput.getValues().value; + } if ('string' === typeof choice || 'number' === typeof choice) { return choice; } @@ -2034,4 +2547,34 @@ return null; } + /** + * ### initDefaultChoice + * + * Clicks on the default choices if they exist, or mark it as todo + * + * @param {ChoiceTable} that This instance + * + * @see ChoiceTable._initDefaultChoice + */ + function initDefaultChoice(that) { + var choice; + choice = that.defaultChoice; + // Already appended. + if (that.table) { + if (J.isArray(choice)) { + for (i = 0; i < choice.length; i++) { + that.clickChoice(i); + } + } + else { + that.clickChoice(choice); + } + that._initDefaultChoice = false; + } + else { + // Mark the choice to be inited as soon as possible. + that._initDefaultChoice = true; + } + } + })(node); diff --git a/widgets/ChoiceTableGroup.js b/widgets/ChoiceTableGroup.js index 183fc04..19f2876 100644 --- a/widgets/ChoiceTableGroup.js +++ b/widgets/ChoiceTableGroup.js @@ -1,6 +1,6 @@ /** * # ChoiceTableGroup - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a table that groups together several choice tables widgets @@ -17,29 +17,23 @@ // ## Meta-data - ChoiceTableGroup.version = '1.8.0'; + ChoiceTableGroup.version = '1.9.0'; ChoiceTableGroup.description = 'Groups together and manages sets of ' + 'ChoiceTable widgets.'; - ChoiceTableGroup.title = 'Make your choice'; ChoiceTableGroup.className = 'choicetable choicetablegroup'; ChoiceTableGroup.separator = '::'; ChoiceTableGroup.texts = { - autoHint: function(w) { - if (w.requiredChoice) return '*'; - else return false; - }, - error: 'Selection required.' }; // ## Dependencies ChoiceTableGroup.dependencies = { - JSUS: {} + ChoiceTable: {} }; /** @@ -415,6 +409,17 @@ * @see ChoiceTable.tabbable */ this.tabbable = null; + + /** + * ### ChoiceTableGroup.valueOnly + * + * If TRUE, `getValues` returns only the field `value` from ChoiceTable + * + * Default FALSE + * + * @see ChoiceTableGroup.getValues + */ + this.valueOnly = null; } // ## ChoiceTableGroup methods @@ -565,11 +570,25 @@ 'be a string, false, or undefined. Found: ' + opts.hint); } - else { - // Returns undefined if there are no constraints. - this.hint = this.getText('autoHint'); + + if (this.required && this.hint !== false && + opts.displayRequired !== false) { + + tmp = this.requiredMark; + + if (this.hint) { + if (this.hint.charAt(this.hint.length-1) !== tmp) { + this.hint += ' ' + tmp; + } + } + else { + this.hint = tmp; + } + } + // this.hint = node.widgets.utils.processHints(opts.hint); + // Set the timeFrom, if any. if (opts.timeFrom === false || 'string' === typeof opts.timeFrom) { @@ -620,6 +639,8 @@ if (opts.tabbable !== false) this.tabbable = true; + if (opts.valueOnly === true) this.valueOnly = true; + // Separator checked by ChoiceTable. if (opts.separator) this.separator = opts.separator; @@ -642,16 +663,21 @@ opts.freeText : !!opts.freeText; if (opts.header) { - if (!J.isArray(opts.header) || - opts.header.length !== opts.choices.length) { + tmp = opts.header; + // One td will colspan all choices. + if ('string' === typeof tmp) { + tmp = [ tmp ]; + } + else if (!J.isArray(tmp) || + (tmp.length !== 1 && tmp.length !== opts.choices.length)) { throw new Error('ChoiceTableGroup.init: header ' + - 'must be an array of length ' + + 'must be string, array (size ' + opts.choices.length + - ' or undefined. Found: ' + opts.header); + '), or undefined. Found: ' + tmp); } - this.header = opts.header; + this.header = tmp; } @@ -707,7 +733,7 @@ * @see ChoiceTableGroup.order */ ChoiceTableGroup.prototype.buildTable = function() { - var i, len, tr, H, ct; + var i, len, td, tr, H, ct; var j, lenJ, lenJOld, hasRight, cell; H = this.orientation === 'H'; @@ -720,11 +746,13 @@ className: 'header' }); for ( ; ++i < this.header.length ; ) { - W.add('td', tr, { + td = W.add('td', tr, { innerHTML: this.header[i], className: 'header' }); } + // Only one element, header spans throughout. + if (i === 1) td.setAttribute('colspan', this.choices.length); i = -1; } @@ -1063,6 +1091,8 @@ * - reset: If TRUTHY and no item raises an error, * then it resets the state of all items before * returning it. Default: FALSE. + * - valueOnly: If TRUE it returns only the value of each ChoiceTable + * instead of the all object from .getValues(). Experimental. * * @return {object} Object containing the choice and paradata * @@ -1070,10 +1100,11 @@ * @see ChoiceTableGroup.reset */ ChoiceTableGroup.prototype.getValues = function(opts) { - var obj, i, len, tbl, toHighlight, toReset; + var obj, i, len, tbl, toHighlight, toReset, res, valueOnly; obj = { id: this.id, order: this.order, + nClicks: 0, items: {}, isCorrect: true }; @@ -1082,18 +1113,23 @@ // Make sure reset is done only at the end. toReset = opts.reset; opts.reset = false; + valueOnly = opts.valueOnly === true || this.valueOnly; i = -1, len = this.items.length; for ( ; ++i < len ; ) { tbl = this.items[i]; - obj.items[tbl.id] = tbl.getValues(opts); - if (obj.items[tbl.id].choice === null) { + res = tbl.getValues(opts); + obj.items[tbl.id] = valueOnly ? res.value : res; + if (res.choice === null) { obj.missValues = true; - if (tbl.requiredChoice) { + if (this.required || tbl.requiredChoice) { toHighlight = true; obj.isCorrect = false; } } - if (obj.items[tbl.id].isCorrect === false && opts.highlight) { + else { + obj.nClicks += res.nClicks; + } + if (res.isCorrect === false && opts.highlight) { toHighlight = true; } } @@ -1258,7 +1294,6 @@ s.group = that.id; s.groupOrder = i+1; s.orientation = that.orientation; - s.title = false; s.listeners = false; s.separator = that.separator; diff --git a/widgets/Consent.js b/widgets/Consent.js index ca541f6..0b83646 100644 --- a/widgets/Consent.js +++ b/widgets/Consent.js @@ -1,6 +1,6 @@ /** * # Consent - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2024 Stefano Balietti * MIT Licensed * * Displays a consent form with buttons to accept/reject it @@ -15,10 +15,9 @@ // ## Meta-data - Consent.version = '0.3.0'; + Consent.version = '0.8.0'; Consent.description = 'Displays a configurable consent form.'; - Consent.title = false; Consent.panel = false; Consent.className = 'consent'; @@ -50,21 +49,25 @@ * * Creates a new instance of Consent * - * @param {object} options Optional. Configuration options - * which is forwarded to Consent.init. - * * @see Consent.init */ function Consent() { /** - * ## Consent.consent + * ## Consent.consentTexts * * The object containing the variables to substitute * * Default: node.game.settings.CONSENT */ - this.consent = null; + this.consentTexts = null; + + /** + * ## Consent.agreed + * + * If TRUE, consent has been given + */ + this.agreed = null; /** * ## Consent.showPrint @@ -74,6 +77,84 @@ * Default: TRUE */ this.showPrint = null; + + /** + * ## Consent.showAgreeBtns + * + * If TRUE, the agree/disagree buttons are shown + * + * Default: TRUE + */ + this.showAgreeBtns = null; + + /** + * ## Consent.disconnect + * + * If TRUE, client is disconnected upon reject + * + * Default: TRUE + */ + this.disconnect = null; + + /** + * ## Consent.checkboxes + * + * Checkboxes that need to checked to consent + * + * The content of the arrays can be strings, or objects that specify + * additional properties, i.e.: + * + * ```js + * + * { + * label: 'This is the label text', + * required: false, // Default true + * className: 'myclass' // Added to outer div, default: 'form-switch' + * } + * ``` + * + * They can also be functions that either return strings or objects, + * or FALSE, if the checkbox should not be added. + * + */ + this.checkboxes = []; + + /** + * ## Consent.fineprint + * + * Additional text displayed in a small font under the checkboxes + */ + this.fineprint = null; + + /** + * ## Consent.prefix + * + * The prefix to the ids created by the widget + * + * Default: '' + */ + this.prefix = ''; + + /** + * ## Consent.consentId + * + * The id of the HTML element that contains the consent + * + * The widget will be appended here, if found. + * + * Default: `prefix` + 'consent' + */ + this.consentId = 'consent'; + + /** + * ## Consent.doneOnAgree + * + * If TRUE, `node.done` is called upon agreeing to consent form + * + * Default: TRUE + */ + this.doneOnAgree; + } // ## Consent methods. @@ -86,61 +167,161 @@ * @param {object} opts Optional. Configuration options. */ Consent.prototype.init = function(opts) { + var that; opts = opts || {}; - this.consent = opts.consent || node.game.settings.CONSENT; + this.consentTexts = opts.consent || node.game.settings.CONSENT; - if (this.consent && 'object' !== typeof this.consent) { - throw new TypeError('Consent: consent must be object or ' + - 'undefined. Found: ' + this.consent); + if (this.consentTexts && 'object' !== typeof this.consentTexts) { + throw new TypeError('Consent.init: consent must be object or ' + + 'undefined. Found: ' + this.consentTexts); } this.showPrint = opts.showPrint === false ? false : true; + + this.showBtns = opts.showAgreeBtns === false ? false : true; + + this.disconnect = opts.disconnect === false ? false : true; + + this.doneOnAgree = opts.doneOnAgree === false ? false : true; + + if (J.isArray(opts.checkboxes)) { + that = this; + opts.checkboxes.forEach(function(item) { + if ('function' === typeof item) { + item = item(); + if (item === false) return; + } + that.checkboxes.push(item); + }); + } + else if (opts.checkboxes) { + throw new TypeError('Consent.init: checkboxes must be array or ' + + 'undefined. Found: ' + this.checkboxes); + } + + _assignStr(this, opts, 'prefix'); + _assignStr(this, opts, 'fineprint'); + _assignStr(this, opts, 'consentId'); + + if ('undefined' === typeof opts.consentId) { + this.consentId = _addPrefix(this, this.consentId); + } }; Consent.prototype.enable = function() { - var a, na; - if (this.notAgreed) return; - a = W.gid('agree'); - if (a) a.disabled = false; - na = W.gid('notAgree'); - if (na) na.disabled = false; + if (this.agreed !== null) return; + _toggleEnable(true); }; Consent.prototype.disable = function() { - var a, na; - if (this.notAgreed) return; - a = W.gid('agree'); - if (a) a.disabled = true; - na = W.gid('notAgree'); - if (na) na.disabled = true; + _toggleEnable(false); }; Consent.prototype.append = function() { - var consent, html; + var that, consent, isRtl, html, btn1, btn2, st1, st2; + + that = this; + // Hide not agreed div. - W.hide('notAgreed'); - - consent = W.gid('consent'); + W.hide(_addPrefix(this, 'notAgreed')); + + consent = W.gid(this.consentId); + if (!consent) { + node.warn('Consent.append: the page does not contain an ' + + 'element with id "' + this.consentId + + '", it will use widget\'s root'); + + consent = w.bodyDiv; + } html = ''; + + // Checkboxes. + + isRtl = W.isRTL(this.bodyDiv); + + if (this.checkboxes.length || this.fineprint) { + + html += '
'; + + if (this.checkboxes.length) { + html += '
'; + this.checkboxes.forEach(function(c, idx) { + var id, label, btn, className; + id = _getCbxId(that, idx+1); + + className = 'form-check'; + if (isRtl) className += '-reverse'; + + if ('object' === typeof c) { + label = c.label; + className += ' ' + c.className; + } + else { + label = c; + } + + btn = ''; + label = ''; + + html += '
'; + html += '
'; + // The reverse class takes care of switching the order + // of btn and label. + html += btn + label; + html += '
'; + }); + html += '
'; + } + + if (this.fineprint) { + html += '

'; + html += this.fineprint; + html += '

'; + } + + html += '
'; + + } // Print. if (this.showPrint) { html = this.getText('printText'); - html += '

'; + html += '

'; } - // Header for buttons. - html += '' + this.getText('consentTerms') + '
'; + + if (this.showBtns !== false) { + // Header for buttons. + html += '' + this.getText('consentTerms') + '
'; - // Buttons. - html += '
' + - '
'; + // Buttons. + html += ''; + } + consent.innerHTML += html; setTimeout(function() { W.adjustFrameHeight(); }); @@ -148,7 +329,7 @@ Consent.prototype.listeners = function() { var that = this; - var consent = this.consent; + var consent = this.consentTexts; node.on('FRAME_LOADED', function() { var a, na, p, id; @@ -166,14 +347,20 @@ } // Add listeners on buttons. - a = W.gid('agree'); - na = W.gid('notAgree'); - - if (!a) throw new Error('Consent: agree button not found'); - if (!na) throw new Error('Consent: notAgree button not found'); - - - a.onclick = function() { node.done({ consent: true }); }; + if (!that.showBtns) return; + + a = W.gid(_addPrefix(this, 'agree')); + na = W.gid(_addPrefix(this, 'notAgree')); + + a.onclick = function() { + var consent; + node.emit('CONSENT_ACCEPTING'); + consent = that.getValues({ agreed: true }); + if (!consent.consent) return; + this.agreed = true; + node.emit('CONSENT_ACCEPTED', consent); + if (that.doneOnAgree) node.done(consent); + }; na.onclick = function() { var showIt, confirmed; @@ -182,7 +369,7 @@ node.emit('CONSENT_REJECTING'); - that.notAgreed = true; + that.agreed = false; node.set({ consent: false, // Need to send these two because it's not a DONE msg. @@ -194,16 +381,22 @@ a.onclick = null; na.onclick = null; - node.socket.disconnect(); - W.hide('consent'); - W.show('notAgreed'); + // Disconnect, if requested. + if (that.disconnect) { + // Destroy disconnectBox (if found) before disconnecting. + if (node.game.discBox) node.game.discBox.destroy(); + node.socket.disconnect(); + } + + W.hide(that.consentId); + W.show(_addPrefix(that, 'notAgreed')); // If a show-consent button is found enable it. - showIt = W.gid('show-consent'); + showIt = W.gid(_addPrefix(that, 'show-consent')); if (showIt) { showIt.onclick = function() { var div, s; - div = W.toggle('consent'); + div = W.toggle(that.consentId); s = div.style.display === '' ? 'hide' : 'show'; this.innerHTML = that.getText('showHideConsent', s); }; @@ -213,4 +406,130 @@ }); }; + /** + * ## Consent.getValues + * + * Returns the current selection on Consent + * + * @param {object} opts Configuration object. Options: + * - highlight: if TRUE, missing consents on checkboxes are highlighted. + * Default: TRUE. + * - agreed: TRUE to flag that the user has already clicked on agree + * @returns {object} consent Values of consent. + * + * ```js + * { + * consent: true, // if all consent conditions are fullfilled + * checkboxes: true // if all required checkboxes are checked + * [checkbox_ID1...IDN]: true // one property per checkbox + * } + */ + Consent.prototype.getValues = function(opts) { + var consent, that; + that = this; + consent = { consent: true }; + opts = opts || {}; + if (this.checkboxes.length) { + consent.checkboxes = true; + this.checkboxes.forEach(function(c, idx) { + var cbx, id, req; + id = _getCbxId(that, idx+1); + cbx = W.gid(id); + if (!cbx) { + node.warn('Consent: could not find checkbox ' + id); + } + else { + req = that.checkboxes[idx]; + consent[id] = cbx.checked; + + if ('string' === typeof req || + req.required !== false) { + + if (!cbx.checked) { + // At least one is needed to deny consent. + consent.checkboxes = consent.consent = false; + if (opts.highlight !== false) W.shake(cbx); + } + } + } + }); + } + if (this.agreed !== true && this.showBtns && !opts.agreed) { + consent.consent = false; + } + return consent; + }; + + // ### Helper functions + + + /** ### _toggleEnable + * + * Enables/disables inputs in the widget + * + * @param {boolean} state True or false + */ + function _toggleEnable(state) { + var elem, i; + elem = W.gid('agree'); + if (elem) elem.disabled = state; + elem = W.gid('notAgree'); + if (elem) elem.disabled = state; + if (this.checkboxes && this.checkboxes.length) { + for (i = 0; i < this.checkboxes.length; i++) { + elem = W.gid(_getCbxId(i+1)); + if (elem) elem.disabled = state; + } + } + } + + /** + * ### _addPrefix + * + * Adds a the widget prefix to a string, if one is set. + * + * @param {object} w This widget + * @param {string} str The string to manipulate + * + * @returns {string} The id of the checkbox at a given index + */ + function _addPrefix(w, str) { + return (w.prefix ? (w.prefix + '_') : '') + str; + } + + /** + * ### _getCbxId + * + * Returns a standardized id for a chekbox based on its index. + * + * @param {object} w This widget + * @param {number} idx The id of the checkbox + * + * @returns {string} The id of the checkbox at a given index + */ + function _getCbxId(w, idx) { + return _addPrefix(w, 'consent_checkbox_' + idx); + } + + /** + * ### _assignStr + * + * Checks the value of a field in an object, if string it stores it + * + * @param {object} w This widget + * @param {object} opts The configuration options with the field to check + * @param {string} id The id to assign + */ + function _assignStr(w, opts, id) { + var str; + str = opts[id]; + if ('string' === typeof str) { + w[id] = str; + } + else if (str) { + throw new TypeError('Consent.init: ' + id + 'Id must be ' + + 'string or undefined. Found: ' + str); + } + } + })(node); diff --git a/widgets/ContentBox.js b/widgets/ContentBox.js index da3d0cc..cdbb872 100644 --- a/widgets/ContentBox.js +++ b/widgets/ContentBox.js @@ -1,6 +1,6 @@ /** * # ContentBox - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays some content. @@ -18,14 +18,9 @@ ContentBox.version = '0.2.0'; ContentBox.description = 'Simply displays some content'; - ContentBox.title = false; ContentBox.panel = false; ContentBox.className = 'contentbox'; - // ## Dependencies - - ContentBox.dependencies = {}; - /** * ## ContentBox constructor * diff --git a/widgets/Controls.js b/widgets/Controls.js index 46028a2..a3e4dde 100644 --- a/widgets/Controls.js +++ b/widgets/Controls.js @@ -1,6 +1,6 @@ /** * # Controls - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates and manipulates a set of forms @@ -20,7 +20,6 @@ Controls.version = '0.5.1'; Controls.description = 'Wraps a collection of user-inputs controls.'; - Controls.title = 'Controls'; Controls.className = 'controls'; /** @@ -77,7 +76,7 @@ } Controls.prototype.add = function(root, id, attributes) { - // TODO: replace W.addTextInput + // TODO: replace W.addTextInput //return W.addTextInput(root, id, attributes); }; @@ -193,7 +192,7 @@ }; } - if (attributes.label) { + if (attributes.label) { W.add('label', container, { 'for': elem.id, innerHTML: attributes.label @@ -428,7 +427,7 @@ for (key in this.features) { if (this.features.hasOwnProperty(key)) { el = W.getElementById(key); - if (el.checked) return el.value; + if (el.checked) return el.value; } } return false; diff --git a/widgets/CustomInput.js b/widgets/CustomInput.js index 3e1ad78..d3ae5f5 100644 --- a/widgets/CustomInput.js +++ b/widgets/CustomInput.js @@ -1,6 +1,6 @@ /** * # CustomInput - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a configurable input form with validation @@ -18,7 +18,6 @@ CustomInput.version = '0.12.0'; CustomInput.description = 'Creates a configurable input form'; - CustomInput.title = false; CustomInput.panel = false; CustomInput.className = 'custominput'; @@ -180,7 +179,8 @@ res = '(Must be before ' + w.params.max + ')'; } } - return w.required ? ((res || '') + ' *') : (res || false); + return w.required && w.displayRequired ? + ((res || '') + ' ' + w.requiredMark) : (res || false); }, numericErr: function(w) { var str, p; @@ -240,12 +240,6 @@ emptyErr: 'Cannot be empty' }; - // ## Dependencies - - CustomInput.dependencies = { - JSUS: {} - }; - /** * ## CustomInput constructor * @@ -468,7 +462,7 @@ * @param {object} opts Configuration options */ CustomInput.prototype.init = function(opts) { - var tmp, that, e, isText, setValues; + var tmp, val, that, e, isText, setValues; that = this; e = 'CustomInput.init: '; @@ -535,102 +529,102 @@ 'or undefined. Found: ' + opts.validation); } - tmp = opts.validation; + val = opts.validation; } - else { - // Add default validations based on type. + // Add default validations based on type. - if (this.type === 'number' || this.type === 'float' || - this.type === 'int' || this.type === 'text') { + if (this.type === 'number' || this.type === 'float' || + this.type === 'int' || this.type === 'text') { - isText = this.type === 'text'; + isText = this.type === 'text'; - // Greater than. - if ('undefined' !== typeof opts.min) { - tmp = J.isNumber(opts.min); - if (false === tmp) { - throw new TypeError(e + 'min must be number or ' + - 'undefined. Found: ' + opts.min); - } - this.params.lower = opts.min; - this.params.leq = true; + // Greater than. + if ('undefined' !== typeof opts.min) { + tmp = J.isNumber(opts.min); + if (false === tmp) { + throw new TypeError(e + 'min must be number or ' + + 'undefined. Found: ' + opts.min); } - // Less than. - if ('undefined' !== typeof opts.max) { - tmp = J.isNumber(opts.max); - if (false === tmp) { - throw new TypeError(e + 'max must be number or ' + - 'undefined. Found: ' + opts.max); - } - this.params.upper = opts.max; - this.params.ueq = true; + this.params.lower = opts.min; + this.params.leq = true; + } + // Less than. + if ('undefined' !== typeof opts.max) { + tmp = J.isNumber(opts.max); + if (false === tmp) { + throw new TypeError(e + 'max must be number or ' + + 'undefined. Found: ' + opts.max); } + this.params.upper = opts.max; + this.params.ueq = true; + } - if (opts.strictlyGreater) this.params.leq = false; - if (opts.strictlyLess) this.params.ueq = false; + if (opts.strictlyGreater) this.params.leq = false; + if (opts.strictlyLess) this.params.ueq = false; - // Checks on both min and max. - if ('undefined' !== typeof this.params.lower && - 'undefined' !== typeof this.params.upper) { + // Checks on both min and max. + if ('undefined' !== typeof this.params.lower && + 'undefined' !== typeof this.params.upper) { - if (this.params.lower > this.params.upper) { - throw new TypeError(e + 'min cannot be greater ' + - 'than max. Found: ' + - opts.min + '> ' + opts.max); + if (this.params.lower > this.params.upper) { + throw new TypeError(e + 'min cannot be greater ' + + 'than max. Found: ' + + opts.min + '> ' + opts.max); + } + // Exact length. + if (this.params.lower === this.params.upper) { + if (!this.params.leq || !this.params.ueq) { + + throw new TypeError(e + 'min cannot be equal to ' + + 'max when strictlyGreater or ' + + 'strictlyLess are set. ' + + 'Found: ' + opts.min); } - // Exact length. - if (this.params.lower === this.params.upper) { - if (!this.params.leq || !this.params.ueq) { - - throw new TypeError(e + 'min cannot be equal to ' + - 'max when strictlyGreater or ' + - 'strictlyLess are set. ' + - 'Found: ' + opts.min); - } - if (this.type === 'int' || this.type === 'text') { - if (J.isFloat(this.params.lower)) { + if (this.type === 'int' || this.type === 'text') { + if (J.isFloat(this.params.lower)) { - throw new TypeError(e + 'min cannot be a ' + - 'floating point number ' + - 'and equal to ' + - 'max, when type ' + - 'is not "float". Found: ' + - opts.min); - } + throw new TypeError(e + 'min cannot be a ' + + 'floating point number ' + + 'and equal to ' + + 'max, when type ' + + 'is not "float". Found: ' + + opts.min); } - // Store this to create better error strings. - this.params.exactly = true; - } - else { - // Store this to create better error strings. - this.params.between = true; } + // Store this to create better error strings. + this.params.exactly = true; } + else { + // Store this to create better error strings. + this.params.between = true; + } + } - // Checks for text only. - if (isText) { + // Checks for text only. + if (isText) { - this.params.noNumbers = opts.noNumbers; + this.params.noNumbers = opts.noNumbers; - if ('undefined' !== typeof this.params.lower) { - if (this.params.lower < 0) { - throw new TypeError(e + 'min cannot be negative ' + - 'when type is "text". Found: ' + - this.params.lower); - } - if (!this.params.leq) this.params.lower++; + if ('undefined' !== typeof this.params.lower) { + if (this.params.lower < 0) { + throw new TypeError(e + 'min cannot be negative ' + + 'when type is "text". Found: ' + + this.params.lower); } - if ('undefined' !== typeof this.params.upper) { - if (this.params.upper < 0) { - throw new TypeError(e + 'max cannot be negative ' + - 'when type is "text". Found: ' + - this.params.upper); - } - if (!this.params.ueq) this.params.upper--; + if (!this.params.leq) this.params.lower++; + } + if ('undefined' !== typeof this.params.upper) { + if (this.params.upper < 0) { + throw new TypeError(e + 'max cannot be negative ' + + 'when type is "text". Found: ' + + this.params.upper); } + if (!this.params.ueq) this.params.upper--; + } - tmp = function(value) { + if (!val) { + val = function(value) { var len, p, out, err; p = that.params; len = value.length; @@ -644,9 +638,9 @@ } else { if (('undefined' !== typeof p.lower && - len < p.lower) || - ('undefined' !== typeof p.upper && - len > p.upper)) { + len < p.lower) || + ('undefined' !== typeof p.upper && + len > p.upper)) { err = true; } @@ -656,18 +650,20 @@ if (err) out.err = err; return out; }; - - setValues = function() { - var a, b; - a = 'undefined' !== typeof that.params.lower ? - (that.params.lower + 1) : 5; - b = 'undefined' !== typeof that.params.upper ? - that.params.upper : (a + 5); - return J.randomString(J.randomInt(a, b)); - }; } - else { - tmp = (function() { + + setValues = function() { + var a, b; + a = 'undefined' !== typeof that.params.lower ? + (that.params.lower + 1) : 5; + b = 'undefined' !== typeof that.params.upper ? + that.params.upper : (a + 5); + return J.randomString(J.randomInt(a, b)); + }; + } + else { + if (!val) { + val = (function() { var cb; if (that.type === 'float') cb = J.isFloat; else if (that.type === 'int') cb = J.isInt; @@ -683,95 +679,97 @@ }; }; })(); - - setValues = function() { - var p, a, b; - p = that.params; - if (that.type === 'float') return J.random(); - a = 0; - if ('undefined' !== typeof p.lower) { - a = p.leq ? (p.lower - 1) : p.lower; - } - if ('undefined' !== typeof p.upper) { - b = p.ueq ? p.upper : (p.upper - 1); - } - else { - b = 100 + a; - } - return J.randomInt(a, b); - }; } - // Preset inputWidth. - if (this.params.upper) { - if (this.params.upper < 10) this.inputWidth = '100px'; - else if (this.params.upper < 20) this.inputWidth = '200px'; - } + setValues = function() { + var p, a, b; + p = that.params; + if (that.type === 'float') return J.random(); + a = 0; + if ('undefined' !== typeof p.lower) { + a = p.leq ? (p.lower - 1) : p.lower; + } + if ('undefined' !== typeof p.upper) { + b = p.ueq ? p.upper : (p.upper - 1); + } + else { + b = 100 + a; + } + return J.randomInt(a, b); + }; + } + // Preset inputWidth. + if (this.params.upper) { + if (this.params.upper < 10) this.inputWidth = '100px'; + else if (this.params.upper < 20) this.inputWidth = '200px'; } - else if (this.type === 'date') { - if ('undefined' !== typeof opts.format) { - // TODO: use regex. - if (opts.format !== 'mm-dd-yy' && - opts.format !== 'dd-mm-yy' && - opts.format !== 'mm-dd-yyyy' && - opts.format !== 'dd-mm-yyyy' && - opts.format !== 'mm.dd.yy' && - opts.format !== 'dd.mm.yy' && - opts.format !== 'mm.dd.yyyy' && - opts.format !== 'dd.mm.yyyy' && - opts.format !== 'mm/dd/yy' && - opts.format !== 'dd/mm/yy' && - opts.format !== 'mm/dd/yyyy' && - opts.format !== 'dd/mm/yyyy') { - - throw new Error(e + 'date format is invalid. Found: ' + - opts.format); - } - this.params.format = opts.format; - } - else { - this.params.format = 'mm/dd/yyyy'; + + } + else if (this.type === 'date') { + if ('undefined' !== typeof opts.format) { + // TODO: use regex. + if (opts.format !== 'mm-dd-yy' && + opts.format !== 'dd-mm-yy' && + opts.format !== 'mm-dd-yyyy' && + opts.format !== 'dd-mm-yyyy' && + opts.format !== 'mm.dd.yy' && + opts.format !== 'dd.mm.yy' && + opts.format !== 'mm.dd.yyyy' && + opts.format !== 'dd.mm.yyyy' && + opts.format !== 'mm/dd/yy' && + opts.format !== 'dd/mm/yy' && + opts.format !== 'mm/dd/yyyy' && + opts.format !== 'dd/mm/yyyy') { + + throw new Error(e + 'date format is invalid. Found: ' + + opts.format); } + this.params.format = opts.format; + } + else { + this.params.format = 'mm/dd/yyyy'; + } - this.params.sep = this.params.format.charAt(2); - tmp = this.params.format.split(this.params.sep); - this.params.yearDigits = tmp[2].length; - this.params.dayPos = tmp[0].charAt(0) === 'd' ? 0 : 1; - this.params.monthPos = this.params.dayPos ? 0 : 1; - this.params.dateLen = tmp[2].length + 6; - if (opts.minDate) { - tmp = getParsedDate(opts.minDate, this.params); - if (!tmp) { - throw new Error(e + 'minDate must be a Date object. ' + - 'Found: ' + opts.minDate); - } - this.params.minDate = tmp; + this.params.sep = this.params.format.charAt(2); + tmp = this.params.format.split(this.params.sep); + this.params.yearDigits = tmp[2].length; + this.params.dayPos = tmp[0].charAt(0) === 'd' ? 0 : 1; + this.params.monthPos = this.params.dayPos ? 0 : 1; + this.params.dateLen = tmp[2].length + 6; + if (opts.minDate) { + tmp = getParsedDate(opts.minDate, this.params); + if (!tmp) { + throw new Error(e + 'minDate must be a Date object. ' + + 'Found: ' + opts.minDate); } - if (opts.maxDate) { - tmp = getParsedDate(opts.maxDate, this.params); - if (!tmp) { - throw new Error(e + 'maxDate must be a Date object. ' + - 'Found: ' + opts.maxDate); - } - if (this.params.minDate && - this.params.minDate.obj > tmp.obj) { + this.params.minDate = tmp; + } + if (opts.maxDate) { + tmp = getParsedDate(opts.maxDate, this.params); + if (!tmp) { + throw new Error(e + 'maxDate must be a Date object. ' + + 'Found: ' + opts.maxDate); + } + if (this.params.minDate && + this.params.minDate.obj > tmp.obj) { - throw new Error(e + 'maxDate cannot be prior to ' + - 'minDate. Found: ' + tmp.str + - ' < ' + this.params.minDate.str); - } - this.params.maxDate = tmp; + throw new Error(e + 'maxDate cannot be prior to ' + + 'minDate. Found: ' + tmp.str + + ' < ' + this.params.minDate.str); } + this.params.maxDate = tmp; + } - // Preset inputWidth. - if (this.params.yearDigits === 2) this.inputWidth = '100px'; - else this.inputWidth = '150px'; + // Preset inputWidth. + if (this.params.yearDigits === 2) this.inputWidth = '100px'; + else this.inputWidth = '150px'; - // Preset placeholder. - this.placeholder = this.params.format; + // Preset placeholder. + this.placeholder = this.params.format; - tmp = function(value) { + if (!val) { + val = function(value) { var p, tokens, tmp, res, dayNum, l1, l2; p = that.params; @@ -818,7 +816,7 @@ else { // Is it leap year? dayNum = (res.year % 4 === 0 && res.year % 100 !== 0) || - res.year % 400 === 0 ? 29 : 28; + res.year % 400 === 0 ? 29 : 28; } res.month = tmp; // Day. @@ -844,51 +842,53 @@ } return res; }; + } - setValues = function() { - var p, minD, maxD, d, day, month, year; - p = that.params; - minD = p.minDate ? p.minDate.obj : new Date('01/01/1900'); - maxD = p.maxDate ? p.maxDate.obj : undefined; - d = J.randomDate(minD, maxD); - day = d.getDate(); - month = (d.getMonth() + 1); - year = d.getFullYear(); - if (p.yearDigits === 2) year = ('' + year).substr(2); - if (p.monthPos === 0) d = month + p.sep + day; - else d = day + p.sep + month; - d += p.sep + year; - return d; - }; + setValues = function() { + var p, minD, maxD, d, day, month, year; + p = that.params; + minD = p.minDate ? p.minDate.obj : new Date('01/01/1900'); + maxD = p.maxDate ? p.maxDate.obj : undefined; + d = J.randomDate(minD, maxD); + day = d.getDate(); + month = (d.getMonth() + 1); + year = d.getFullYear(); + if (p.yearDigits === 2) year = ('' + year).substr(2); + if (p.monthPos === 0) d = month + p.sep + day; + else d = day + p.sep + month; + d += p.sep + year; + return d; + }; + } + else if (this.type === 'us_state') { + if (opts.abbreviation) { + this.params.abbr = true; + this.inputWidth = '100px'; + } + else { + this.inputWidth = '200px'; } - else if (this.type === 'us_state') { - if (opts.abbreviation) { - this.params.abbr = true; - this.inputWidth = '100px'; + if (opts.territories !== false) { + this.terr = true; + if (this.params.abbr) { + tmp = getUsStatesList('usStatesTerrByAbbrLow'); } else { - this.inputWidth = '200px'; + tmp = getUsStatesList('usStatesTerrLow'); } - if (opts.territories !== false) { - this.terr = true; - if (this.params.abbr) { - tmp = getUsStatesList('usStatesTerrByAbbrLow'); - } - else { - tmp = getUsStatesList('usStatesTerrLow'); - } + } + else { + if (this.params.abbr) { + tmp = getUsStatesList('usStatesByAbbrLow'); } else { - if (this.params.abbr) { - tmp = getUsStatesList('usStatesByAbbrLow'); - } - else { - tmp = getUsStatesList('usStatesLow'); - } + tmp = getUsStatesList('usStatesLow'); } - this.params.usStateVal = tmp; + } + this.params.usStateVal = tmp; - tmp = function(value) { + if (!val) { + val = function(value) { var res; res = { value: value }; if (!that.params.usStateVal[value.toLowerCase()]) { @@ -896,14 +896,16 @@ } return res; }; + } - setValues = function() { - return J.randomKey(that.params.usStateVal); - }; + setValues = function() { + return J.randomKey(that.params.usStateVal); + }; - } - else if (this.type === 'us_zip') { - tmp = function(value) { + } + else if (this.type === 'us_zip') { + if (val) { + val = function(value) { var res; res = { value: value }; if (!isValidUSZip(value)) { @@ -911,83 +913,85 @@ } return res; }; - - setValues = function() { - return Math.floor(Math.random()*90000) + 10000; - }; } - // Lists. + setValues = function() { + return Math.floor(Math.random()*90000) + 10000; + }; + } - else if (this.type === 'list' || - this.type === 'us_city_state_zip') { + // Lists. - if (opts.listSeparator) { - if ('string' !== typeof opts.listSeparator) { - throw new TypeError(e + 'listSeparator must be ' + - 'string or undefined. Found: ' + - opts.listSeperator); - } - this.params.listSep = opts.listSeparator; - } - else { - this.params.listSep = ','; + else if (this.type === 'list' || + this.type === 'us_city_state_zip') { + + if (opts.listSeparator) { + if ('string' !== typeof opts.listSeparator) { + throw new TypeError(e + 'listSeparator must be ' + + 'string or undefined. Found: ' + + opts.listSeperator); } + this.params.listSep = opts.listSeparator; + } + else { + this.params.listSep = ','; + } - if (this.type === 'us_city_state_zip') { + if (this.type === 'us_city_state_zip') { - getUsStatesList('usStatesTerrByAbbr'); - this.params.minItems = this.params.maxItems = 3; - this.params.fixedSize = true; - this.params.itemValidation = function(item, idx) { - if (idx === 2) { - if (!usStatesTerrByAbbr[item.toUpperCase()]) { - return { err: that.getText('usStateAbbrErr') }; - } + getUsStatesList('usStatesTerrByAbbr'); + this.params.minItems = this.params.maxItems = 3; + this.params.fixedSize = true; + this.params.itemValidation = function(item, idx) { + if (idx === 2) { + if (!usStatesTerrByAbbr[item.toUpperCase()]) { + return { err: that.getText('usStateAbbrErr') }; } - else if (idx === 3) { - if (!isValidUSZip(item)) { - return { err: that.getText('usZipErr') }; - } + } + else if (idx === 3) { + if (!isValidUSZip(item)) { + return { err: that.getText('usZipErr') }; } - }; + } + }; - this.placeholder = 'Town' + this.params.listSep + - ' State' + this.params.listSep + ' ZIP'; - } - else { - if ('undefined' !== typeof opts.minItems) { - tmp = J.isInt(opts.minItems, 0); - if (tmp === false) { - throw new TypeError(e + 'minItems must be ' + - 'a positive integer. Found: ' + - opts.minItems); - } - this.params.minItems = tmp; + this.placeholder = 'Town' + this.params.listSep + + ' State' + this.params.listSep + ' ZIP'; + } + else { + if ('undefined' !== typeof opts.minItems) { + tmp = J.isInt(opts.minItems, 0); + if (tmp === false) { + throw new TypeError(e + 'minItems must be ' + + 'a positive integer. Found: ' + + opts.minItems); } - else if (this.required) { - this.params.minItems = 1; + this.params.minItems = tmp; + } + else if (this.required) { + this.params.minItems = 1; + } + if ('undefined' !== typeof opts.maxItems) { + tmp = J.isInt(opts.maxItems, 0); + if (tmp === false) { + throw new TypeError(e + 'maxItems must be ' + + 'a positive integer. Found: ' + + opts.maxItems); } - if ('undefined' !== typeof opts.maxItems) { - tmp = J.isInt(opts.maxItems, 0); - if (tmp === false) { - throw new TypeError(e + 'maxItems must be ' + - 'a positive integer. Found: ' + - opts.maxItems); - } - if (this.params.minItems && - this.params.minItems > tmp) { + if (this.params.minItems && + this.params.minItems > tmp) { - throw new TypeError(e + 'maxItems must be larger ' + - 'than minItems. Found: ' + - tmp + ' < ' + - this.params.minItems); - } - this.params.maxItems = tmp; + throw new TypeError(e + 'maxItems must be larger ' + + 'than minItems. Found: ' + + tmp + ' < ' + + this.params.minItems); } + this.params.maxItems = tmp; } + } - tmp = function(value) { + if (!val) { + val = function(value) { var i, len, v, iVal, err; value = value.split(that.params.listSep); len = value.length; @@ -1039,42 +1043,41 @@ } return { value: value }; }; + } - if (this.type === 'us_city_state_zip') { - setValues = function() { - var sep; - sep = that.params.listSep + ' '; - return J.randomString(8) + sep + - J.randomKey(usStatesTerrByAbbr) + sep + - (Math.floor(Math.random()*90000) + 10000); - }; - } - else { - setValues = function(opts) { - var p, minItems, nItems, i, str, sample; - p = that.params; - minItems = p.minItems || 0; - if (opts.availableValues) { - nItems = J.randomInt(minItems, - opts.availableValues.length); - nItems--; - sample = J.sample(0, (nItems-1)); - } - else { - nItems = J.randomInt(minItems, - p.maxItems || (minItems + 5)); - nItems--; - } - str = ''; - for (i = 0; i < nItems; i++) { - if (i !== 0) str += p.listSep + ' '; - if (sample) str += opts.availableValues[sample[i]]; - else str += J.randomString(J.randomInt(3,10)); - } - return str; - }; - } - + if (this.type === 'us_city_state_zip') { + setValues = function() { + var sep; + sep = that.params.listSep + ' '; + return J.randomString(8) + sep + + J.randomKey(usStatesTerrByAbbr) + sep + + (Math.floor(Math.random()*90000) + 10000); + }; + } + else { + setValues = function(opts) { + var p, minItems, nItems, i, str, sample; + p = that.params; + minItems = p.minItems || 0; + if (opts.availableValues) { + nItems = J.randomInt(minItems, + opts.availableValues.length); + nItems--; + sample = J.sample(0, (nItems-1)); + } + else { + nItems = J.randomInt(minItems, + p.maxItems || (minItems + 5)); + nItems--; + } + str = ''; + for (i = 0; i < nItems; i++) { + if (i !== 0) str += p.listSep + ' '; + if (sample) str += opts.availableValues[sample[i]]; + else str += J.randomString(J.randomInt(3,10)); + } + return str; + }; } // US_Town,State, Zip Code @@ -1091,10 +1094,10 @@ if (value.trim() === '') { if (that.required) res.err = that.getText('emptyErr'); } - else if (tmp) { - res = tmp(value); + else if (val) { + res = val.call(this, value); } - if (that.userValidation) that.userValidation(res); + if (that.userValidation) that.userValidation.call(this, res); return res; }; @@ -1208,7 +1211,9 @@ 'undefined. Found: ' + opts.hint); } this.hint = opts.hint; - if (this.required) this.hint += ' *'; + if (this.required && this.displayRequired) { + this.hint += ' ' + this.requiredMark; + } } else { this.hint = this.getText('autoHint'); @@ -1444,7 +1449,6 @@ * * @return {mixed} The value in the input * - * @see CustomInput.verifyChoice * @see CustomInput.reset */ CustomInput.prototype.getValues = function(opts) { diff --git a/widgets/CustomInputGroup.js b/widgets/CustomInputGroup.js index 039e337..93276dd 100644 --- a/widgets/CustomInputGroup.js +++ b/widgets/CustomInputGroup.js @@ -1,6 +1,6 @@ /** * # CustomInputGroup - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a table that groups together several custom input widgets @@ -21,23 +21,16 @@ CustomInputGroup.description = 'Groups together and manages sets of ' + 'CustomInput widgets.'; - CustomInputGroup.title = false; CustomInputGroup.className = 'custominput custominputgroup'; CustomInputGroup.separator = '::'; CustomInputGroup.texts.autoHint = function(w) { - if (w.requiredChoice) return '*'; + if (w.requiredChoice && w.displayRequired) return w.requiredMark; else return false; }; CustomInputGroup.texts.inputErr = 'One or more errors detected.'; - // ## Dependencies - - CustomInputGroup.dependencies = { - JSUS: {} - }; - /** * ## CustomInputGroup constructor * @@ -332,6 +325,8 @@ * - res: the validation result of the single input * - input: the custom input that fired oninput * - widget: a reference to this widget + * + * @see addCustomInput */ this.oninput = null; @@ -473,7 +468,7 @@ opts.validation); } - // Set the validation function. + // Set the oninput function. if ('function' === typeof opts.oninput) { this._oninput = opts.oninput; @@ -500,7 +495,9 @@ // Set the hint, if any. if ('string' === typeof opts.hint) { this.hint = opts.hint; - if (this.requiredChoice) this.hint += ' *'; + if (this.requiredChoice && this.displayRequired) { + this.hint += ' ' + this.requiredMark; + } } else if ('undefined' !== typeof opts.hint) { throw new TypeError('CustomInputGroup.init: hint must ' + @@ -1054,6 +1051,14 @@ if ('undefined' === typeof s.requiredChoice && that.requiredChoice) { s.requiredChoice = that.requiredChoice; } + + if ('undefined' === typeof s.displayRequired) { + s.displayRequired = that.displayRequired; + } + + if ('undefined' === typeof s.requiredMark) { + s.requiredMark = that.requiredMark; + } if ('undefined' === typeof s.timeFrom) s.timeFrom = that.timeFrom; @@ -1133,7 +1138,7 @@ id: that.id + '_summary', storeRef: false, title: false, - panel: false, + // panel: false, className: 'custominputgroup-summary', disabled: true }, that.sharedOptions); diff --git a/widgets/D3.js b/widgets/D3.js index 8e6b9ca..042e20f 100644 --- a/widgets/D3.js +++ b/widgets/D3.js @@ -34,8 +34,7 @@ // ## Dependencies D3.dependencies = { - d3: {}, - JSUS: {} + d3: {} }; function D3 (options) { diff --git a/widgets/DebugWall.js b/widgets/DebugWall.js index 5c11d2a..6f87242 100644 --- a/widgets/DebugWall.js +++ b/widgets/DebugWall.js @@ -24,12 +24,6 @@ DebugWall.title = 'Debug Wall'; DebugWall.className = 'debugwall'; - // ## Dependencies - - DebugWall.dependencies = { - JSUS: {} - }; - /** * ## DebugWall constructor * diff --git a/widgets/DisconnectBox.js b/widgets/DisconnectBox.js index d1b9e1a..a5c0f17 100644 --- a/widgets/DisconnectBox.js +++ b/widgets/DisconnectBox.js @@ -1,6 +1,6 @@ /** * # DisconnectBox - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Shows a disconnect button @@ -18,7 +18,6 @@ DisconnectBox.version = '0.4.0'; DisconnectBox.description = 'Monitors and handles disconnections'; - DisconnectBox.title = false; DisconnectBox.panel = false; DisconnectBox.className = 'disconnectbox'; diff --git a/widgets/DoneButton.js b/widgets/DoneButton.js index 19bed33..1b01185 100644 --- a/widgets/DoneButton.js +++ b/widgets/DoneButton.js @@ -1,6 +1,6 @@ /** * # DoneButton - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates a button that if pressed emits node.done() @@ -19,16 +19,10 @@ DoneButton.description = 'Creates a button that if ' + 'pressed emits node.done().'; - DoneButton.title = false; + DoneButton.panel = false; DoneButton.className = 'donebutton'; DoneButton.texts.done = 'Done'; - // ## Dependencies - - DoneButton.dependencies = { - JSUS: {} - }; - /** * ## DoneButton constructor * @@ -53,8 +47,8 @@ this.button = options.button; } else if ('undefined' === typeof options.button) { - this.button = document.createElement('input'); - this.button.type = 'button'; + this.button = document.createElement('button'); + // this.button.type = 'button'; } else { throw new TypeError('DoneButton constructor: options.button must ' + @@ -64,6 +58,10 @@ this.button.onclick = function() { if (that.onclick && false === that.onclick()) return; + if (node.game.isWidgetStep()) { + // Widget has a next visualization in the same step. + if (node.widgets.last.next() !== false) return; + } if (node.done()) that.disable(); }; @@ -136,28 +134,30 @@ if (tmp) this.button.id = tmp; // Button className. - if ('undefined' === typeof opts.className) { + if ('undefined' === typeof opts.classNameBtn) { tmp = 'btn btn-lg btn-primary'; } - else if (opts.className === false) { + else if (opts.classNameBtn === false) { tmp = ''; } - else if ('string' === typeof opts.className) { - tmp = opts.className; + else if ('string' === typeof opts.classNameBtn) { + tmp = opts.classNameBtn; } - else if (J.isArray(opts.className)) { - tmp = opts.className.join(' '); + else if (J.isArray(opts.classNameBtn)) { + tmp = opts.classNameBtn.join(' '); } else { - throw new TypeError('DoneButton.init: className must ' + + throw new TypeError('DoneButton.init: classNameBtn must ' + 'be string, array, or undefined. Found: ' + - opts.className); + opts.classNameBtn); } this.button.className = tmp; // Button text. - this.button.value = 'string' === typeof opts.text ? - opts.text : this.getText('done'); + // this.button.value = 'string' === typeof opts.text ? + // opts.text : this.getText('done'); + this.button.innerHTML = 'string' === typeof opts.text ? + opts.text : this.getText('done'); this.disableOnDisconnect = 'undefined' === typeof opts.disableOnDisconnect ? @@ -172,14 +172,7 @@ 'be number or undefined. Found: ' + tmp); } - tmp = opts.onclick; - if (tmp) { - if ('function' !== typeof tmp) { - throw new TypeError('DoneButton.init: onclick must function ' + - 'or undefined. Found: ' + tmp); - } - this.onclick = tmp; - } + setOnClick(this, opts.onclick); }; DoneButton.prototype.append = function() { @@ -202,10 +195,9 @@ // then unlocked by GameWindow, but otherwise it must be // done here. node.on('PLAYING', function() { - var prop, step, delay; + var prop, delay; - step = node.game.getCurrentGameStage(); - prop = node.game.plot.getProperty(step, 'donebutton'); + prop = node.game.getProperty('donebutton'); if (prop === false || (prop && prop.enableOnPlaying === false)) { // It might be disabled already, but we do it again. that.disable(); @@ -230,8 +222,17 @@ that.enable(); } } - if ('string' === typeof prop) that.button.value = prop; - else if (prop && prop.text) that.button.value = prop.text; + if ('string' === typeof prop) { + // that.button.value = prop; + that.button.innerHTML = prop; + } + else if (prop) { + // if (prop.text) that.button.value = prop.text; + if (prop.text) that.button.innerHTML = prop.text; + if (prop.onclick) setOnClick(that, prop.onclick, true); + } + + }); if (this.disableOnDisconnect) { @@ -264,12 +265,15 @@ var oldText, that; if (duration) { that = this; - oldText = this.button.value; + // oldText = this.button.value; + oldText = this.button.innerHTML; node.timer.setTimeout(function() { - that.button.value = oldText; + // that.button.value = oldText; + that.button.innerHTML = oldText; }, duration); } - this.button.value = text; + // this.button.value = text; + this.button.innerHTML = text; }; /** @@ -296,4 +300,26 @@ this.emit('enabled', opts); }; + + // ## Helper functions. + + // Checks and sets the onclick function. + function setOnClick(that, onclick, step) { + var str; + if ('undefined' !== typeof onclick) { + if ('function' !== typeof onclick && onclick !== null) { + str = 'DoneButton.init'; + if (step) str += ' (step property)'; + throw new TypeError(str + ': onclick must be function, null,' + + ' or undefined. Found: ' + onclick); + } + that.onclick = onclick; + } + if (step) { + node.once('REALLY_DONE', function() { + that.onclick = null; + }); + } + } + })(node); diff --git a/widgets/Dropdown.js b/widgets/Dropdown.js index d835254..b20eb7b 100644 --- a/widgets/Dropdown.js +++ b/widgets/Dropdown.js @@ -1,13 +1,23 @@ +/** + * # DropDown + * Copyright(c) 2023 Stefano Balietti + * MIT Licensed + * + * Creates a customizable dropdown menu + * + * www.nodegame.org + */ (function(node) { node.widgets.register('Dropdown', Dropdown); // Meta-data. - Dropdown.version = '0.2.0'; + Dropdown.version = '0.4.0'; Dropdown.description = 'Creates a configurable dropdown menu.'; Dropdown.texts = { + // Texts here (more info on this later). error: function (w, value) { if (value !== null && w.fixedChoice && @@ -25,8 +35,6 @@ } }; - // Title is displayed in the header. - Dropdown.title = false; // Classname is added to the widgets. Dropdown.className = 'dropdown'; @@ -48,6 +56,13 @@ */ this.mainText = null; + /** + * ### Dropdown.hint + * + * An additional text with information in lighter font + */ + this.hint = null; + /** * ### Dropdown.labelText * @@ -79,10 +94,17 @@ /** * ### Dropdown.menu * - * Holder of the HTML element (datalist or select) + * Holder of the selected value (input or select) */ this.menu = null; + /** + * ### Dropdown.datalist + * + * Holder of the options for the datalist element + */ + this.datalist = null; + /** * ### Dropdown.listener * @@ -127,7 +149,7 @@ // Call onchange, if any. if (that.onchange) { - that.onchange(that.currentChoice, that); + that.onchange(that.currentChoice, menu, that); } }; @@ -257,214 +279,249 @@ } - Dropdown.prototype.init = function (options) { + Dropdown.prototype.init = function (opts) { // Init widget variables, but do not create // HTML elements, they should be created in append. var tmp; if (!this.id) { - throw new TypeError('Dropdown.init: options.id is missing'); + throw new TypeError('Dropdown.init: id is missing'); } - if ('string' === typeof options.mainText) { - this.mainText = options.mainText; + if ('string' === typeof opts.mainText) { + this.mainText = opts.mainText; } - else if ('undefined' !== typeof options.mainText) { - throw new TypeError('Dropdown.init: options.mainText must ' + + else if ('undefined' !== typeof opts.mainText) { + throw new TypeError('Dropdown.init: mainText must ' + 'be string or undefined. Found: ' + - options.mainText); + opts.mainText); } // Set the labelText, if any. - if ('string' === typeof options.labelText) { - this.labelText = options.labelText; + if ('string' === typeof opts.labelText) { + this.labelText = opts.labelText; } - else if ('undefined' !== typeof options.labelText) { - throw new TypeError('Dropdown.init: options.labelText must ' + + else if ('undefined' !== typeof opts.labelText) { + throw new TypeError('Dropdown.init: labelText must ' + 'be string or undefined. Found: ' + - options.labelText); + opts.labelText); } // Set the placeholder text, if any. - if ('string' === typeof options.placeholder) { - this.placeholder = options.placeholder; + if ('string' === typeof opts.placeholder) { + this.placeholder = opts.placeholder; } - else if ('undefined' !== typeof options.placeholder) { - throw new TypeError('Dropdown.init: options.placeholder must ' + + else if ('undefined' !== typeof opts.placeholder) { + throw new TypeError('Dropdown.init: placeholder must ' + 'be string or undefined. Found: ' + - options.placeholder); + opts.placeholder); } // Add the choices. - if ('undefined' !== typeof options.choices) { - this.choices = options.choices; + if ('undefined' !== typeof opts.choices) { + this.choices = opts.choices; } // Option requiredChoice, if any. - if ('boolean' === typeof options.requiredChoice) { - this.requiredChoice = options.requiredChoice; + if ('boolean' === typeof opts.requiredChoice) { + this.requiredChoice = opts.requiredChoice; } - else if ('undefined' !== typeof options.requiredChoice) { - throw new TypeError('Dropdown.init: options.requiredChoice ' + + else if ('undefined' !== typeof opts.requiredChoice) { + throw new TypeError('Dropdown.init: requiredChoice ' + 'be boolean or undefined. Found: ' + - options.requiredChoice); + opts.requiredChoice); } // Add the correct choices. - if ('undefined' !== typeof options.correctChoice) { + if ('undefined' !== typeof opts.correctChoice) { if (this.requiredChoice) { throw new Error('Dropdown.init: cannot specify both ' + - 'options requiredChoice and correctChoice'); + 'opts requiredChoice and correctChoice'); } - if (J.isArray(options.correctChoice) && - options.correctChoice.length > options.choices.length) { - throw new Error('Dropdown.init: options.correctChoice ' + - 'length cannot exceed options.choices length'); + if (J.isArray(opts.correctChoice) && + opts.correctChoice.length > opts.choices.length) { + throw new Error('Dropdown.init: correctChoice ' + + 'length cannot exceed opts.choices length'); } else { - this.correctChoice = options.correctChoice; + this.correctChoice = opts.correctChoice; } } // Option fixedChoice, if any. - if ('boolean' === typeof options.fixedChoice) { - this.fixedChoice = options.fixedChoice; + if ('boolean' === typeof opts.fixedChoice) { + this.fixedChoice = opts.fixedChoice; } - else if ('undefined' !== typeof options.fixedChoice) { - throw new TypeError('Dropdown.init: options.fixedChoice ' + + else if ('undefined' !== typeof opts.fixedChoice) { + throw new TypeError('Dropdown.init: fixedChoice ' + 'be boolean or undefined. Found: ' + - options.fixedChoice); + opts.fixedChoice); } - if ("undefined" === typeof options.tag || - "datalist" === options.tag || - "select" === options.tag) { - this.tag = options.tag; + if ("undefined" === typeof opts.tag) { + this.tag = "datalist"; + } + else if ("datalist" === opts.tag || "select" === opts.tag) { + this.tag = opts.tag; } else { - throw new TypeError('Dropdown.init: options.tag must ' + - 'be "datalist" or "select". Found: ' + - options.tag); + throw new TypeError('Dropdown.init: tag must ' + + 'be "datalist", "select" or undefined. Found: ' + opts.tag); } // Set the main onchange listener, if any. - if ('function' === typeof options.listener) { + if ('function' === typeof opts.listener) { this.listener = function (e) { - options.listener.call(this, e); + opts.listener.call(this, e); }; } - else if ('undefined' !== typeof options.listener) { - throw new TypeError('Dropdown.init: opts.listener must ' + + else if ('undefined' !== typeof opts.listener) { + throw new TypeError('Dropdown.init: listener must ' + 'be function or undefined. Found: ' + - options.listener); + opts.listener); } // Set an additional onchange, if any. - if ('function' === typeof options.onchange) { - this.onchange = options.onchange; + if ('function' === typeof opts.onchange) { + this.onchange = opts.onchange; } - else if ('undefined' !== typeof options.onchange) { - throw new TypeError('Dropdownn.init: opts.onchange must ' + + else if ('undefined' !== typeof opts.onchange) { + throw new TypeError('Dropdownn.init: onchange must ' + 'be function or undefined. Found: ' + - options.onchange); + opts.onchange); } // Set an additional validation, if any. - if ('function' === typeof options.validation) { - this.validation = options.validation; + if ('function' === typeof opts.validation) { + this.validation = opts.validation; } - else if ('undefined' !== typeof options.validation) { - throw new TypeError('Dropdownn.init: opts.validation must ' + + else if ('undefined' !== typeof opts.validation) { + throw new TypeError('Dropdownn.init: validation must ' + 'be function or undefined. Found: ' + - options.validation); + opts.validation); } // Option shuffleChoices, default false. - if ('undefined' === typeof options.shuffleChoices) tmp = false; - else tmp = !!options.shuffleChoices; + if ('undefined' === typeof opts.shuffleChoices) tmp = false; + else tmp = !!opts.shuffleChoices; this.shuffleChoices = tmp; - if (options.width) { - if ('string' !== typeof options.width) { + if (opts.width) { + if ('string' !== typeof opts.width) { throw new TypeError('Dropdownn.init:width must be string or ' + - 'undefined. Found: ' + options.width); + 'undefined. Found: ' + opts.width); } - this.inputWidth = options.width; + this.inputWidth = opts.width; } // Validation Speed - if ('undefined' !== typeof options.validationSpeed) { + if ('undefined' !== typeof opts.validationSpeed) { - tmp = J.isInt(options.valiadtionSpeed, 0, undefined, true); + tmp = J.isInt(opts.valiadtionSpeed, 0, undefined, true); if (tmp === false) { throw new TypeError('Dropdownn.init: validationSpeed must ' + ' a non-negative number or undefined. Found: ' + - options.validationSpeed); + opts.validationSpeed); } this.validationSpeed = tmp; } + // Hint (must be done after requiredChoice) + tmp = opts.hint; + if ('function' === typeof tmp) { + tmp = tmp.call(this); + if ('string' !== typeof tmp && false !== tmp) { + throw new TypeError('Dropdown.init: hint cb must ' + + 'return string or false. Found: ' + + tmp); + } + } + if ('string' === typeof tmp || false === tmp) { + this.hint = tmp; + } + else if ('undefined' !== typeof tmp) { + throw new TypeError('Dropdown.init: hint must ' + + 'be a string, false, or undefined. Found: ' + + tmp); + } + if (this.requiredChoice && tmp !== false && + opts.displayRequired !== false) { + + this.hint = tmp ? + (this.hint + ' ' + this.requiredMark) : ' ' + this.requiredMark; + } + } // Implements the Widget.append method. Dropdown.prototype.append = function () { - if (W.gid(this.id)) { throw new Error('Dropdown.append: id is not unique: ' + this.id); } - var text = this.text; - var label = this.label; + var mt; - text = W.get('p'); - text.innerHTML = this.mainText; - text.id = 'p'; - this.bodyDiv.appendChild(text); + if (this.mainText) { + mt = W.append('span', this.bodyDiv, { + className: 'dropdown-maintext', + innerHTML: this.mainText + }); + } - label = W.get('label'); - label.innerHTML = this.labelText - this.bodyDiv.appendChild(label); + // Hint. + if (this.hint) { + W.append('span', mt || this.bodyDiv, { + className: 'dropdown-hint', + innerHTML: this.hint + }); + } + + if (this.labelText) { + W.append('label', this.bodyDiv, { + innerHTML: this.labelText + }); + } this.setChoices(this.choices, true); this.errorBox = W.append('div', this.bodyDiv, { - className: 'errbox', id: 'errbox' + className: 'errbox' }); }; Dropdown.prototype.setChoices = function (choices, append) { - var tag, option, order; - var select, datalist, input, create; - var i, len; + var isDatalist, order; + var select; + var i, len, value, name; // TODO validate choices. this.choices = choices; if (!append) return; - create = false; - if (this.menu) this.menu.innerHTML = ''; - else create = true; - - if (create) { - tag = this.tag; - if (tag === "datalist" || "undefined" === typeof tag) { + isDatalist = this.tag === 'datalist'; - datalist = W.get('datalist'); - datalist.id = "dropdown"; + // Create the structure from scratch or just clear all options. + if (this.menu) { + select = isDatalist ? this.datalist : this.menu; + select.innerHTML = ''; + } + else { + if (isDatalist) { - input = W.get('input'); - input.setAttribute('list', datalist.id); - input.id = this.id; - input.autocomplete = "off"; + this.menu = W.add('input', this.bodyDiv, { + id: this.id, + autocomplete: 'off' + }); - this.bodyDiv.appendChild(input); - this.bodyDiv.appendChild(datalist); - this.menu = input; + this.datalist = select = W.add('datalist', this.bodyDiv, { + id: this.id + "_datalist" + }); + this.menu.setAttribute('list', this.datalist.id); } else { @@ -481,17 +538,19 @@ // Adding placeholder. if (this.placeholder) { - if (tag === "datalist") { + if (isDatalist) { this.menu.placeholder = this.placeholder; } else { - option = W.get('option'); - option.value = ""; - option.innerHTML = this.placeholder; - option.setAttribute("disabled", ""); - option.setAttribute("selected", ""); - option.setAttribute("hidden", ""); - this.menu.appendChild(option); + + W.add('option', this.menu, { + value: '', + innerHTML: this.placeholder, + // Makes the placeholder unselectable after first click. + disabled: '', + selected: '', + hidden: '' + }); } } @@ -500,14 +559,29 @@ order = J.seq(0, len - 1); if (this.shuffleChoices) order = J.shuffle(order); for (i = 0; i < len; i++) { - option = W.get('option'); - option.value = choices[order[i]]; - option.innerHTML = choices[order[i]]; - this.menu.appendChild(option); + + // Determining value and name of choice. + value = name = choices[order[i]]; + if ('object' === typeof value) { + if ('undefined' !== typeof value.value) { + name = value.name; + value = value.value; + } + else if (J.isArray(value)) { + name = value[1]; + value = value[0]; + } + } + + // select is a datalist element if tag is "datalist". + W.add('option', select, { + value: value, + innerHTML: name + }); } this.enable(); - } + }; /** * ### Dropdown.verifyChoice @@ -521,27 +595,38 @@ * - correctChoice: the choices are compared against correct ones. * - fixedChoice: compares the choice with given choices. * - * @return {boolean|null} TRUE if current choice is correct, - * FALSE if it is not correct, or NULL if no correct choice - * was set + * If a custom validation is set, it will executed with the current + * result of the validation. + * + * @return {object} res The result of the verification and validation. + * The object is of the type: + * ```js + * { + * value: boolean/null // TRUE if current choice is correct, + * // FALSE if it is not correct, + * // or NULL if no correct choice was set. + * } + * ``` + * The custom validation function, if any is set, can add + * information to the return object. * + * @see Dropdown.validation */ Dropdown.prototype.verifyChoice = function () { var that = this; var correct = this.correctChoice; var current = this.currentChoice; + var correctOptions; var res = { value: '' }; if (this.tag === "select" && this.numberOfChanges === 0) { - - current = this.currentChoice = this.menu.value; - + current = this.currentChoice = this.menu.value || null; } if (this.requiredChoice) { - res.value = current !== null; + res.value = current !== null && current !== this.placeholder; } // If no correct choice is set return null. @@ -553,7 +638,7 @@ res.value = current === this.choices[correct]; } if (J.isArray(correct)) { - var correctOptions = correct.map(function (x) { + correctOptions = correct.map(function (x) { return that.choices[x]; }); res.value = correctOptions.indexOf(current) >= 0; @@ -563,13 +648,7 @@ if (this.choices.indexOf(current) < 0) res.value = false; } - if (this.validation) { - if (undefined === typeof res) { - throw new TypeError('something'); - } - - this.validation(this.currentChoice, res); - } + if (this.validation) this.validation(this.currentChoice, res); return res; }; @@ -627,6 +706,135 @@ this.emit('unhighlighted'); }; + /** + * ### Dropdown.selectChoice + * + * Select a given choice in the datalist or select tag. + * + * @param {string|number} choice. Its value depends on the tag. + * + * - "datalist": a string, if number it is resolved to the name of + * the choice at idx === choice. + * - "select": a number, if string it is resolved to the idx of + * the choice name === choice. Value -1 will unselect all choices. + * + * @return {string|number} idx The resolved name or index + */ + Dropdown.prototype.selectChoice = function (choice) { + // idx is a number if tag is select and a string if tag is datalist. + var idx; + + if (!this.choices || !this.choices.length) return; + if ('undefined' === typeof choice) return; + + idx = choice; + + if (this.tag === 'select') { + if ('string' === typeof choice) { + idx = getIdxOfChoice(this, choice); + if (idx === -1) { + node.warn('Dropdown.selectChoice: choice not found: ' + + choice); + return; + } + } + else if (null === choice || false === choice) { + idx = 0; + } + else if ('number' === typeof choice) { + // 1-based. 0 is for deselecting everything. + idx++; + } + else { + throw new TypeError('Dropdown.selectChoice: invalid choice: ' + + choice); + } + + // Set the choice. + this.menu.selectedIndex = idx; + } + else { + + if ('number' === typeof choice) { + idx = getChoiceOfIdx(this, choice); + if ('undefined' === typeof idx) { + node.warn('Dropdown.selectChoice: choice not found: ' + + choice); + return; + } + } + else if ('string' !== typeof choice) { + throw new TypeError('Dropdown.selectChoice: invalid choice: ' + + choice); + } + + this.menu.value = idx; + } + + // Simulate event. + this.listener({ target: this.menu }); + + return idx; + }; + + /** + * ### Dropdown.setValues + * + * Set the values on the dropdown menu + * + * @param {object} opts Optional. Configuration options. + * + * @see Dropdown.verifyChoice + */ + Dropdown.prototype.setValues = function(opts) { + var choice, correctChoice; + var i, len, j, lenJ; + + if (!this.choices || !this.choices.length) { + throw new Error('Dropdown.setValues: no choices found.'); + } + if ('undefined' === typeof opts) opts = {}; + + // TODO: this code is duplicated from ChoiceTable. + if (opts.correct && this.correctChoice !== null) { + + // Make it an array (can be a string). + correctChoice = J.isArray(this.correctChoice) ? + this.correctChoice : [this.correctChoice]; + + i = -1, len = correctChoice.length; + for ( ; ++i < len ; ) { + choice = parseInt(correctChoice[i], 10); + if (this.shuffleChoices) { + j = -1, lenJ = this.order.length; + for ( ; ++j < lenJ ; ) { + if (this.order[j] === choice) { + choice = j; + break; + } + } + } + + this.selectChoice(choice); + } + return; + } + + // Set values, random or pre-set. + if ('number' === typeof opts || 'string' === typeof opts) { + opts = { values: opts }; + } + else if (opts && 'undefined' === typeof opts.values) { + // Select has index 0 for deselecting + opts = { values: J.randomInt(this.choices.length) -1 }; + // TODO: merge other options if they are used by selectChoice. + } + + // If other options are used (rather than values) change TODO above. + this.selectChoice(opts.values); + + }; + /** * ### Dropdown.getValues * @@ -639,9 +847,9 @@ * @see Dropdown.verifyChoice */ Dropdown.prototype.getValues = function (opts) { - var obj; + var obj, verif; opts = opts || {}; - var verif = this.verifyChoice().value; + verif = this.verifyChoice().value; obj = { id: this.id, @@ -660,6 +868,7 @@ if (null !== this.correctChoice || null !== this.requiredChoice || null !== this.fixedChoice) { + obj.isCorrect = verif; if (!obj.isCorrect && opts.highlight) this.highlight(); } @@ -669,6 +878,17 @@ return obj; }; + /** + * ### Dropdown.isChoiceDone + * + * Returns TRUE if the choice/s has been done, if requested + * + * @return {boolean} TRUE if the choice is done + */ + Dropdown.prototype.isChoiceDone = function() { + return this.verifyChoice().value !== false; + }; + /** * ### Dropdown.listeners * @@ -692,7 +912,7 @@ /** * ### Dropdown.disable * - * Enables the dropdown menu + * Disables the dropdown menu */ Dropdown.prototype.disable = function () { if (this.disabled === true) return; @@ -716,4 +936,36 @@ this.emit('enabled'); }; + // ## Helper methods. + + + function getChoiceOfIdx(that, idx) { + return extractChoice(that.choices[idx]); + + } + + function extractChoice(c) { + if ('object' === typeof c) { + if ('undefined' !== typeof c.name) c = c.name; + else c = c[1]; + } + return c; + } + + function getIdxOfChoice(that, choice) { + var i, len, c; + len = that.choices.length; + for (i = 0; i < len; i++) { + c = that.choices[i]; + // c can be string, object, or array. + if ('object' === typeof c) { + if ('undefined' !== typeof c.name) c = c.name; + else c = c[1]; + } + if (c === choice) return i; + } + return -1; + } + + })(node); diff --git a/widgets/EmailForm.js b/widgets/EmailForm.js index 6bc8dab..6d8992f 100644 --- a/widgets/EmailForm.js +++ b/widgets/EmailForm.js @@ -1,6 +1,6 @@ /** * # EmailForm - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays a form to input email @@ -18,7 +18,6 @@ EmailForm.version = '0.13.1'; EmailForm.description = 'Displays a configurable email form.'; - EmailForm.title = false; EmailForm.className = 'emailform'; EmailForm.texts = { diff --git a/widgets/EndScreen.js b/widgets/EndScreen.js index bea5591..3d36709 100644 --- a/widgets/EndScreen.js +++ b/widgets/EndScreen.js @@ -1,6 +1,6 @@ /** * # EndScreen - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Creates an interface to display final earnings, exit code, etc. @@ -16,11 +16,10 @@ // ## Add Meta-data - EndScreen.version = '0.7.2'; + EndScreen.version = '0.8.0'; EndScreen.description = 'Game end screen. With end game message, ' + 'email form, and exit code.'; - EndScreen.title = false; EndScreen.className = 'endscreen'; EndScreen.texts = { @@ -50,11 +49,11 @@ * * Creates a new instance of EndScreen * - * @param {object} options Configuration options + * @param {object} opts Configuration options * * @see EndScreen.init */ - function EndScreen(options) { + function EndScreen(opts) { /** * ### EndScreen.showEmailForm @@ -146,86 +145,101 @@ * * If TRUE, after being appended it sends a 'WIN' message to server * - * Default: FALSE + * Default: TRUE */ - this.askServer = options.askServer || false; + this.askServer = true; + + /** + * ### EndScreen.maxDecimals + * + * The max number of decimals in each number in the win field + * + * Decimals are not enforceed, i.e., if a number has no decimals, + * it will be left as is. + * + * FALSE to allow for any number of decimals. + * + * It only applies to incoming data from server. + * + * Default: 2 + */ + this.maxDec = 2; } - EndScreen.prototype.init = function(options) { + EndScreen.prototype.init = function(opts) { + + if ('undefined' !== typeof opts.askServer) { + this.askServer = !!opts.askServer; + } - if (options.email === false) { + if (opts.email === false) { this.showEmailForm = false; } - else if ('boolean' === typeof options.showEmailForm) { - this.showEmailForm = options.showEmailForm; + else if ('boolean' === typeof opts.showEmailForm) { + this.showEmailForm = opts.showEmailForm; } - else if ('undefined' !== typeof options.showEmailForm) { - throw new TypeError('EndScreen.init: ' + - 'options.showEmailForm ' + - 'must be boolean or undefined. ' + - 'Found: ' + options.showEmailForm); + else if ('undefined' !== typeof opts.showEmailForm) { + throw new TypeError('EndScreen.init: opts.showEmailForm ' + + 'must be boolean or undefined. Found: ' + + opts.showEmailForm); } - if (options.feedback === false) { + if (opts.feedback === false) { this.showFeedbackForm = false; } - else if ('boolean' === typeof options.showFeedbackForm) { - this.showFeedbackForm = options.showFeedbackForm; + else if ('boolean' === typeof opts.showFeedbackForm) { + this.showFeedbackForm = opts.showFeedbackForm; } - else if ('undefined' !== typeof options.showFeedbackForm) { - throw new TypeError('EndScreen.init: ' + - 'options.showFeedbackForm ' + - 'must be boolean or undefined. ' + - 'Found: ' + options.showFeedbackForm); + else if ('undefined' !== typeof opts.showFeedbackForm) { + throw new TypeError('EndScreen.init: opts.showFeedbackForm ' + + 'must be boolean or undefined. Found: ' + + opts.showFeedbackForm); } - if (options.totalWin === false) { + if (opts.totalWin === false) { this.showTotalWin = false; } - else if ('boolean' === typeof options.showTotalWin) { - this.showTotalWin = options.showTotalWin; + else if ('boolean' === typeof opts.showTotalWin) { + this.showTotalWin = opts.showTotalWin; } - else if ('undefined' !== typeof options.showTotalWin) { - throw new TypeError('EndScreen.init: ' + - 'options.showTotalWin ' + - 'must be boolean or undefined. ' + - 'Found: ' + options.showTotalWin); + else if ('undefined' !== typeof opts.showTotalWin) { + throw new TypeError('EndScreen.init: opts.showTotalWin ' + + 'must be boolean or undefined. Found: ' + + opts.showTotalWin); } - if (options.exitCode === false) { - options.showExitCode !== false + if (opts.exitCode === false) { + opts.showExitCode !== false } - else if ('boolean' === typeof options.showExitCode) { - this.showExitCode = options.showExitCode; + else if ('boolean' === typeof opts.showExitCode) { + this.showExitCode = opts.showExitCode; } - else if ('undefined' !== typeof options.showExitCode) { - throw new TypeError('EndScreen.init: ' + - 'options.showExitCode ' + - 'must be boolean or undefined. ' + - 'Found: ' + options.showExitCode); + else if ('undefined' !== typeof opts.showExitCode) { + throw new TypeError('EndScreen.init: opts.showExitCode ' + + 'must be boolean or undefined. Found: ' + + opts.showExitCode); } - if ('string' === typeof options.totalWinCurrency && - options.totalWinCurrency.trim() !== '') { + if ('string' === typeof opts.totalWinCurrency && + opts.totalWinCurrency.trim() !== '') { - this.totalWinCurrency = options.totalWinCurrency; + this.totalWinCurrency = opts.totalWinCurrency; } - else if ('undefined' !== typeof options.totalWinCurrency) { + else if ('undefined' !== typeof opts.totalWinCurrency) { throw new TypeError('EndScreen.init: ' + - 'options.totalWinCurrency must be undefined ' + + 'opts.totalWinCurrency must be undefined ' + 'or a non-empty string. Found: ' + - options.totalWinCurrency); + opts.totalWinCurrency); } - if (options.totalWinCb) { - if ('function' === typeof options.totalWinCb) { - this.totalWinCb = options.totalWinCb; + if (opts.totalWinCb) { + if ('function' === typeof opts.totalWinCb) { + this.totalWinCb = opts.totalWinCb; } else { - throw new TypeError('EndScreen.init: ' + - 'options.totalWinCb ' + - 'must be function or undefined. ' + - 'Found: ' + options.totalWinCb); + throw new TypeError('EndScreen.init: opts.totalWinCb ' + + 'must be function or undefined. Found: ' + + opts.totalWinCb); } } @@ -244,13 +258,13 @@ errString: 'Please enter a valid email and retry' }, setMsg: true // Sends a set message for logic's db. - }, options.email)); + }, opts.email)); } if (this.showFeedbackForm) { this.feedback = node.widgets.get('Feedback', J.mixin( { storeRef: false, minChars: 50, setMsg: true }, - options.feedback)); + opts.feedback)); } }; @@ -272,6 +286,7 @@ var totalWinElement, totalWinParaElement, totalWinInputElement; var exitCodeElement, exitCodeParaElement, exitCodeInputElement; var exitCodeBtn, exitCodeGroup; + var basePay; var that = this; endScreenElement = document.createElement('div'); @@ -322,7 +337,8 @@ exitCodeGroup.className = 'input-group-btn'; exitCodeBtn = document.createElement('button'); - exitCodeBtn.className = 'btn btn-default endscreen-copy-btn'; + exitCodeBtn.className = + 'btn btn-outline-secondary endscreen-copy-btn'; exitCodeBtn.innerHTML = this.getText('copyButton'); exitCodeBtn.type = 'button'; exitCodeBtn.onclick = function() { @@ -338,6 +354,13 @@ this.exitCodeInputElement = exitCodeInputElement; } + basePay = node.game.settings.BASE_PAY; + if ('undefined' !== typeof basePay) { + this.updateDisplay({ + basePay: basePay, total: basePay, exitCode: 'N/A' + }); + } + if (this.showEmailForm) { node.widgets.append(this.emailForm, endScreenElement, { title: false, @@ -373,7 +396,8 @@ document.execCommand('copy', false); inp.remove(); alert(this.getText('exitCopyMsg')); - } catch (err) { + } + catch (err) { alert(this.getText('exitCopyError')); } }; @@ -389,7 +413,7 @@ */ EndScreen.prototype.updateDisplay = function(data) { var preWin, totalWin, totalRaw, exitCode; - var totalHTML, exitCodeHTML, ex, err; + var totalHTML, exitCodeHTML, ex, err, i, len; if (this.totalWinCb) { totalWin = this.totalWinCb(data, this); @@ -415,37 +439,39 @@ preWin = ''; if ('undefined' !== typeof data.basePay) { - preWin = data.basePay; - + preWin = enforceDecimals(data.basePay, this.maxDec); } if ('undefined' !== typeof data.bonus && data.showBonus !== false) { if (preWin !== '') preWin += ' + '; - preWin += data.bonus; + preWin += enforceDecimals(data.bonus, this.maxDec); } if (data.partials) { if (!J.isArray(data.partials)) { - node.err('EndScreen error, invalid partials win: ' + - data.partials); + node.err('EndScreen error, partials must be array. ' + + 'Found: ' + data.partials); } else { - // If there is a basePay we already have a preWin. - if (preWin !== '') preWin += ' + '; - preWin += data.partials.join(' + '); + len = data.partials.length; + for (i = 0; i < len; i++) { + preWin += ' + ' + enforceDecimals(data.partials[i], + this.maxDec); + } } } if ('undefined' !== typeof data.totalRaw) { if (preWin) preWin += ' = '; else preWin = ''; - preWin += data.totalRaw; + preWin += enforceDecimals(data.totalRaw, this.maxDec); // Get Exchange Rate. ex = 'undefined' !== typeof data.exchangeRate ? - data.exchangeRate : node.game.settings.EXCHANGE_RATE; + enforceDecimals(data.exchangeRate, this.maxDec) : + node.game.settings.EXCHANGE_RATE; // If we have an exchange rate, check if we have a totalRaw. if ('undefined' !== typeof ex) preWin += '*' + ex; @@ -461,6 +487,9 @@ totalWin = this.getText('errTotalWin'); err = true; } + else { + totalWin = enforceDecimals(totalWin, this.maxDec); + } } } @@ -490,4 +519,23 @@ } }; + /** + * #### enforceDecimals + * + * @param {number|string} num The number or string to enforce + * @param {number|bool} nDec Number of decimals, or FALSE to not enforce + * @param {boolean} forceNum If TRUE, it forces the return of a number. + * + * @returns The number with at most the specified num of decimals + */ + function enforceDecimals(num, nDec, forceNum) { + var idx; + if (nDec !== false) { + num = '' + num; + idx = num.lastIndexOf('.'); + if (idx > num.length - 3) num = num.substring(0, idx+3); + } + return forceNum ? Number(num) : num; + } + })(node); diff --git a/widgets/Feedback.js b/widgets/Feedback.js index 5f8bd95..f765ad6 100644 --- a/widgets/Feedback.js +++ b/widgets/Feedback.js @@ -1,6 +1,6 @@ /** * # Feedback - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Sends a feedback message to the server @@ -22,7 +22,6 @@ Feedback.version = '1.6.0'; Feedback.description = 'Displays a configurable feedback form'; - Feedback.title = 'Feedback'; Feedback.className = 'feedback'; Feedback.texts = { @@ -82,12 +81,6 @@ colOver = '#a32020'; // #f2dede'; colRemain = '#78b360'; // '#dff0d8'; - // ## Dependencies - - Feedback.dependencies = { - JSUS: {} - }; - /** * ## Feedback constructor * @@ -264,6 +257,12 @@ } } + if (this.minWords || this.minChars || this.maxWords || + this.maxChars) { + + this.required = true; + } + /** * ### Feedback.rows * @@ -565,6 +564,17 @@ return res; }; + /** + * ### Feedback.isChoiceDone + * + * Returns TRUE if the feedback was filled as requested + * + * @return {boolean} TRUE if the feedback was filled as requested + */ + Feedback.prototype.isChoiceDone = function() { + return this.verifyFeedback(); + }; + /** * ### Feedback.append * diff --git a/widgets/Goto.js b/widgets/Goto.js new file mode 100644 index 0000000..7d9a545 --- /dev/null +++ b/widgets/Goto.js @@ -0,0 +1,122 @@ +/** + * # Goto + * Copyright(c) 2023 Stefano Balietti + * MIT Licensed + * + * Creates a simple interface to go to a step in the sequence. + * + * www.nodegame.org + * + * + * TODO: Update Style: + + + + + */ + (function(node) { + + "use strict"; + + node.widgets.register('Goto', Goto); + + // ## Meta-data + + Goto.version = '0.0.1'; + Goto.description = 'Creates a simple interface to move across ' + + 'steps in the sequence.'; + + Goto.panel = false; + Goto.className = 'goto'; + + /** + * ## Goto constructor + * + * Creates a new instance of Goto + * + * @param {object} options Optional. Configuration options. + * + * @see Goto.init + */ + function Goto(options) { + /** + * ### Goto.dropdown + * + * A callback executed after the button is clicked + * + * If it return FALSE, node.done() is not called. + */ + this.dropdown; + } + + Goto.prototype.append = function() { + this.dropdown = node.widgets.append('Dropdown', this.bodyDiv, { + tag: 'select', + choices: getSequence(), + id: 'ng_goto', + placeholder: 'Go to Step', + width: '15rem', + onchange: function(choice, datalist, that) { + node.game.gotoStep(choice); + } + }); + }; + + /** + * ### Goto.disable + * + * Disables the widget + */ + Goto.prototype.disable = function(opts) { + if (this.disabled) return; + this.disabled = true; + this.dropdown.enable(); + this.emit('disabled', opts); + }; + + /** + * ### Goto.enable + * + * Enables the widget + */ + Goto.prototype.enable = function(opts) { + if (!this.disabled) return; + this.disabled = false; + this.dropdown.disable(); + this.emit('enabled', opts); + }; + + + // ## Helper functions. + + function getSequence(seq) { + var i, j, out, value, vvalue, name, ss; + out = []; + seq = seq || node.game.plot.stager.sequence; + for ( i = 0 ; i < seq.length ; i++) { + value = (i+1); + name = seq[i].id; + for ( j = 0 ; j < seq[i].steps.length ; j++) { + ss = seq[i].steps.length === 1; + vvalue = ss ? value : value + '.' + (j+1); + out.push({ + value: vvalue, + name: vvalue + ' ' + + (ss ? name : name + '.' + seq[i].steps[j]) + }); + } + } + return out; + } + +})(node); diff --git a/widgets/GroupMalleability.js b/widgets/GroupMalleability.js index f106f42..9b2359d 100644 --- a/widgets/GroupMalleability.js +++ b/widgets/GroupMalleability.js @@ -1,6 +1,6 @@ /** * # GroupMalleability - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to measure users' perception of group malleability. @@ -15,7 +15,7 @@ // ## Meta-data - GroupMalleability.version = '0.1.0'; + GroupMalleability.version = '0.2.0'; GroupMalleability.description = 'Displays an interface to measure ' + 'perception for group malleability.'; @@ -57,10 +57,6 @@ 'can work quickly, your first feeling is generally best.' }; - // ## Dependencies - - GroupMalleability.dependencies = {}; - /** * ## GroupMalleability constructor * @@ -145,6 +141,11 @@ else if (opts.mainText !== false) { this.mainText = this.getText('mainText'); } + + // Keep reference to pass to ChoiceTableGroup on creation. + this.requiredMark = opts.requiredMark; + this.displayRequired = opts.displayRequired; + }; GroupMalleability.prototype.append = function() { @@ -158,7 +159,9 @@ title: false, panel: false, requiredChoice: this.required, - header: this.header + header: this.header, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark }); }; diff --git a/widgets/LanguageSelector.js b/widgets/LanguageSelector.js index 94622ff..17a2b80 100644 --- a/widgets/LanguageSelector.js +++ b/widgets/LanguageSelector.js @@ -1,6 +1,6 @@ /** * # LanguageSelector - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Manages and displays information about languages available and selected @@ -17,19 +17,16 @@ // ## Meta-data - LanguageSelector.version = '0.6.2'; + LanguageSelector.version = '0.6.3'; LanguageSelector.description = 'Display information about the current ' + - 'language and allows to change language.'; - LanguageSelector.title = 'Language'; - LanguageSelector.className = 'languageselector'; + 'language and allows users to change it.'; - LanguageSelector.texts.loading = 'Loading language information...'; + LanguageSelector.title = 'Select Language'; - // ## Dependencies - LanguageSelector.dependencies = { - JSUS: {} - }; + LanguageSelector.className = 'languageselector'; + + LanguageSelector.texts.loading = 'Loading...'; /** * ## LanguageSelector constructor @@ -181,13 +178,14 @@ * @see LanguageSelector.setLanguage */ this.onLangCallback = function(msg) { - var language; + var language, label, display, counter; // Clear display. while (that.displayForm.firstChild) { that.displayForm.removeChild(that.displayForm.firstChild); } + counter = 0; // Initialize widget. that.availableLanguages = msg.data; if (that.usingButtons) { @@ -195,31 +193,32 @@ // Creates labeled buttons. for (language in msg.data) { if (msg.data.hasOwnProperty(language)) { - that.optionsLabel[language] = W.get('label', { + label = W.get('label', { id: language + 'Label', 'for': language + 'RadioButton' }); - that.optionsDisplay[language] = W.get('input', { + display = W.get('input', { id: language + 'RadioButton', type: 'radio', name: 'languageButton', value: msg.data[language].name }); - that.optionsDisplay[language].onclick = - makeSetLanguageOnClick(language); - - that.optionsLabel[language].appendChild( - that.optionsDisplay[language]); - that.optionsLabel[language].appendChild( - document.createTextNode( - msg.data[language].nativeName)); - W.add('br', that.displayForm); - that.optionsLabel[language].className = - 'unselectedButtonLabel'; - that.displayForm.appendChild( - that.optionsLabel[language]); + display.onclick = makeOnClick(language); + + label.appendChild(display); + + label.appendChild(document.createTextNode( + msg.data[language].nativeName)); + + if (++counter !== 1) W.add('br', that.displayForm); + label.className = 'unselected'; + that.displayForm.appendChild(label); + + that.optionsLabel[language] = label; + that.optionsDisplay[language] = display; + } } } @@ -227,18 +226,19 @@ that.displaySelection = W.get('select', 'selectLanguage'); for (language in msg.data) { - that.optionsLabel[language] = + label = document.createTextNode(msg.data[language].nativeName); - that.optionsDisplay[language] = W.get('option', { + display = W.get('option', { id: language + 'Option', value: language }); - that.optionsDisplay[language].appendChild( - that.optionsLabel[language]); - that.displaySelection.appendChild( - that.optionsDisplay[language]); + display.appendChild(label); + that.displaySelection.appendChild(display); + that.optionsLabel[language] = label; + that.optionsDisplay[language] = display } + that.displayForm.appendChild(that.displaySelection); that.displayForm.onchange = function() { that.setLanguage(that.displaySelection.value, @@ -259,7 +259,7 @@ that.onLangCallbackExtension = null; } - function makeSetLanguageOnClick(langStr) { + function makeOnClick(langStr) { return function() { that.setLanguage(langStr, that.updatePlayer === 'onselect'); }; @@ -364,7 +364,7 @@ this.optionsDisplay[this.currentLanguage].checked = 'unchecked'; this.optionsLabel[this.currentLanguage].className = - 'unselectedButtonLabel'; + 'unselected'; } } @@ -374,8 +374,7 @@ if (this.usingButtons) { // Check language button and change className of label. this.optionsDisplay[this.currentLanguage].checked = 'checked'; - this.optionsLabel[this.currentLanguage].className = - 'selectedButtonLabel'; + this.optionsLabel[this.currentLanguage].className = 'selected'; } else { this.displaySelection.value = this.currentLanguage; diff --git a/widgets/MoneyTalks.js b/widgets/MoneyTalks.js index 942b62f..7f8c710 100644 --- a/widgets/MoneyTalks.js +++ b/widgets/MoneyTalks.js @@ -21,12 +21,6 @@ MoneyTalks.title = 'Earnings'; MoneyTalks.className = 'moneytalks'; - // ## Dependencies - - MoneyTalks.dependencies = { - JSUS: {} - }; - /** * ## MoneyTalks constructor * diff --git a/widgets/MoodGauge.js b/widgets/MoodGauge.js index 595103b..8679cfc 100644 --- a/widgets/MoodGauge.js +++ b/widgets/MoodGauge.js @@ -1,6 +1,6 @@ /** * # MoodGauge - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to query users about mood, emotions and well-being @@ -15,21 +15,15 @@ // ## Meta-data - MoodGauge.version = '0.4.0'; + MoodGauge.version = '0.5.0'; MoodGauge.description = 'Displays an interface to measure mood ' + 'and emotions.'; - MoodGauge.title = 'Mood Gauge'; MoodGauge.className = 'moodgauge'; MoodGauge.texts.mainText = 'Thinking about yourself and how you normally' + ' feel, to what extent do you generally feel: '; - // ## Dependencies - MoodGauge.dependencies = { - JSUS: {} - }; - /** * ## MoodGauge constructor * @@ -221,14 +215,13 @@ // ## Available methods. // ### I_PANAS_SF - function I_PANAS_SF(options) { - var items, emotions, choices, left, right; + function I_PANAS_SF(opts) { + var items, emotions, choices, left, right, l; var gauge, i, len; - choices = options.choices || - [ '1', '2', '3', '4', '5' ]; + choices = opts.choices || [ '1', '2', '3', '4', '5' ]; - emotions = options.emotions || [ + emotions = opts.emotions || [ 'Upset', 'Hostile', 'Alert', @@ -240,32 +233,32 @@ 'Afraid', 'Active' ]; - - left = options.left || 'never'; - - right = options.right || 'always'; - len = emotions.length; + left = opts.left || 'never'; + right = opts.right || 'always'; + items = new Array(len); i = -1; for ( ; ++i < len ; ) { + l = '' + emotions[i] + ': ' + left; items[i] = { id: emotions[i], - left: '' + emotions[i] + ': never', + left: l, right: right, - choices: choices + sameCellWidth: '200px' }; } gauge = node.widgets.get('ChoiceTableGroup', { - id: options.id || 'ipnassf', + id: opts.id || 'ipnassf', items: items, mainText: this.mainText || this.getText('mainText'), - title: false, requiredChoice: true, - storeRef: false + storeRef: false, + header: opts.header, + choices: choices, }); return gauge; diff --git a/widgets/Requirements.js b/widgets/Requirements.js index cd77868..8a575eb 100644 --- a/widgets/Requirements.js +++ b/widgets/Requirements.js @@ -34,7 +34,6 @@ // ## Dependencies Requirements.dependencies = { - JSUS: {}, List: {} }; diff --git a/widgets/RiskGauge.js b/widgets/RiskGauge.js index 160a6a9..5eb090f 100644 --- a/widgets/RiskGauge.js +++ b/widgets/RiskGauge.js @@ -1,6 +1,6 @@ /** * # RiskGauge - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to measure risk preferences with different methods @@ -17,11 +17,10 @@ // ## Meta-data - RiskGauge.version = '0.8.0'; + RiskGauge.version = '0.9.0'; RiskGauge.description = 'Displays an interface to ' + 'measure risk preferences with different methods.'; - RiskGauge.title = 'Risk Gauge'; RiskGauge.className = 'riskgauge'; RiskGauge.texts = { @@ -91,10 +90,6 @@ // Backward compatibility. RiskGauge.texts.mainText = RiskGauge.texts.holt_laury_mainText; - // ## Dependencies - RiskGauge.dependencies = { - JSUS: {} - }; /** * ## RiskGauge constructor @@ -219,6 +214,9 @@ this.on('unhighlighted', function() { if (gauge.unhighlight) gauge.unhighlight(); }); + + this.displayRequired = opts.displayRequired; + this.requiredMark = opts.requiredMark; }; RiskGauge.prototype.append = function() { @@ -311,7 +309,9 @@ mainText: this.mainText || this.getText('holt_laury_mainText'), title: false, requiredChoice: true, - storeRef: false + storeRef: false, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark }); return gauge; @@ -378,6 +378,10 @@ // Public variables. + // Enables done button on open (only if DoneButton is found under + // node.game.doneButton). + this.enableDoneBtn = opts.enableDoneButton !== false; + // Store locally because they are overwritten. TODO: check if needed. this._highlight = this.highlight; this._unhighlight = this.unhighlight; @@ -447,6 +451,15 @@ this.withPrize = 'undefined' === typeof opts.withPrize ? true : !!opts.withPrize; + this.onopen = null; + if (opts.onopen) { + if ('function' !== typeof opts.onopen) { + throw new TypeError('Bomb: onopen must be function or ' + + 'undefined. Found: ' + opts.onopen); + } + this.onopen = opts.onopen; + } + // Bomb box. // Pick bomb box id, if probability permits it, else set to -1. // Resulting id is between 1 and totBoxes. @@ -507,7 +520,8 @@ // Main text. W.add('div', that.bodyDiv, { innerHTML: that.mainText || - that.getText('bomb_mainText', probBomb) + that.getText('bomb_mainText', probBomb), + className: 'bomb-maintext' }); // Slider. @@ -519,12 +533,11 @@ initialValue: 0, displayValue: false, displayNoChange: false, + displayRequired: that.displayRequired, + requiredMark: that.requiredMark, type: 'flat', required: true, panel: false, - // texts: { - // currentValue: that.getText('sliderValue') - // }, onmove: function(value) { var i, div, c, v; @@ -553,9 +566,9 @@ // Update display. W.gid('bomb_numBoxes').innerText = value; - c = that.currency; - v = that.boxValue; if (that.withPrize) { + c = that.currency; + v = that.boxValue; W.gid('bomb_boxValue').innerText = v + c; W.gid('bomb_totalWin').innerText = Number((value * v)).toFixed(2) + c; @@ -585,7 +598,7 @@ W.add('p', infoDiv, { innerHTML: that.getText('bomb_boxValue') + ' ' + - this.boxValue + '' + that.boxValue + '' }); W.add('p', infoDiv, { innerHTML: that.getText('bomb_totalWin') + @@ -596,7 +609,7 @@ bombResult = W.add('p', infoDiv, { id: 'bomb_result' }); button = W.add('button', that.bodyDiv, { - className: 'btn-danger', + className: 'btn btn-lg btn-danger', innerHTML: that.getText('bomb_openButton'), }); // Initially hidden. @@ -625,6 +638,14 @@ cl = 'bomb_' + (isWinner ? 'won' : 'lost'); bombResult.innerHTML = that.getText(cl); bombResult.className += (' ' + cl); + + // Enable done button, if found and disabled. + if (that.enableDoneBtn && node.game.doneButton && + node.game.doneButton.isDisabled()) { + + node.game.doneButton.enable(); + } + if (that.onopen) that.onopen(isWinner, that); }; } }; diff --git a/widgets/SDO.js b/widgets/SDO.js index 8564e9a..1e765f9 100644 --- a/widgets/SDO.js +++ b/widgets/SDO.js @@ -1,6 +1,6 @@ /** * # SDO - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to measure users' social dominance orientation (S.D.O.) @@ -15,11 +15,10 @@ // ## Meta-data - SDO.version = '0.3.0'; + SDO.version = '0.4.0'; SDO.description = 'Displays an interface to measure Social ' + 'Dominance Orientation (S.D.O.).'; - SDO.title = 'SDO'; SDO.className = 'SDO'; @@ -232,6 +231,10 @@ } this.mainText = opts.mainText; } + + // Keep reference to pass to ChoiceTableGroup on creation. + this.requiredMark = opts.requiredMark; + this.displayRequired = opts.displayRequired; }; SDO.prototype.append = function() { @@ -243,7 +246,9 @@ title: false, panel: false, requiredChoice: this.required, - header: this.header + header: this.header, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark }); }; diff --git a/widgets/SVOGauge.js b/widgets/SVOGauge.js index dc27acc..a592baf 100644 --- a/widgets/SVOGauge.js +++ b/widgets/SVOGauge.js @@ -1,6 +1,6 @@ /** * # SVOGauge - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays an interface to measure users' social value orientation (S.V.O.) @@ -15,16 +15,15 @@ // ## Meta-data - SVOGauge.version = '0.8.1'; + SVOGauge.version = '0.9.0'; SVOGauge.description = 'Displays an interface to measure social ' + 'value orientation (S.V.O.).'; - SVOGauge.title = 'SVO Gauge'; SVOGauge.className = 'svogauge'; SVOGauge.texts = { mainText: 'You and another randomly selected participant ' + - 'will receive an extra bonus.
' + + 'will receive an extra bonus. ' + 'Choose the preferred bonus amounts (in cents) for you ' + 'and the other participant in each row.
' + 'We will select one row at random ' + @@ -35,10 +34,6 @@ left: 'Your Bonus:
Other\'s Bonus:' }; - // ## Dependencies - - SVOGauge.dependencies = {}; - /** * ## SVOGauge constructor * @@ -165,6 +160,9 @@ this.on('unhighlighted', function() { gauge.unhighlight(); }); + + this.displayRequired = opts.displayRequired; + this.requiredMark = opts.requiredMark; }; SVOGauge.prototype.append = function() { @@ -319,7 +317,9 @@ title: false, renderer: renderer, requiredChoice: this.required, - storeRef: false + storeRef: false, + displayRequired: this.displayRequired, + requiredMark: this.requiredMark }); return gauge; diff --git a/widgets/Slider.js b/widgets/Slider.js index 499d8bf..13e1f3d 100644 --- a/widgets/Slider.js +++ b/widgets/Slider.js @@ -1,6 +1,6 @@ /** * # Slider - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2024 Stefano Balietti * MIT Licensed * * Creates a configurable slider. @@ -17,19 +17,27 @@ // ## Meta-data - Slider.version = '0.4.0'; + Slider.version = '0.7.0'; Slider.description = 'Creates a configurable slider'; - Slider.title = false; Slider.className = 'slider'; - // ## Dependencies - Slider.texts = { currentValue: function(widget, value) { return 'Value: ' + value; }, - noChange: 'No change' + noChange: 'No change', + // TODO: if the knob is hidden, the message is a bit unclear. + error: 'Movement required. If you agree with the current ' + + 'value, move the slider away and then back to this position.', + autoHint: function(w) { + var h = ''; + if (w.knobHiddenFirst) { + h += 'The slider knob will be shown after the first click. '; + } + if (w.required) h += 'Movement required.'; + return h || false; + } }; @@ -72,6 +80,12 @@ */ this.initialValue = 50; + /** Slider.step + * + * Legal increments for the slider + */ + this.step = 1; + /** * ### Slider.mainText * @@ -143,14 +157,29 @@ */ this.displayNoChange = true; - /** Slider.noChangeSpan + /** Slider.noChangeBtn * * The checkbox form marking the no-change * * @see Slider.displayNoChange * @see Slider.noChangeCheckbox + * @see Slider.noChangeCb */ - this.noChangeSpan = null; + this.noChangeBtn = null; + + /** + * ### Slider.noChangeCb + * + * If a callback executed when the noChangeBtn is clicked + */ + this.noChangeCb = null; + + /** + * ### Slider.errorBox + * + * An HTML element displayed when a validation error occurs + */ + this.errorBox = null; /** Slider.totalMove * @@ -158,6 +187,12 @@ */ this.totalMove = 0; + /** Slider.nClicks + * + * Counts onmousedown/touchstart events on the slider + */ + this.nClicks = 0; + /** Slider.volumeSlider * * If TRUE, only the slider to the left of the pointer is colored @@ -172,6 +207,18 @@ */ this.hoverColor = '#2076ea'; + /** Slider.left + * + * A text to be displayed at the leftmost position + */ + this.left = null; + + /** Slider.right + * + * A text to be displayed at the righttmost position + */ + this.right = null; + /** Slider.listener * * The main function listening for slider movement @@ -182,22 +229,23 @@ * by the no-change checkbox. Note: when the function is invoked * by the browser, noChange is the change event. * + * @param {boolean} init Optional If true, the function is called + * by the init method, and some operations (e.g., updating totalMove) + * are not executed. + * * @see Slider.onmove */ var timeOut = null; - this.listener = function(noChange) { + this.listener = function(noChange, init, sync) { + var _listener; if (!noChange && timeOut) return; if (that.isHighlighted()) that.unhighlight(); - timeOut = setTimeout(function() { - var percent, diffPercent; + _listener = function() { + var percent, diff; percent = (that.slider.value - that.min) * that.scale; - diffPercent = percent - that.currentValue; - that.currentValue = percent; - - // console.log(diffPercent); // console.log(that.slider.value, percent); if (that.type === 'volume') { @@ -211,24 +259,38 @@ if (that.displayValue) { that.valueSpan.innerHTML = - that.getText('currentValue', that.slider.value); + that.getText('currentValue', that.slider.value); } if (that.displayNoChange && noChange !== true) { if (that.noChangeCheckbox.checked) { that.noChangeCheckbox.checked = false; - J.removeClass(that.noChangeSpan, 'italic'); + J.removeClass(that.noChangeBtn, 'italic'); } } - that.totalMove += Math.abs(diffPercent); - - if (that.onmove) { - that.onmove.call(that, that.slider.value, diffPercent); + if (!init) { + // Old (currentValue was a percent). + // diffPercent = percent - that.currentValue; + // that.totalMove += Math.abs(diffPercent); + diff = that.slider.value - that.currentValue; + // console.log(diff); + that.totalMove += Math.abs(diff); + if (that.onmove) { + that.onmove.call(that, that.slider.value, diff); + } } + // Update currentValue. + // Change: vefore currentValue was equal to percent. + that.currentValue = that.slider.value; + + timeOut = null; - }, 0); + }; + + if (sync) _listener(); + else timeOut = setTimeout(_listener, 0); } /** Slider.onmove @@ -250,6 +312,25 @@ */ this.timeFrom = 'step'; + /** + * ### Slider.knobHiddenFirst + * + * If TRUE, the knob of the slider is hidden before interaction + */ + this.knobHiddenFirst = false; + + + /** + * ### Slider._tmpColor + * + * The original color of the rangeFill container (default black) + * + * that is replaced upon highlighting. + * Need to do js onmouseover because ccs:hover does not work here. + * + */ + this._tmpColor; + } // ## Slider methods @@ -301,12 +382,34 @@ this.initialValue = this.currentValue = tmp; } + // Must be before auto-hint. + if ('undefined' !== typeof opts.hideKnob) { + this.knobHiddenFirst = !!opts.hideKnob; + } + + if ('undefined' !== typeof opts.step) { + tmp = J.isInt(opts.step); + if ('number' !== typeof tmp) { + throw new TypeError(e + 'step must be an integer or ' + + 'undefined. Found: ' + opts.step); + } + this.step = tmp; + } + if ('undefined' !== typeof opts.displayValue) { this.displayValue = !!opts.displayValue; } if ('undefined' !== typeof opts.displayNoChange) { this.displayNoChange = !!opts.displayNoChange; } + if ('undefined' !== typeof opts.noChangeCb) { + if ('function' !== typeof opts.noChangeCb) { + throw new TypeError(e + 'noChangeCb must be function or ' + + 'undefined. Found: ' + opts.noChangeCb); + + } + this.noChangeCb = opts.noChangeCb; + } if (opts.type) { if (opts.type !== 'volume' && opts.type !== 'flat') { @@ -344,13 +447,13 @@ this.hint = opts.hint; } else { - // TODO: Do we need it? - // this.hint = this.getText('autoHint'); + this.hint = this.getText('autoHint'); } if (this.required && this.hint !== false) { - if (!this.hint) this.hint = 'Movement required'; - this.hint += ' *'; + if (opts.displayRequired !== false) { + this.hint += ' ' + this.requiredMark; + } } if (opts.onmove) { @@ -387,6 +490,23 @@ } this.correctValue = opts.correctValue; } + + tmp = opts.left; + if ('undefined' !== typeof tmp) { + if ('string' !== typeof tmp && 'number' !== typeof tmp) { + throw new TypeError(e + 'left must be string, number or ' + + 'undefined. Found: ' + tmp); + } + this.left = '' + tmp; + } + tmp = opts.right; + if ('undefined' !== typeof tmp) { + if ('string' !== typeof tmp && 'number' !== typeof tmp) { + throw new TypeError(e + 'right must be string, number or ' + + 'undefined. Found: ' + tmp); + } + this.right = '' + tmp; + } }; /** @@ -396,12 +516,7 @@ * @param {object} opts Configuration options */ Slider.prototype.append = function() { - var container; - - // The original color of the rangeFill container (default black) - // that is replaced upon highlighting. - // Need to do js onmouseover because ccs:hover does not work here. - var tmpColor; + var container, tmp; var that = this; @@ -424,92 +539,298 @@ className: 'container-slider' }); + if (this.left) { + tmp = W.add('span', container); + tmp.innerHTML = this.left; + tmp.style.position = 'relative'; + tmp.style.top = '-20px'; + tmp.style.float = 'left'; + } + this.rangeFill = W.add('div', container, { className: 'fill-slider', // id: 'range-fill' }); - this.slider = W.add('input', container, { + tmp = { className: 'volume-slider', - // id: 'range-slider-input', name: 'rangeslider', type: 'range', min: this.min, - max: this.max - }); + max: this.max, + step: this.step, + }; + if (this.knobHiddenFirst) tmp.style = { opacity: 0 }; + this.slider = W.add('input', container, tmp); + + // Count nClicks. + this.slider.onmousedown = function() { + // Important that it is not three equals here. + if (that.knobHiddenFirst && that.isKnobHidden()) { + that.showKnob(); + that.listener(true, false, true); + } + that.nClicks++; + }; + // For mobile. + this.slider.ontouchstart = this.slider.onmousedown; + // TODO: we should use a CSS class. this.slider.onmouseover = function() { - tmpColor = that.rangeFill.style.background || 'black'; + if (that.slider.disabled) return; + that._tmpColor = that.rangeFill.style.background || 'black'; that.rangeFill.style.background = that.hoverColor; }; this.slider.onmouseout = function() { - that.rangeFill.style.background = tmpColor; + if (that.slider.disabled) return; + that.rangeFill.style.background = that._tmpColor; }; if (this.sliderWidth) this.slider.style.width = this.sliderWidth; - if (this.displayValue) { - this.valueSpan = W.add('span', this.bodyDiv, { - className: 'slider-display-value' - }); + if (this.right) { + tmp = W.add('span', container); + tmp.innerHTML = this.right; + tmp.style.position = 'relative'; + tmp.style.top = '-20px'; + tmp.style.float = 'right'; } if (this.displayNoChange) { - this.noChangeSpan = W.add('span', this.bodyDiv, { - className: 'slider-display-nochange', + this.noChangeBtn = W.add('button', this.bodyDiv, { + className: 'btn btn-danger btn-sm slider-display-nochange', innerHTML: this.getText('noChange') + ' ' }); - this.noChangeCheckbox = W.add('input', this.noChangeSpan, { + this.noChangeCheckbox = W.add('input', this.noChangeBtn, { type: 'checkbox' }); - this.noChangeCheckbox.onclick = function() { - if (that.noChangeCheckbox.checked) { - if (that.slider.value === that.initialValue) return; - that.slider.value = that.initialValue; - that.listener(true); - J.addClass(that.noChangeSpan, 'italic'); + + this.noChangeBtn.onclick = function(event) { + var c, isCheckBox; + c = that.noChangeCheckbox; + isCheckBox = event.target && event.target.type === 'checkbox'; + + // Currently no change, pressed to re-activate movements. + if (that.noChange) { + J.removeClass(that.noChangeBtn, 'italic'); + that.noChange = false; + // Click the checkbox (unless already clicked). + c.checked = false; + that.enableSlider(); } + // Activated no-change. else { - J.removeClass(that.noChangeSpan, 'italic'); + J.addClass(that.noChangeBtn, 'italic'); + // Update state. + that.noChange = true; + // Click the checkbox (unless already clicked). + c.checked = true; + that.disableSlider(); } + // Call callback with current status. + if (that.noChangeCb) that.noChangeCb(that, that.noChange); }; } - this.slider.oninput = this.listener; + if (this.displayValue) { + this.valueSpan = W.add('span', this.bodyDiv, { + className: 'slider-display-value' + }); + } + + this.errorBox = W.append('div', this.bodyDiv, { className: 'errbox' }); + this.slider.value = this.initialValue; + this.slider.oninput = this.listener; - this.slider.oninput(); + this.slider.oninput(false, true); }; Slider.prototype.getValues = function(opts) { - var res, value, nochange; + var res, nochange; opts = opts || {}; res = true; if ('undefined' === typeof opts.highlight) opts.highlight = true; - value = this.currentValue; - nochange = this.noChangeCheckbox && this.noChangeCheckbox.checked; - if ((this.required && this.totalMove === 0 && !nochange) || - (null !== this.correctValue && this.correctValue !== value)) { - - if (opts.highlight) this.highlight(); + if (!this.isChoiceDone()) { + if (opts.highlight) { + this.highlight(); + this.setError(this.getText('error')); + } res = false; } + nochange = this.noChangeCheckbox && this.noChangeCheckbox.checked; + return { - value: value, + value: this.currentValue, noChange: !!nochange, initialValue: this.initialValue, totalMove: this.totalMove, + nClicks: this.nClicks, isCorrect: res, time: node.timer.getTimeSince(this.timeFrom) - }; + }; }; Slider.prototype.setValues = function(opts) { - opts = opts || {}; - this.slider.value = opts.value; - this.slider.oninput(); + var value; + if ('undefined' === typeof opts) opts = {}; + else if ('number' === typeof opts) opts = { value: opts }; + + if (opts.correct && this.correctValue !== null) { + value = this.correctValue; + } + else if ('number' !== typeof opts.value) { + value = J.randomInt(0, 101)-1; + + // Check if movement is required and no movement was done and + // the random value is equal to the current value. If so, add 1. + if (this.required && this.totalMove === 0 && + value === this.slider.value) { + + value++; + } + } + else { + value = opts.value; + } + + this.slider.value = value; + this.slider.oninput(false, false, true); + }; + + + + /** + * ### Slider.disableSlider + * + * Disables the slider only + * + * It hides the knob by default + * + * @param {boolean} showKnob If FALSE it does not show knob + */ + Slider.prototype.disableSlider = function (hideKnob) { + W.addClass(this.rangeFill, 'disabled'); + W.addClass(this.slider, 'disabled'); + this._tmpColor = this.rangeFill.style.background || 'black'; + this.rangeFill.style.background = 'grey'; + this.slider.disabled = true; + if (hideKnob !== false) this.hideKnob(); + }; + + /** + * ### Slider.enableSlider + * + * Enables the slider only + * + * It shows the knob by default, unless it has never been clicked and + * the `knobHiddenFirst` is TRUE. + * + * @param {boolean} showKnob If FALSE it does not show knob + */ + Slider.prototype.enableSlider = function (showKnob) { + W.removeClass(this.rangeFill, 'disabled'); + W.removeClass(this.slider, 'disabled'); + this.rangeFill.style.background = this._tmpColor; + this.slider.disabled = false; + if (showKnob !== false) { + if (this.knobHiddenFirst && this.nClicks !== 0) { + this.showKnob(); + } + } + }; + + /** + * ### Slider.hideKnob + * + * Hides the knob + */ + Slider.prototype.hideKnob = function () { + this.slider.style.opacity = 0; + }; + + /** + * ### Slider.showKnob + * + * Hides the knob + */ + Slider.prototype.showKnob = function () { + this.slider.style.opacity = 1; + }; + + /** + * ### Slider.isKnobHidden + * + * Hides the knob + */ + Slider.prototype.isKnobHidden = function () { + // Two equals important. + return this.slider.style.opacity == 0; + }; + + /** + * ### Slider.disable + * + * Disables the widget + */ + Slider.prototype.disable = function () { + if (this.disabled === true) return; + this.disabled = true; + this.disableSlider(); + if (this.noChangeBtn) { + this.noChangeBtn.disabled = true; + this.noChangeCheckbox.disabled = true; + } + this.emit('disabled'); + }; + + /** + * ### Slider.enable + * + * Enables the widget + */ + Slider.prototype.enable = function () { + if (this.disabled === false) return; + this.disabled = false; + this.enableSlider(); + if (this.noChangeBtn) { + this.noChangeBtn.disabled = false; + this.noChangeCheckbox.disabled = false; + } + this.emit('enabled'); + }; + + /** + * ### Slider.setError + * + * Set the error msg inside the errorBox and call highlight + * + * @param {string} The error msg (can contain HTML) + * + * @see Slider.highlight + * @see Slider.errorBox + */ + Slider.prototype.setError = function(err) { + this.errorBox.innerHTML = err || ''; + if (err) this.highlight(); + else this.unhighlight(); + }; + + /** + * ### Slider.isChoiceDone + * + * Returns TRUE if the slider has been moved (if requested) + * + * @return {boolean} TRUE if the choice is done + */ + Slider.prototype.isChoiceDone = function() { + var value, nochange; + value = this.currentValue; + nochange = this.noChangeCheckbox && this.noChangeCheckbox.checked; + return !((this.required && this.totalMove === 0 && !nochange) || + (null !== this.correctValue && this.correctValue !== value)); }; })(node); diff --git a/widgets/VisualRound.js b/widgets/VisualRound.js index 0cdcd56..8689a48 100644 --- a/widgets/VisualRound.js +++ b/widgets/VisualRound.js @@ -1,6 +1,6 @@ /** * # VisualRound - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Display information about rounds and/or stage in the game @@ -18,10 +18,9 @@ // ## Meta-data - VisualRound.version = '0.9.0'; + VisualRound.version = '0.9.1'; VisualRound.description = 'Displays current/total/left round/stage/step. '; - VisualRound.title = false; VisualRound.className = 'visualround'; VisualRound.texts = { @@ -229,12 +228,6 @@ this.updateInformation(); - if (!this.options.displayMode && this.options.displayModeNames) { - console.log('***VisualTimer.init: options.displayModeNames is ' + - 'deprecated. Use options.displayMode instead.***'); - this.options.displayMode = this.options.displayModeNames; - } - if (!this.options.displayMode) { this.setDisplayMode([ 'COUNT_UP_ROUNDS_TO_TOTAL_IFNOT1', diff --git a/widgets/VisualStage.js b/widgets/VisualStage.js index 913e051..0c0eda4 100644 --- a/widgets/VisualStage.js +++ b/widgets/VisualStage.js @@ -1,6 +1,6 @@ /** * # VisualStage - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Shows the name of the current, previous and next step. @@ -21,7 +21,6 @@ VisualStage.description = 'Displays the name of the current, previous and next step of the game.'; - VisualStage.title = false; VisualStage.className = 'visualstage'; VisualStage.texts = { diff --git a/widgets/VisualTimer.js b/widgets/VisualTimer.js index b56cf74..0ad4861 100644 --- a/widgets/VisualTimer.js +++ b/widgets/VisualTimer.js @@ -126,21 +126,21 @@ * - waitBoxOptions: an option object to be passed to `TimerBox` * - mainBoxOptions: an option object to be passed to `TimerBox` * - * @param {object} options Optional. Configuration options + * @param {object} opts Optional. Configuration options * * @see TimerBox * @see GameTimer */ - VisualTimer.prototype.init = function(options) { - var t, gameTimerOptions; + VisualTimer.prototype.init = function(opts) { + var gameTimerOptions; // We keep the check for object, because this widget is often // called by users and the restart methods does not guarantee // an object. - options = options || {}; - if ('object' !== typeof options) { - throw new TypeError('VisualTimer.init: options must be ' + - 'object or undefined. Found: ' + options); + opts = opts || {}; + if ('object' !== typeof opts) { + throw new TypeError('VisualTimer.init: opts must be ' + + 'object or undefined. Found: ' + opts); } // Important! Do not modify directly options, because it might @@ -149,36 +149,36 @@ // If gameTimer is not already set, check options, then // try to use node.game.timer, if defined, otherwise crete a new timer. - if ('undefined' !== typeof options.gameTimer) { + if ('undefined' !== typeof opts.gameTimer) { if (this.gameTimer) { - throw new Error('GameTimer.init: options.gameTimer cannot ' + + throw new Error('GameTimer.init: opts.gameTimer cannot ' + 'be set if a gameTimer is already existing: ' + this.name); } - if ('object' !== typeof options.gameTimer) { - throw new TypeError('VisualTimer.init: options.' + + if ('object' !== typeof opts.gameTimer) { + throw new TypeError('VisualTimer.init: opts.' + 'gameTimer must be object or ' + - 'undefined. Found: ' + options.gameTimer); + 'undefined. Found: ' + opts.gameTimer); } - this.gameTimer = options.gameTimer; + this.gameTimer = opts.gameTimer; } else { if (!this.isInitialized) { this.internalTimer = true; this.gameTimer = node.timer.createTimer({ - name: options.name || 'VisualTimer_' + J.randomInt(10000000) + name: opts.name || 'VisualTimer_' + J.randomInt(10000000) }); } } - if (options.hooks) { + if (opts.hooks) { if (!this.internalTimer) { throw new Error('VisualTimer.init: cannot add hooks on ' + 'external gameTimer.'); } - if (!J.isArray(options.hooks)) { - gameTimerOptions.hooks = [ options.hooks ]; + if (!J.isArray(opts.hooks)) { + gameTimerOptions.hooks = [ opts.hooks ]; } } else { @@ -197,29 +197,29 @@ // Important! Manual clone must be done after hooks and gameTimer. // Parse milliseconds option. - if ('undefined' !== typeof options.milliseconds) { + if ('undefined' !== typeof opts.milliseconds) { gameTimerOptions.milliseconds = - node.timer.parseInput('milliseconds', options.milliseconds); + node.timer.parseInput('milliseconds', opts.milliseconds); } // Parse update option. - if ('undefined' !== typeof options.update) { + if ('undefined' !== typeof opts.update) { gameTimerOptions.update = - node.timer.parseInput('update', options.update); + node.timer.parseInput('update', opts.update); } else { gameTimerOptions.update = 1000; } // Parse timeup option. - if ('undefined' !== typeof options.timeup) { - gameTimerOptions.timeup = options.timeup; + if ('undefined' !== typeof opts.timeup) { + gameTimerOptions.timeup = opts.timeup; } // Init the gameTimer, regardless of the source (internal vs external). this.gameTimer.init(gameTimerOptions); - t = this.gameTimer; + // var t = this.gameTimer; // TODO: not using session for now. // node.session.register('visualtimer', { @@ -242,10 +242,17 @@ this.options = gameTimerOptions; // Must be after this.options is assigned. - if ('undefined' === typeof this.options.stopOnDone) { + if ('undefined' !== typeof opts.stopOnDone) { + this.options.stopOnDone = !!opts.stopOnDone; + } + else if ('undefined' === typeof this.options.stopOnDone) { this.options.stopOnDone = true; } - if ('undefined' === typeof this.options.startOnPlaying) { + + if ('undefined' !== typeof opts.startOnPlaying) { + this.options.startOnPlaying = !!opts.startOnPlaying; + } + else if ('undefined' === typeof this.options.startOnPlaying) { this.options.startOnPlaying = true; } @@ -257,7 +264,7 @@ } J.mixout(this.options.mainBoxOptions, - {classNameBody: options.className, hideTitle: true}); + {classNameBody: opts.className, hideTitle: true}); J.mixout(this.options.waitBoxOptions, {title: 'Max. wait timer', classNameTitle: 'waitTimerTitle', diff --git a/widgets/WaitingRoom.js b/widgets/WaitingRoom.js index 61cc789..e4021d9 100644 --- a/widgets/WaitingRoom.js +++ b/widgets/WaitingRoom.js @@ -1,6 +1,6 @@ /** * # WaitingRoom - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2023 Stefano Balietti * MIT Licensed * * Displays the number of connected/required players to start a game @@ -14,7 +14,7 @@ node.widgets.register('WaitingRoom', WaitingRoom); // ## Meta-data - WaitingRoom.version = '1.3.0'; + WaitingRoom.version = '1.4.0'; WaitingRoom.description = 'Displays a waiting room for clients.'; WaitingRoom.title = 'Waiting Room'; @@ -154,7 +154,6 @@ // #### defaultTreatments defaultTreatments: 'Defaults:' - }; /** @@ -296,49 +295,71 @@ this.disconnectIfNotSelected = null; /** - * ### WaitingRoom.playWithBotOption + * ### WaitingRoom.userCanDispatch * - * If TRUE, it displays a button to begin the game with bots + * If TRUE, the interface allows to start a new game * * This option is set by the server, local modifications will * not have an effect if server does not allow it * - * @see WaitingRoom.playBotBtn + * @see WaitingRoom.playBtn */ - this.playWithBotOption = null; + this.userCanDispatch = null; /** - * ### WaitingRoom.playBotBtn + * ### WaitingRoom.playBtn * - * Reference to the button to play with bots + * Reference to the button to play a new game * * Will be created if requested by options. * - * @see WaitingRoom.playWithBotOption + * @see WaitingRoom.userCanDispatch */ - this.playBotBtn = null; + this.playBtn = null; /** - * ### WaitingRoom.selectTreatmentOption + * ### WaitingRoom.userCanSelectTreat * * If TRUE, it displays a selector to choose the treatment of the game * * This option is set by the server, local modifications will * not have an effect if server does not allow it */ - this.selectTreatmentOption = null; + this.userCanSelectTreat = null; /** * ### WaitingRoom.treatmentBtn * * Holds the name of selected treatment * - * Only used if `selectTreatmentOption` is enabled + * Only used if `userCanSelectTreat` is enabled * - * @see WaitingRoom.selectTreatmentOption + * @see WaitingRoom.userCanSelectTreat */ this.selectedTreatment = null; + /** + * ### WaitingRoom.addDefaultTreatments + * + * If TRUE, after the user defined treatments, it adds default ones + * + * It has effect only if WaitingRoom.userCanSelectTreat is TRUE. + * + * Default: TRUE + * + * @see WaitingRoom.userCanSelectTreat + */ + this.addDefaultTreatments = null; + + /** + * ### WaitingRoom.treatmentTiles + * + * If TRUE, treatments are displayed in tiles instead of a dropdown + * + * Default: FALSE + */ + this.treatmentTiles = null; + } // ## WaitingRoom methods @@ -357,13 +378,14 @@ * - onSuccess: function executed when all tests succeed * - waitTime: max waiting time to execute all tests (in milliseconds) * - startDate: max waiting time to execute all tests (in milliseconds) - * - playWithBotOption: displays button to dispatch players with bots - * - selectTreatmentOption: displays treatment selector + * - userCanDispatch: displays button to dispatch a new game + * - userCanSelectTreat: displays treatment selector * * @param {object} conf Configuration object. */ WaitingRoom.prototype.init = function(conf) { - var that = this; + var t, that; + that = this; if ('object' !== typeof conf) { throw new TypeError('WaitingRoom.init: conf must be object. ' + @@ -448,143 +470,52 @@ } - if (conf.playWithBotOption) this.playWithBotOption = true; - else this.playWithBotOption = false; - if (conf.selectTreatmentOption) this.selectTreatmentOption = true; - else this.selectTreatmentOption = false; - - - // Display Exec Mode. - this.displayExecMode(); - - // Button for bots and treatments. - - if (this.playWithBotOption && !document.getElementById('bot_btn')) { - // Closure to create button group. - (function(w) { - var btnGroup = document.createElement('div'); - btnGroup.role = 'group'; - btnGroup['aria-label'] = 'Play Buttons'; - btnGroup.className = 'btn-group'; - - var playBotBtn = document.createElement('input'); - playBotBtn.className = 'btn btn-primary btn-lg'; - playBotBtn.value = w.getText('playBot'); - playBotBtn.id = 'bot_btn'; - playBotBtn.type = 'button'; - playBotBtn.onclick = function() { - w.playBotBtn.value = w.getText('connectingBots'); - w.playBotBtn.disabled = true; - node.say('PLAYWITHBOT', 'SERVER', w.selectedTreatment); - setTimeout(function() { - w.playBotBtn.value = w.getText('playBot'); - w.playBotBtn.disabled = false; - }, 5000); - }; - - btnGroup.appendChild(playBotBtn); - - // Store reference in widget. - w.playBotBtn = playBotBtn; - - if (w.selectTreatmentOption) { - - var btnGroupTreatments = document.createElement('div'); - btnGroupTreatments.role = 'group'; - btnGroupTreatments['aria-label'] = 'Select Treatment'; - btnGroupTreatments.className = 'btn-group'; - - var btnTreatment = document.createElement('button'); - btnTreatment.className = 'btn btn-default btn-lg ' + - 'dropdown-toggle'; - btnTreatment['data-toggle'] = 'dropdown'; - btnTreatment['aria-haspopup'] = 'true'; - btnTreatment['aria-expanded'] = 'false'; - btnTreatment.innerHTML = w.getText('selectTreatment'); - - var span = document.createElement('span'); - span.className = 'caret'; - - btnTreatment.appendChild(span); - - var ul = document.createElement('ul'); - ul.className = 'dropdown-menu'; - ul.style['text-align'] = 'left'; - - var li, a, t, liT1, liT2, liT3; - if (conf.availableTreatments) { - li = document.createElement('li'); - li.innerHTML = w.getText('gameTreatments'); - li.className = 'dropdown-header'; - ul.appendChild(li); - for (t in conf.availableTreatments) { - if (conf.availableTreatments.hasOwnProperty(t)) { - li = document.createElement('li'); - li.id = t; - a = document.createElement('a'); - a.href = '#'; - a.innerHTML = '' + t + ': ' + - conf.availableTreatments[t]; - li.appendChild(a); - if (t === 'treatment_latin_square') liT3 = li; - else if (t === 'treatment_rotate') liT1 = li; - else if (t === 'treatment_random') liT2 = li; - else ul.appendChild(li); - } - } - li = document.createElement('li'); - li.role = 'separator'; - li.className = 'divider'; - ul.appendChild(li); - li = document.createElement('li'); - li.innerHTML = w.getText('defaultTreatments'); - li.className = 'dropdown-header'; - ul.appendChild(li); - ul.appendChild(liT1); - ul.appendChild(liT2); - ul.appendChild(liT3); - } + if (conf.userCanDispatch) this.userCanDispatch = true; + else this.userCanDispatch = false; + if (conf.userCanSelectTreat) this.userCanSelectTreat = true; + else this.userCanSelectTreat = false; + if ('undefined' !== typeof conf.addDefaultTreatments) { + this.addDefaultTreatments = !!conf.addDefaultTreatments; + } + else { + this.addDefaultTreatments = true; + } - btnGroupTreatments.appendChild(btnTreatment); - btnGroupTreatments.appendChild(ul); + // Button to start a new game and select treatments. + if (conf.queryStringTreatVar) { + t = J.getQueryString(conf.queryStringTreatVar); - btnGroup.appendChild(btnGroupTreatments); + if (t) { + if (!conf.availableTreatments[t]) { + alert('Unknown treatment: ' + t); + } + else { + node.say('DISPATCH', 'SERVER', t); + return; + } + } + } - // We are not using bootstrap js files - // and we redo the job manually here. - btnTreatment.onclick = function() { - // When '' is hidden by bootstrap class. - if (ul.style.display === '') { - ul.style.display = 'block'; - } - else { - ul.style.display = ''; - } - }; + if (conf.treatmentTileCb) { + this.treatmentTileCb = conf.treatmentTileCb; + } - ul.onclick = function(eventData) { - var t; - t = eventData.target; - // When '' is hidden by bootstrap class. - ul.style.display = ''; - t = t.parentNode.id; - // Clicked on description? - if (!t) t = eventData.target.parentNode.parentNode.id; - // Nothing relevant clicked (e.g., header). - if (!t) return; - btnTreatment.innerHTML = t + ' '; - btnTreatment.appendChild(span); - w.selectedTreatment = t; - }; + if ('undefined' !== typeof conf.treatmentTiles) { + this.treatmentTiles = conf.treatmentTiles; + } - // Store Reference in widget. - w.treatmentBtn = btnTreatment; - } - // Append button group. - w.bodyDiv.appendChild(document.createElement('br')); - w.bodyDiv.appendChild(btnGroup); + // Display Exec Mode. + this.displayExecMode(); - })(this); + // Displays treatments / play btn. + if (this.userCanDispatch) { + if (this.userCanSelectTreat) { + this.treatmentTiles ? buildTreatTiles(this, conf) : + buildTreatDropdown(this, conf) + } + else { + addPlayBtn(this); + } } // Handle destroy. @@ -646,6 +577,8 @@ * * Displays the state of the waiting room on screen * + * @param {object} update Object containing info about the waiting room + * * @see WaitingRoom.updateState */ WaitingRoom.prototype.updateState = function(update) { @@ -670,6 +603,12 @@ */ WaitingRoom.prototype.updateDisplay = function() { var numberOfGameSlots, numberOfGames; + + if (!this.execModeDiv) { + node.warn('WaitingRoom: cannot update display, inteface not ready'); + return; + } + if (this.connected > this.poolSize) { numberOfGames = Math.floor(this.connected / this.groupSize); if ('undefined' !== typeof this.nGames) { @@ -836,11 +775,6 @@ // Write about disconnection in page. that.bodyDiv.innerHTML = that.getText('disconnect'); - - // Enough to not display it in case of page refresh. - // setTimeout(function() { - // alert('Disconnection from server detected!'); - // }, 200); }); node.on.data('ROOM_CLOSED', function() { @@ -848,17 +782,22 @@ }); }; + /** + * ### WaitingRoom.stopTimer + * + * If found, it stops the timer + */ WaitingRoom.prototype.stopTimer = function() { if (this.timer) { - node.info('waiting room: STOPPING TIMER'); - this.timer.destroy(); + node.info('waiting room: PAUSING TIMER'); + this.timer.stop(); } }; /** * ### WaitingRoom.disconnect * - * Disconnects the playr, stops the timer, and displays a msg + * Disconnects the player, stops the timer, and displays a msg * * @param {string|function} msg. Optional. A disconnect message. If set, * replaces the current value for future calls. @@ -871,6 +810,11 @@ this.stopTimer(); }; + /** + * ### WaitingRoom.alertPlayer + * + * Plays a sound and blinks the title of the tab to alert the player + */ WaitingRoom.prototype.alertPlayer = function() { var clearBlink, onFrame; var blink, sound; @@ -912,4 +856,244 @@ } }; + // ### Helper functions. + + function addPlayBtn(w) { + var btnGroup, playBtn; + + // Already added. + btnGroup = document.getElementById('play_btn_group'); + if (btnGroup) return btnGroup; + + // Add button to start game. + btnGroup = document.createElement('div'); + btnGroup.id = 'play_btn_group'; + btnGroup.role = 'group'; + btnGroup['aria-label'] = 'Play Buttons'; + btnGroup.className = 'btn-group'; + + playBtn = document.createElement('input'); + playBtn.className = 'btn btn-primary btn-lg'; + playBtn.value = w.getText('playBot'); + playBtn.id = 'play_btn'; + playBtn.type = 'button'; + playBtn.onclick = function() { + w.playBtn.value = w.getText('connectingBots'); + w.playBtn.disabled = true; + node.say('DISPATCH', 'SERVER', w.selectedTreatment); + setTimeout(function() { + w.playBtn.value = w.getText('playBot'); + w.playBtn.disabled = false; + }, 5000); + }; + + btnGroup.appendChild(playBtn); + + // Store reference in widget. + w.playBtn = playBtn; + + // Append button group. + w.bodyDiv.appendChild(document.createElement('br')); + w.bodyDiv.appendChild(btnGroup); + + return btnGroup; + } + + function buildTreatDropdown(w, conf) { + + var btnGroup; + btnGroup = addPlayBtn(w); + + var btnGroupTreatments = document.createElement('div'); + btnGroupTreatments.role = 'group'; + btnGroupTreatments['aria-label'] = 'Select Treatment'; + btnGroupTreatments.className = 'btn-group'; + + var btnTreatment = document.createElement('button'); + btnTreatment.className = 'btn btn-default btn-lg ' + + 'dropdown-toggle'; + btnTreatment['data-toggle'] = 'dropdown'; + btnTreatment['aria-haspopup'] = 'true'; + btnTreatment['aria-expanded'] = 'false'; + btnTreatment.innerHTML = w.getText('selectTreatment'); + + var span = document.createElement('span'); + span.className = 'caret'; + + btnTreatment.appendChild(span); + + var ul = document.createElement('ul'); + ul.className = 'dropdown-menu'; + ul.style['text-align'] = 'left'; + + var li, a, t, liT1, liT2, liT3, liT4; + if (conf.availableTreatments) { + li = document.createElement('li'); + li.innerHTML = w.getText('gameTreatments'); + li.className = 'dropdown-header'; + ul.appendChild(li); + for (t in conf.availableTreatments) { + if (conf.availableTreatments.hasOwnProperty(t)) { + li = document.createElement('li'); + li.id = t; + a = document.createElement('a'); + a.href = '#'; + a.innerHTML = '' + t + ': ' + + conf.availableTreatments[t]; + li.appendChild(a); + if (t === 'treatment_latin_square') liT3 = li; + else if (t === 'treatment_rotate') liT1 = li; + else if (t === 'treatment_random') liT2 = li; + else if (t === 'treatment_weighted_random') liT4 = li; + else ul.appendChild(li); + } + } + + if (w.addDefaultTreatments !== false) { + li = document.createElement('li'); + li.role = 'separator'; + li.className = 'divider'; + ul.appendChild(li); + li = document.createElement('li'); + li.innerHTML = w.getText('defaultTreatments'); + li.className = 'dropdown-header'; + ul.appendChild(li); + ul.appendChild(liT1); + ul.appendChild(liT2); + ul.appendChild(liT3); + ul.appendChild(liT4); + } + } + + btnGroupTreatments.appendChild(btnTreatment); + btnGroupTreatments.appendChild(ul); + + btnGroup.appendChild(btnGroupTreatments); + + // We are not using bootstrap js files + // and we redo the job manually here. + btnTreatment.onclick = function() { + // When '' is hidden by bootstrap class. + if (ul.style.display === '') { + ul.style.display = 'block'; + } + else { + ul.style.display = ''; + } + }; + + ul.onclick = function(eventData) { + var t; + t = eventData.target; + // When '' is hidden by bootstrap class. + ul.style.display = ''; + t = t.parentNode.id; + // Clicked on description? + if (!t) t = eventData.target.parentNode.parentNode.id; + // Nothing relevant clicked (e.g., header). + if (!t) return; + btnTreatment.innerHTML = t + ' '; + btnTreatment.appendChild(span); + w.selectedTreatment = t; + }; + + // Store Reference in widget. + w.treatmentBtn = btnTreatment; + } + + function buildTreatTiles(w, conf) { + var div, a, t, T, display, counter; + var divT1, divT2, divT3, divT4; + var flexBox; + + flexBox = W.add('div', w.bodyDiv); + flexBox.style.display = 'flex'; + flexBox.style['flex-wrap'] = 'wrap'; + flexBox.style['column-gap'] = '20px'; + flexBox.style['justify-content'] = 'space-between'; + flexBox.style['margin'] = '50px 100px 30px 150px'; + flexBox.style['text-align'] = 'center'; + + // border: 1px solid #CCC; + // border-radius: 10px; + // box-shadow: 2px 2px 10px; + // FONT-WEIGHT: 200; + // padding: 10px; + + // --- CAN - SOC waitroom modification --- // + + flexBox.className = 'waitroom-listContainer'; + + // -------------- // + + + counter = 0; + if (conf.availableTreatments) { + for (t in conf.availableTreatments) { + if (conf.availableTreatments.hasOwnProperty(t)) { + div = document.createElement('div'); + div.id = t; + div.style.flex = '200px'; + div.style['margin-top'] = '10px'; + div.className = 'treatment waitroom-list'; + // div.style.display = 'flex'; + + a = document.createElement('span'); + // a.className = + // 'btn btn-default btn-large round btn-icon'; + // a.href = '#'; + if (w.treatmentTileCb) { + display = w.treatmentTileCb(t, + conf.availableTreatments[t], ++counter, w); + } + else { + T = t; + if (t.length > 16) { + T = '' + + t.substr(0, 13) + '...'; + } + display = '' + T + '
' + + '' + + conf.availableTreatments[t] + ''; + } + a.innerHTML = display; + + div.appendChild(a); + + div.onclick = function() { + var t; + t = this.id; + // Clicked on description? + // btnTreatment.innerHTML = t + ' '; + w.selectedTreatment = t; + node.say('DISPATCH', 'SERVER', + w.selectedTreatment); + }; + + t = t.substring(10); + if (t === 'latin_square') divT3 = div; + else if (t === 'rotate') divT1 = div; + else if (t === 'random') divT2 = div; + else if (t === 'weighted_random') divT4 = div; + else flexBox.appendChild(div); + + } + } + + // Hack to fit nicely the treatments. + // div = document.createElement('div'); + // div.style.flex = '200px'; + // div.style['margin-top'] = '10px'; + // div.className = 'waitroom-list'; + // flexBox.appendChild(div); + + if (w.addDefaultTreatments !== false) { + flexBox.appendChild(divT1); + flexBox.appendChild(divT2); + flexBox.appendChild(divT3); + flexBox.appendChild(divT4); + } + } + } + })(node);