From 2cc9a768a45fa1a12afdc670d0e75f0e36c531b3 Mon Sep 17 00:00:00 2001 From: Stefano Balieti Date: Wed, 31 Mar 2021 09:18:44 +0200 Subject: [PATCH 01/51] built --- build/nodegame-full.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 0444a160..b107e8cb 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -5469,7 +5469,7 @@ if (!Array.prototype.indexOf) { * @see PARSE.isFloat */ PARSE.isNumber = function(n, lower, upper, leq, ueq) { - if (isNaN(n) || !isFinite(n)) return false; + if (isNaN(n) || !isFinite(n) || n === "") return false; n = parseFloat(n); if ('number' === typeof lower && (leq ? n < lower : n <= lower)) { return false; @@ -45405,7 +45405,7 @@ if (!Array.prototype.indexOf) { /** * # ChoiceTable - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a configurable table where each cell is a selectable choice @@ -45422,7 +45422,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceTable.version = '1.7.0'; + ChoiceTable.version = '1.8.0'; ChoiceTable.description = 'Creates a configurable table where ' + 'each cell is a selectable choice.'; @@ -46701,7 +46701,10 @@ if (!Array.prototype.indexOf) { } // Set table id. this.table.id = this.id; - if (this.className) J.addClass(this.table, this.className); + // Class. + tmp = this.className ? [ this.className ] : []; + if (this.orientation !== 'H') tmp.push('choicetable-vertical'); + if (tmp.length) J.addClass(this.table, tmp); else this.table.className = ''; // Append table. this.bodyDiv.appendChild(this.table); @@ -46932,8 +46935,8 @@ if (!Array.prototype.indexOf) { * * @param {string|number} i The numeric position of a choice in display * - * @return {string|undefined} The value associated the numeric position. - * If no value is found, returns undefined + * @return {string|undefined} The value associated with the numeric + * position. If no value is found, returns undefined * * @see ChoiceTable.order * @see ChoiceTable.choices @@ -47156,7 +47159,7 @@ if (!Array.prototype.indexOf) { // This is the positional index. j = J.randomInt(-1, (this.choicesCells.length-1)); // If shuffled, we need to resolve it. - choice = this.shuffleChoices ? this.getChoiceAtPosition(j) : j; + choice = this.shuffleChoices ? this.choicesValues[j] : j; // Do not click it again if it is already selected. if (!this.isChoiceCurrent(choice)) this.choicesCells[j].click(); } @@ -47359,12 +47362,12 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceTableGroup.version = '1.6.1'; + ChoiceTableGroup.version = '1.7.0'; ChoiceTableGroup.description = 'Groups together and manages sets of ' + 'ChoiceTable widgets.'; ChoiceTableGroup.title = 'Make your choice'; - ChoiceTableGroup.className = 'choicetable'; // TODO: choicetablegroup? + ChoiceTableGroup.className = 'choicetable choicetablegroup'; ChoiceTableGroup.separator = '::'; @@ -47973,11 +47976,11 @@ if (!Array.prototype.indexOf) { if (opts.header) { if (!J.isArray(opts.header) || - opts.header.length !== opts.items.length - 1) { + opts.header.length !== opts.choices.length) { throw new Error('ChoiceTableGroup.init: header ' + 'must be an array of length ' + - (opts.items.length - 1) + + opts.choices.length + ' or undefined. Found: ' + opts.header); } @@ -50848,12 +50851,12 @@ if (!Array.prototype.indexOf) { // ## Meta-data - CustomInputGroup.version = '0.2.0'; + CustomInputGroup.version = '0.3.0'; CustomInputGroup.description = 'Groups together and manages sets of ' + 'CustomInput widgets.'; CustomInputGroup.title = false; - CustomInputGroup.className = 'custominputgroup'; + CustomInputGroup.className = 'custominput custominputgroup'; CustomInputGroup.separator = '::'; @@ -50878,9 +50881,7 @@ if (!Array.prototype.indexOf) { * If a `table` option is specified, it sets it as main * table. All other options are passed to the init method. */ - function CustomInputGroup(options) { - var that; - that = this; + function CustomInputGroup() { /** * ### CustomInputGroup.dl From bc9879de185620ad3ac790afb5953ebc82cad8c9 Mon Sep 17 00:00:00 2001 From: Stefano Balieti Date: Wed, 31 Mar 2021 09:29:11 +0200 Subject: [PATCH 02/51] fixed stepRule OTHERS_SYNC_STAGE --- build/nodegame-full.js | 135 +++++++++++++++++++++++---------------- lib/modules/stepRules.js | 3 +- 2 files changed, 82 insertions(+), 56 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 9602a4ea..64c8ec48 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -5469,7 +5469,7 @@ if (!Array.prototype.indexOf) { * @see PARSE.isFloat */ PARSE.isNumber = function(n, lower, upper, leq, ueq) { - if (isNaN(n) || !isFinite(n)) return false; + if (isNaN(n) || !isFinite(n) || n === "") return false; n = parseFloat(n); if ('number' === typeof lower && (leq ? n < lower : n <= lower)) { return false; @@ -10676,7 +10676,7 @@ if (!Array.prototype.indexOf) { /** * # Stepping Rules - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Collections of rules to determine whether the game should step forward. @@ -10777,6 +10777,7 @@ if (!Array.prototype.indexOf) { exports.stepRules.OTHERS_SYNC_STAGE = function(stage, myStageLevel, pl, game) { + var nSteps; if (!pl.size()) return false; stage = pl.first().stage; nSteps = game.plot.stepsToNextStage(stage); @@ -32823,7 +32824,7 @@ if (!Array.prototype.indexOf) { /** * # GameWindow - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * API to interface nodeGame with the browser window @@ -33676,17 +33677,16 @@ if (!Array.prototype.indexOf) { * * Appends a configurable div element at to "top" of the page * - * @param {Element} root Optional. The HTML element to which the info - * panel will be appended. Default: + * @param {object} opts Optional. Configuration options: TODO * - * - above the main frame, or - * - below the header, or - * - inside _documents.body_. + * - toggleBtn + * - toggleBtnLabel + * - toggleBtnRoot: + * - force: destroys current Info Panel * - * @param {string} frameName Optional. The name of the iframe. Default: - * 'ng_mainframe' * @param {boolean} force Optional. Will create the frame even if an - * existing one is found. Default: FALSE + * existing one is found. Deprecated, use force flag in options. + * Default: FALSE * * @return {InfoPanel} A reference to the InfoPanel object * @@ -33698,12 +33698,20 @@ if (!Array.prototype.indexOf) { var infoPanelDiv, root, btn; opts = opts || {}; - if (force) { + // Backward compatible. + if ('undefined' === typeof force) force = opts.force; + + if (force && this.infoPanel) { this.infoPanel.destroy(); this.infoPanel = null; } if (this.infoPanel) { + if (this.infoPanel.toggleBtn) { + if ('undefined' === typeof opts.toggleBtn) { + opts.toggleBtn = false; + } + } // if (!force) { // throw new Error('GameWindow.generateInfoPanel: info panel is ' + // 'already existing. Use force to regenerate.'); @@ -33735,26 +33743,26 @@ if (!Array.prototype.indexOf) { } // Adds Toggle Button if not false. - if (opts.btn !== false) { + if (opts.toggleBtn !== false) { root = null; - if (!opts.btnRoot) { + if (!opts.toggleBtnRoot) { if (this.headerElement) root = this.headerElement; } else { - if ('string' === typeof opts.btnRoot) { - root = W.gid(opts.btnRoot); + if ('string' === typeof opts.toggleBtnRoot) { + root = W.gid(opts.toggleBtnRoot); } else { - root = opts.btnRoot; + root = opts.toggleBtnRoot; } if (!J.isElement(root)) { - throw new Error('GameWindow.generateInfoPanel: btnRoot ' + - 'did not resolve to a valid HTMLElement: ' + - opts.btnRoot); + throw new Error('GameWindow.generateInfoPanel: ' + + 'toggleBtnRoot did not resolve to a ' + + 'valid HTMLElement: ' + opts.toggleBtnRoot); } } if (root) { - btn = W.infoPanel.createToggleButton(opts.btnLabel); + btn = W.infoPanel.createToggleBtn(opts.toggleBtnLabel); root.appendChild(btn); } } @@ -36650,6 +36658,7 @@ if (!Array.prototype.indexOf) { * @see InfoPanel.toggleBtn * @see InfoPanel.toggle */ + InfoPanel.prototype.createToggleBtn = InfoPanel.prototype.createToggleButton = function(label) { var that, button; @@ -40483,7 +40492,7 @@ if (!Array.prototype.indexOf) { // block node.done(). if (options.required || options.requiredChoice || - options.correctChoice) { + 'undefined' !== typeof options.correctChoice) { // Flag required is undefined, if not set to false explicitely. widget.required = true; @@ -44657,7 +44666,7 @@ if (!Array.prototype.indexOf) { /** * # ChoiceTable - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a configurable table where each cell is a selectable choice @@ -44674,7 +44683,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceTable.version = '1.7.0'; + ChoiceTable.version = '1.8.0'; ChoiceTable.description = 'Creates a configurable table where ' + 'each cell is a selectable choice.'; @@ -45953,7 +45962,10 @@ if (!Array.prototype.indexOf) { } // Set table id. this.table.id = this.id; - if (this.className) J.addClass(this.table, this.className); + // Class. + tmp = this.className ? [ this.className ] : []; + if (this.orientation !== 'H') tmp.push('choicetable-vertical'); + if (tmp.length) J.addClass(this.table, tmp); else this.table.className = ''; // Append table. this.bodyDiv.appendChild(this.table); @@ -46184,8 +46196,8 @@ if (!Array.prototype.indexOf) { * * @param {string|number} i The numeric position of a choice in display * - * @return {string|undefined} The value associated the numeric position. - * If no value is found, returns undefined + * @return {string|undefined} The value associated with the numeric + * position. If no value is found, returns undefined * * @see ChoiceTable.order * @see ChoiceTable.choices @@ -46408,7 +46420,7 @@ if (!Array.prototype.indexOf) { // This is the positional index. j = J.randomInt(-1, (this.choicesCells.length-1)); // If shuffled, we need to resolve it. - choice = this.shuffleChoices ? this.getChoiceAtPosition(j) : j; + choice = this.shuffleChoices ? this.choicesValues[j] : j; // Do not click it again if it is already selected. if (!this.isChoiceCurrent(choice)) this.choicesCells[j].click(); } @@ -46594,7 +46606,7 @@ if (!Array.prototype.indexOf) { /** * # ChoiceTableGroup - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a table that groups together several choice tables widgets @@ -46611,12 +46623,12 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceTableGroup.version = '1.6.1'; + ChoiceTableGroup.version = '1.7.0'; ChoiceTableGroup.description = 'Groups together and manages sets of ' + 'ChoiceTable widgets.'; ChoiceTableGroup.title = 'Make your choice'; - ChoiceTableGroup.className = 'choicetable'; // TODO: choicetablegroup? + ChoiceTableGroup.className = 'choicetable choicetablegroup'; ChoiceTableGroup.separator = '::'; @@ -47225,11 +47237,11 @@ if (!Array.prototype.indexOf) { if (opts.header) { if (!J.isArray(opts.header) || - opts.header.length !== opts.items.length - 1) { + opts.header.length !== opts.choices.length) { throw new Error('ChoiceTableGroup.init: header ' + 'must be an array of length ' + - (opts.items.length - 1) + + opts.choices.length + ' or undefined. Found: ' + opts.header); } @@ -50100,12 +50112,12 @@ if (!Array.prototype.indexOf) { // ## Meta-data - CustomInputGroup.version = '0.2.0'; + CustomInputGroup.version = '0.3.0'; CustomInputGroup.description = 'Groups together and manages sets of ' + 'CustomInput widgets.'; CustomInputGroup.title = false; - CustomInputGroup.className = 'custominputgroup'; + CustomInputGroup.className = 'custominput custominputgroup'; CustomInputGroup.separator = '::'; @@ -50130,9 +50142,7 @@ if (!Array.prototype.indexOf) { * If a `table` option is specified, it sets it as main * table. All other options are passed to the init method. */ - function CustomInputGroup(options) { - var that; - that = this; + function CustomInputGroup() { /** * ### CustomInputGroup.dl @@ -50979,14 +50989,14 @@ if (!Array.prototype.indexOf) { // res.err = this.getText('inputErr'); this.validation(res, values); if (opts.highlight && res.err) this.setError(res.err); - + if (res.err) res.isCorrect = false; } else if (toReset) this.reset(toReset); if (!res.isCorrect && opts.highlight) this.highlight(); // Restore opts.reset. opts.reset = toReset; - + if (this.textarea) res.freetext = this.textarea.value; return res; }; @@ -55881,15 +55891,15 @@ if (!Array.prototype.indexOf) { str += 'Every box contains a prize of ' + widget.boxValue + ' ' + widget.currency + ', but '; if (probBomb === 1) { - str += 'one box contains a bomb.'; + str += 'one random box contains a bomb.'; } else { if (widget.revealProbBomb) { str += 'with probability ' + probBomb + - ' one of those boxes contains a bomb.'; + ' one random box contains a bomb.'; } else { - str += 'one of those boxes might contain a bomb.'; + str += 'one random box might contain a bomb.'; } } str += ' You must decide how many boxes you want to open.'; @@ -57014,7 +57024,7 @@ if (!Array.prototype.indexOf) { /** * # SVOGauge - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Displays an interface to measure users' social value orientation (S.V.O.) @@ -57029,16 +57039,25 @@ if (!Array.prototype.indexOf) { // ## Meta-data - SVOGauge.version = '0.7.0'; + SVOGauge.version = '0.8.1'; SVOGauge.description = 'Displays an interface to measure social ' + 'value orientation (S.V.O.).'; SVOGauge.title = 'SVO Gauge'; SVOGauge.className = 'svogauge'; - SVOGauge.texts.mainText = 'Select your preferred option among those' + - ' available below:'; - SVOGauge.texts.left = 'You:
Other:'; + SVOGauge.texts = { + mainText: 'You and another randomly selected participant ' + + '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 ' + + 'and add the bonus to your and the ' + + 'other participant\'s payment. Your choice will remain ' + + 'anonymous.', + + left: 'Your Bonus:
Other\'s Bonus:' + }; // ## Dependencies @@ -57131,10 +57150,11 @@ if (!Array.prototype.indexOf) { } this.method = opts.method; } - if (opts.mainText) { - if ('string' !== typeof opts.mainText) { + if ('undefined' !== typeof opts.mainText) { + if (opts.mainText !== false && 'string' !== typeof opts.mainText) { throw new TypeError('SVOGauge.init: mainText must be string ' + - 'or undefined. Found: ' + opts.mainText); + 'false, or undefined. Found: ' + + opts.mainText); } this.mainText = opts.mainText; } @@ -57245,7 +57265,7 @@ if (!Array.prototype.indexOf) { // ### SVO_Slider function SVO_Slider(options) { - var items, sliders; + var items, sliders, mainText; var gauge, i, len; var renderer; @@ -57337,14 +57357,19 @@ if (!Array.prototype.indexOf) { }; } + if (this.mainText) { + mainText = this.mainText; + } + else if ('undefined' === typeof this.mainText) { + mainText = this.getText('mainText'); + } gauge = node.widgets.get('ChoiceTableGroup', { id: options.id || 'svo_slider', items: items, - // TODO: should it be on getText at all? - mainText: this.mainText || this.getText('mainText'), + mainText: mainText, title: false, renderer: renderer, - requiredChoice: true, + requiredChoice: this.required, storeRef: false }); diff --git a/lib/modules/stepRules.js b/lib/modules/stepRules.js index 48f37f78..1fde867b 100644 --- a/lib/modules/stepRules.js +++ b/lib/modules/stepRules.js @@ -1,6 +1,6 @@ /** * # Stepping Rules - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Collections of rules to determine whether the game should step forward. @@ -101,6 +101,7 @@ exports.stepRules.OTHERS_SYNC_STAGE = function(stage, myStageLevel, pl, game) { + var nSteps; if (!pl.size()) return false; stage = pl.first().stage; nSteps = game.plot.stepsToNextStage(stage); From 4b16592bfa5dd6cdbd9840d9bd4cbba47918b6a8 Mon Sep 17 00:00:00 2001 From: Stefano Balieti Date: Thu, 8 Apr 2021 09:35:26 +0200 Subject: [PATCH 03/51] deprecated instead of backward-incompatible changes in stageBlock and stepBlock --- build/nodegame-full.js | 160 ++++++++++++++++++++++++++---------- lib/stager/stager_blocks.js | 36 +++++--- 2 files changed, 142 insertions(+), 54 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index b107e8cb..cee8a809 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -42304,7 +42304,7 @@ if (!Array.prototype.indexOf) { /** * # Chat - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a simple configurable chat @@ -42747,7 +42747,7 @@ if (!Array.prototype.indexOf) { innerHTML: this.getText('submitButton') }); this.submitButton.onclick = function() { - sendMsg(that); + that.sendMsg(); if ('function' === typeof that.textarea.focus) { that.textarea.focus(); } @@ -42758,7 +42758,7 @@ if (!Array.prototype.indexOf) { this.textarea.onkeydown = function(e) { if (that.useSubmitEnter) { e = e || window.event; - if ((e.keyCode || e.which) === 13) sendMsg(that); + if ((e.keyCode || e.which) === 13) that.sendMsg(); else sendAmTyping(that); } else if (that.showIsTyping) { @@ -42996,21 +42996,32 @@ if (!Array.prototype.indexOf) { totUnread: this.stats.unread, initialMsg: this.initialMsg }; - if (this.db) out.msgs = db.fetch(); + if (this.db) out.msgs = this.db.fetch(); return out; }; - // ## Helper functions. - - // ### sendMsg + // ### Chat.sendMsg // Reads the textarea and delivers the msg to the server. - function sendMsg(that) { - var msg, to, ids; + Chat.prototype.sendMsg = function(msg, opts) { + var to, ids, that; + opts = opts || {}; // No msg sent. - if (that.isDisabled()) return; + if (this.isDisabled()) { + node.warn('Chat is disable, msg not sent.'); + return; + } - msg = that.readTextarea(); + if ('undefined' !== typeof msg) { + msg += ''; + if ('string' !== typeof msg) { + throw new TypeError('Chat.sendMsg: msg must be string, ' + + 'number, or undefined. Found: ' + msg); + } + } + else { + msg = this.readTextarea(); + } // Move cursor at the beginning. if (msg === '') { @@ -43018,22 +43029,33 @@ if (!Array.prototype.indexOf) { return; } // Simplify things, if there is only one recipient. - ids = that.recipientsIds; + ids = opts.recipients || this.recipientsIds; if (ids.length === 0) { node.warn('Chat: empty recipient list, message not sent.'); return; } + // Make it a number if array of size 1, so it is faster. to = ids.length === 1 ? ids[0] : ids; - that.writeMsg('outgoing', { msg: msg }); // to not used now. - node.say(that.chatEvent, to, msg); - // Make sure the cursor goes back to top. - setTimeout(function() { that.textarea.value = ''; }); - // Clear any typing timeout. - if (that.amTypingTimeout) { - clearTimeout(that.amTypingTimeout); - that.amTypingTimeout = null; + + node.say(this.chatEvent, to, msg); + + if (!opts.silent) { + that = this; + // TODO: check the comment: // to not used now. + this.writeMsg('outgoing', { msg: msg }); + + // Make sure the cursor goes back to top. + setTimeout(function() { that.textarea.value = ''; }); + // Clear any typing timeout. + if (this.amTypingTimeout) { + clearTimeout(this.amTypingTimeout); + this.amTypingTimeout = null; + } } } + + // ## Helper functions. + // ### sendMsg // Reads the textarea and delivers the msg to the server. function sendAmTyping(that) { @@ -45429,36 +45451,49 @@ if (!Array.prototype.indexOf) { ChoiceTable.title = 'Make your choice'; ChoiceTable.className = 'choicetable'; - ChoiceTable.texts.autoHint = function(w) { - var res; - if (!w.requiredChoice && !w.selectMultiple) return false; - if (!w.selectMultiple) return '*'; - res = '('; - if (!w.requiredChoice) { - if ('number' === typeof w.selectMultiple) { - res += 'select up to ' + w.selectMultiple; + ChoiceTable.texts = { + + autoHint: function(w) { + var res; + if (!w.requiredChoice && !w.selectMultiple) return false; + if (!w.selectMultiple) return '*'; + res = '('; + if (!w.requiredChoice) { + if ('number' === typeof w.selectMultiple) { + res += 'select up to ' + w.selectMultiple; + } + else { + res += 'multiple selection allowed'; + } } else { - res += 'multiple selection allowed'; - } - } - else { - if ('number' === typeof w.selectMultiple) { - if (w.selectMultiple === w.requiredChoice) { - res += 'select ' + w.requiredChoice; + if ('number' === typeof w.selectMultiple) { + if (w.selectMultiple === w.requiredChoice) { + res += 'select ' + w.requiredChoice; + } + else { + res += 'select between ' + w.requiredChoice + + ' and ' + w.selectMultiple; + } } else { - res += 'select between ' + w.requiredChoice + - ' and ' + w.selectMultiple; + res += 'select at least ' + w.requiredChoice; } } - else { - res += 'select at least ' + w.requiredChoice; + res += ')'; + if (w.requiredChoice) res += ' *'; + return res; + }, + error: function(w, value) { + if (value !== null && + ('number' === typeof w.correctChoice || + 'string' === typeof w.correctChoice)) { + + return 'Not correct, try again.'; } + return 'Selection required.'; } - res += ')'; - if (w.requiredChoice) res += ' *'; - return res; + // correct: 'Correct.' }; ChoiceTable.separator = '::'; @@ -45727,6 +45762,20 @@ if (!Array.prototype.indexOf) { */ this.rightCell = null; + /** + * ### CustomInput.errorBox + * + * An HTML element displayed when a validation error occurs + */ + this.errorBox = null; + + /** + * ### CustomInput.successBox + * + * An HTML element displayed when a validation error occurs + */ + this.successBox = null; + /** * ### ChoiceTable.timeCurrentChoice * @@ -45758,7 +45807,7 @@ if (!Array.prototype.indexOf) { /** * ### ChoiceTable.correctChoice * - * The array of correct choice/s + * The correct choice/s * * The field is an array or number|string depending * on the value of ChoiceTable.selectMultiple @@ -46710,6 +46759,9 @@ if (!Array.prototype.indexOf) { this.bodyDiv.appendChild(this.table); } + this.errorBox = W.append('div', this.bodyDiv, { className: 'errbox' }); + + // Creates a free-text textarea, possibly with placeholder text. if (this.freeText) { this.textarea = document.createElement('textarea'); @@ -46724,6 +46776,22 @@ if (!Array.prototype.indexOf) { } }; + /** + * ### ChoiceTable.setError + * + * Set the error msg inside the errorBox and call highlight + * + * @param {string} The error msg (can contain HTML) + * + * @see ChoiceTable.highlight + * @see ChoiceTable.errorBox + */ + ChoiceTable.prototype.setError = function(err) { + this.errorBox.innerHTML = err || ''; + if (err) this.highlight(); + else this.unhighlight(); + }; + /** * ### ChoiceTable.listeners * @@ -46978,6 +47046,7 @@ if (!Array.prototype.indexOf) { if (!this.table || this.highlighted !== true) return; this.table.style.border = ''; this.highlighted = false; + this.setError(); this.emit('unhighlighted'); }; @@ -47059,7 +47128,10 @@ if (!Array.prototype.indexOf) { if (!obj.isCorrect && opts.highlight) this.highlight(); } if (this.textarea) obj.freetext = this.textarea.value; - if (obj.isCorrect !== false && opts.reset) { + if (obj.isCorrect === false) { + this.setError(this.getText('error', obj.value)); + } + else if (opts.reset) { resetOpts = 'object' !== typeof opts.reset ? {} : opts.reset; this.reset(resetOpts); } diff --git a/lib/stager/stager_blocks.js b/lib/stager/stager_blocks.js index edfad7d2..bb9b7f2c 100644 --- a/lib/stager/stager_blocks.js +++ b/lib/stager/stager_blocks.js @@ -36,12 +36,20 @@ Stager.prototype.stepBlock = function(id, positions) { var curBlock, err; - if ('string' !== typeof id || id.trim() === '') { - throw new TypeError('Stager.stepBlock: id must be a non-empty ' + - 'string. Found: ' + id); + if (arguments.length === 1) { + console.log('***deprecation warning: Stager.stepBlock will ' + + 'require two parameters in the next version.***'); + + positions = id; } - if (this.blocksIds[id]) { - throw new Error('Stager.stepBlock: non-unique id: ' + id); + else { + if ('string' !== typeof id || id.trim() === '') { + throw new TypeError('Stager.stepBlock: id must be a ' + + 'non-empty string. Found: ' + id); + } + if (this.blocksIds[id]) { + throw new Error('Stager.stepBlock: non-unique id: ' + id); + } } // Check if a stage block can be added in this position. @@ -94,12 +102,20 @@ Stager.prototype.stageBlock = function(id, positions) { var curBlock, err; - if ('string' !== typeof id || id.trim() === '') { - throw new TypeError('Stager.stageBlock: id must be a non-empty ' + - 'string. Found: ' + id); + if (arguments.length === 1) { + console.log('***deprecation warning: Stager.stageBlock will ' + + 'require two parameters in the next version.***'); + + positions = id; } - if (this.blocksIds[id]) { - throw new Error('Stager.stageBlock: non-unique id: ' + id); + else { + if ('string' !== typeof id || id.trim() === '') { + throw new TypeError('Stager.stageBlock: id must be a ' + + 'non-empty string. Found: ' + id); + } + if (this.blocksIds[id]) { + throw new Error('Stager.stageBlock: non-unique id: ' + id); + } } // Check if a stage block can be added in this position. From ea2e32cdda55fecfa2d91450541606640abfc1df Mon Sep 17 00:00:00 2001 From: Stefano Balieti Date: Thu, 8 Apr 2021 09:40:39 +0200 Subject: [PATCH 04/51] CHANGELOG --- CHANGELOG | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 58c9ad9a..01bcaa7c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,17 @@ # nodegame-client change log ## Current -- DONE is async to respect to node.game.step to let other listeners on DONE +- DONE is async with respect to node.game.step to let other listeners on DONE finish first. - missValues from widgets does not block next step execution. - Widgets' action required is not checked if the timer is expired. - Fixed node.once.data removing all node.once data listeners after first exec. +- Improved error-checkings with stager.stageBlock and stager.stepBlock. +- Added stager.extendSteps and stager.extendStages. +- Improved stager.skip and stager.unskip: support for arrays. +- Improved stager.isSkipped: checks if all steps inside a stage are skipped to +determine if a stage is skipped. +- Fixing typos, improving doc. ## 6.1.0 - Fixed exit callback of stages leaked into steps. From 1203d669d702aaaa3933d7f43373911f9b8ccf13 Mon Sep 17 00:00:00 2001 From: Stefano Balieti Date: Thu, 8 Apr 2021 09:42:23 +0200 Subject: [PATCH 05/51] changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 01bcaa7c..5d710c18 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ finish first. - missValues from widgets does not block next step execution. - Widgets' action required is not checked if the timer is expired. - Fixed node.once.data removing all node.once data listeners after first exec. +- Fixed step-rule OTHERS_SYNC_STAGE. - Improved error-checkings with stager.stageBlock and stager.stepBlock. - Added stager.extendSteps and stager.extendStages. - Improved stager.skip and stager.unskip: support for arrays. From 27120759aad817385de470f48e0b107dfbc95e1f Mon Sep 17 00:00:00 2001 From: Stefano Balieti Date: Thu, 8 Apr 2021 14:40:26 +0200 Subject: [PATCH 06/51] minor --- build/nodegame-full.js | 154 +++++++++++++++++++++++++++++++--------- lib/sockets/SocketIo.js | 2 +- 2 files changed, 121 insertions(+), 35 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 59a049e6..92c66da9 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -19462,12 +19462,20 @@ if (!Array.prototype.indexOf) { Stager.prototype.stepBlock = function(id, positions) { var curBlock, err; - if ('string' !== typeof id || id.trim() === '') { - throw new TypeError('Stager.stepBlock: id must be a non-empty ' + - 'string. Found: ' + id); + if (arguments.length === 1) { + console.log('***deprecation warning: Stager.stepBlock will ' + + 'require two parameters in the next version.***'); + + positions = id; } - if (this.blocksIds[id]) { - throw new Error('Stager.stepBlock: non-unique id: ' + id); + else { + if ('string' !== typeof id || id.trim() === '') { + throw new TypeError('Stager.stepBlock: id must be a ' + + 'non-empty string. Found: ' + id); + } + if (this.blocksIds[id]) { + throw new Error('Stager.stepBlock: non-unique id: ' + id); + } } // Check if a stage block can be added in this position. @@ -19520,12 +19528,20 @@ if (!Array.prototype.indexOf) { Stager.prototype.stageBlock = function(id, positions) { var curBlock, err; - if ('string' !== typeof id || id.trim() === '') { - throw new TypeError('Stager.stageBlock: id must be a non-empty ' + - 'string. Found: ' + id); + if (arguments.length === 1) { + console.log('***deprecation warning: Stager.stageBlock will ' + + 'require two parameters in the next version.***'); + + positions = id; } - if (this.blocksIds[id]) { - throw new Error('Stager.stageBlock: non-unique id: ' + id); + else { + if ('string' !== typeof id || id.trim() === '') { + throw new TypeError('Stager.stageBlock: id must be a ' + + 'non-empty string. Found: ' + id); + } + if (this.blocksIds[id]) { + throw new Error('Stager.stageBlock: non-unique id: ' + id); + } } // Check if a stage block can be added in this position. @@ -20821,7 +20837,7 @@ if (!Array.prototype.indexOf) { socket = io.connect(url, options); //conf.io - socket.on('connect', function(msg) { + socket.on('connect', function() { node.info('socket.io connection open'); node.socket.onConnect.call(node.socket); socket.on('message', function(msg) { @@ -42363,7 +42379,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - Chat.version = '1.2.1'; + Chat.version = '1.3.0'; Chat.description = 'Offers a uni-/bi-directional communication interface ' + 'between players, or between players and the server.'; @@ -42576,6 +42592,22 @@ if (!Array.prototype.indexOf) { * Once created */ this.isTypingDivs = {}; + + /** + * ### Chat.preprocessMsg + * + * A function that process the msg before being displayed. + * + * Example: + * + * ```js + * function(data, code) { + * data.msg += '!'; + * } + * ``` + */ + this.preprocessMsg = null; + } // ## Chat methods @@ -42611,9 +42643,21 @@ if (!Array.prototype.indexOf) { */ Chat.prototype.init = function(opts) { var tmp, i, rec, sender, that; - + opts = opts || {}; that = this; + // Receiver Only. + this.receiverOnly = !!opts.receiverOnly; + + tmp = opts.preprocessMsg; + if ('function' === typeof tmp) { + this.preprocessMsg = tmp; + } + else if (tmp) { + throw new TypeError('Chat.init: preprocessMsg must be function ' + + 'or undefined. Found: ' + tmp); + } + // Chat id. tmp = opts.chatEvent; if (tmp) { @@ -42841,6 +42885,20 @@ if (!Array.prototype.indexOf) { return c; }; + Chat.prototype.renderMsg = function(data, code) { + var msg; + if ('function' === typeof this.preprocessMsg) { + this.preprocessMsg(data, code); + } + if ('function' === typeof data.msg) { + msg = data.msg(data, code); + } + else { + msg = data.msg; + } + return msg; + }; + /** * ### Chat.scrollToBottom * @@ -42868,7 +42926,11 @@ if (!Array.prototype.indexOf) { } // Remove is typing sign, if any. that.clearIsTyping(msg.from); - that.writeMsg('incoming', { msg: msg.data, id: msg.from }); + msg = { + msg: that.renderMsg(msg.data, 'incoming'), + id: msg.from + }; + that.writeMsg('incoming', msg); }); node.on.data(this.chatEvent + '_QUIT', function(msg) { @@ -42928,7 +42990,7 @@ if (!Array.prototype.indexOf) { this.isTypingTimeouts[id] = setTimeout(function() { that.clearIsTyping(id); that.isTypingTimeouts[id] = null; - }, 5000); + }, 3000); }; Chat.prototype.clearIsTyping = function(id) { @@ -42957,7 +43019,7 @@ if (!Array.prototype.indexOf) { * @see Chat.chatDiv */ Chat.prototype.handleMsg = function(msg) { - var from, args; + var from; from = msg.from; if (from === node.player.id || from === node.player.sid) { node.warn('Chat: your own message came back: ' + msg.id); @@ -43001,11 +43063,20 @@ if (!Array.prototype.indexOf) { return out; }; - // ### Chat.sendMsg - // Reads the textarea and delivers the msg to the server. - Chat.prototype.sendMsg = function(msg, opts) { + /* ### Chat.sendMsg + * + * Delivers a msg to the server + * + * If no options are specified, it reads the textarea. + * + * @param {object} opts Optional. Configutation options: + * - msg: the msg to send. If undefined, it reads the value from textarea; + * if function it executes it and uses the return value. + * - recipients: array of recipients. Default: this.recipientsIds. + * - silent: does not write the msg on the chat. + */ + Chat.prototype.sendMsg = function(opts) { var to, ids, that; - opts = opts || {}; // No msg sent. if (this.isDisabled()) { @@ -43013,19 +43084,33 @@ if (!Array.prototype.indexOf) { return; } - if ('undefined' !== typeof msg) { - msg += ''; - if ('string' !== typeof msg) { - throw new TypeError('Chat.sendMsg: msg must be string, ' + - 'number, or undefined. Found: ' + msg); + if ('object' === typeof opts) { + if ('undefined' !== typeof opts.msg) { + if ('object' === typeof opts.msg) { + throw new TypeError('Chat.sendMsg: opts.msg cannot be ' + + 'object. Found: ' + opts.msg); + } } } else { - msg = this.readTextarea(); + if ('undefined' === typeof opts) { + opts = { msg: this.readTextarea() }; + } + else if ('string' === typeof opts || 'number' === typeof opts) { + opts = { msg: opts }; + } + else { + throw new TypeError('Chat.sendMsg: opts must be string, ' + + 'number, object, or undefined. Found: ' + + opts); + } } + // Calls preprocessMsg and if opts.msg is function, executes it. + opts.msg = this.renderMsg(opts, 'outgoing'); + // Move cursor at the beginning. - if (msg === '') { + if (opts.msg === '') { node.warn('Chat: message has no text, not sent.'); return; } @@ -43038,20 +43123,21 @@ if (!Array.prototype.indexOf) { // Make it a number if array of size 1, so it is faster. to = ids.length === 1 ? ids[0] : ids; - node.say(this.chatEvent, to, msg); + node.say(this.chatEvent, to, opts); if (!opts.silent) { that = this; // TODO: check the comment: // to not used now. - this.writeMsg('outgoing', { msg: msg }); + this.writeMsg('outgoing', opts); // Make sure the cursor goes back to top. setTimeout(function() { that.textarea.value = ''; }); - // Clear any typing timeout. - if (this.amTypingTimeout) { - clearTimeout(this.amTypingTimeout); - this.amTypingTimeout = null; - } + } + + // Clear any typing timeout. + if (this.amTypingTimeout) { + clearTimeout(this.amTypingTimeout); + this.amTypingTimeout = null; } } diff --git a/lib/sockets/SocketIo.js b/lib/sockets/SocketIo.js index 1af4d910..6ae8593c 100644 --- a/lib/sockets/SocketIo.js +++ b/lib/sockets/SocketIo.js @@ -68,7 +68,7 @@ socket = io.connect(url, options); //conf.io - socket.on('connect', function(msg) { + socket.on('connect', function() { node.info('socket.io connection open'); node.socket.onConnect.call(node.socket); socket.on('message', function(msg) { From c2d438625de3710c681c59024560d22ea14a4415 Mon Sep 17 00:00:00 2001 From: Stefano Balieti Date: Thu, 8 Apr 2021 14:42:29 +0200 Subject: [PATCH 07/51] 6.2.0 --- CHANGELOG | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5d710c18..1f9483b9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # nodegame-client change log -## Current +## 6.2.0 - DONE is async with respect to node.game.step to let other listeners on DONE finish first. - missValues from widgets does not block next step execution. diff --git a/package.json b/package.json index a19c7455..fc745750 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nodegame-client", "description": "nodeGame client for the browser and node.js", - "version": "6.1.0", + "version": "6.2.0", "homepage": "http://www.nodegame.org", "keywords": [ "game", From 9e2384a20f3865b4a85b18af173a5ad216a48d2a Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 20 May 2021 11:49:57 +0200 Subject: [PATCH 08/51] SizeManager fix check --- lib/core/SizeManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/SizeManager.js b/lib/core/SizeManager.js index fe24ae20..99c32c33 100644 --- a/lib/core/SizeManager.js +++ b/lib/core/SizeManager.js @@ -409,7 +409,7 @@ * @param {number|array} The value/s for the handler */ SizeManager.prototype.setHandler = function(type, values) { - values = checkMinMaxExactParams('min', values, this.node); + values = checkMinMaxExactParams(type, values, this.node); this[type + 'Threshold'] = values[0]; this[type + 'Cb'] = values[1]; this[type + 'RecoveryCb'] = values[2]; @@ -489,10 +489,10 @@ } else if (num !== '*' && ('number' !== typeof num || !isFinite(num) || num < 1)) { + throw new TypeError('SizeManager.init: ' + name + 'Players must be a finite number greater ' + - 'than 1 or one of the wildcards: *,@. Found: ' + - num); + 'than 1 or a wildcard (*,@). Found: ' + num); } if (!cb) { From be94b152330ba8d549adea1ecec6fd3838631719 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 20 May 2021 11:50:19 +0200 Subject: [PATCH 09/51] Game takes care of scrolling up when a new frame is not loaded --- build/nodegame-full.js | 212 +++++++++++++++++++++++++++++------------ index.browser.js | 2 +- lib/core/Game.js | 12 ++- 3 files changed, 160 insertions(+), 66 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 92c66da9..0fe8998b 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -10338,7 +10338,7 @@ if (!Array.prototype.indexOf) { node.support = JSUS.compatibility(); // Auto-Generated. - node.version = '6.1.0'; + node.version = '6.2.0'; })(window); @@ -15693,7 +15693,7 @@ if (!Array.prototype.indexOf) { * @param {number|array} The value/s for the handler */ SizeManager.prototype.setHandler = function(type, values) { - values = checkMinMaxExactParams('min', values, this.node); + values = checkMinMaxExactParams(type, values, this.node); this[type + 'Threshold'] = values[0]; this[type + 'Cb'] = values[1]; this[type + 'RecoveryCb'] = values[2]; @@ -15773,10 +15773,10 @@ if (!Array.prototype.indexOf) { } else if (num !== '*' && ('number' !== typeof num || !isFinite(num) || num < 1)) { + throw new TypeError('SizeManager.init: ' + name + 'Players must be a finite number greater ' + - 'than 1 or one of the wildcards: *,@. Found: ' + - num); + 'than 1 or a wildcard (*,@). Found: ' + num); } if (!cb) { @@ -24195,7 +24195,7 @@ if (!Array.prototype.indexOf) { /** * # Game - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Handles the flow of the game @@ -25471,13 +25471,19 @@ if (!Array.prototype.indexOf) { else { // Duplicated as below. this.execCallback(cb); - if (w) w.adjustFrameHeight(0, 120); + if (w) { + w.adjustFrameHeight(0, 120); + if (frame.scrollUp !== false) window.scrollTo(0,0); + } } } else { // Duplicated as above. this.execCallback(cb); - if (w) w.adjustFrameHeight(0, 120); + if (w) { + w.adjustFrameHeight(0, 120); + if (frame.scrollUp !== false) window.scrollTo(0, 0); + } } }; @@ -42379,7 +42385,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - Chat.version = '1.3.0'; + Chat.version = '1.5.0'; Chat.description = 'Offers a uni-/bi-directional communication interface ' + 'between players, or between players and the server.'; @@ -42596,7 +42602,10 @@ if (!Array.prototype.indexOf) { /** * ### Chat.preprocessMsg * - * A function that process the msg before being displayed. + * A function that process the msg before being displayed + * + * It does not preprocess the initial message + * and "is typing" notifications. * * Example: * @@ -42885,6 +42894,21 @@ if (!Array.prototype.indexOf) { return c; }; + /** + * ### Chat.writeMsg + * + * It calls preprocess and renders a msg from data + * + * If msg is a function it executes it to render it. + * + * @param {object} data The content of the message + * @param {string} code A value indicating the the type of msg. Available: + * 'incoming', 'outgoing', and anything else. + * + * @return {string} msg The rendered msg + * + * @see Chat.chatDiv + */ Chat.prototype.renderMsg = function(data, code) { var msg; if ('function' === typeof this.preprocessMsg) { @@ -43131,7 +43155,9 @@ if (!Array.prototype.indexOf) { this.writeMsg('outgoing', opts); // Make sure the cursor goes back to top. - setTimeout(function() { that.textarea.value = ''; }); + if (that.textarea) { + setTimeout(function() { that.textarea.value = ''; }); + } } // Clear any typing timeout. @@ -46061,6 +46087,14 @@ if (!Array.prototype.indexOf) { * An object containing the list of disabled values */ this.disabledChoices = {}; + + + /** + * ### ChoiceTable.sameWidthCells + * + * If TRUE, cells have same width regardless of content + */ + this.sameWidthCells = true; } // ## ChoiceTable methods @@ -46425,6 +46459,10 @@ if (!Array.prototype.indexOf) { })(); } } + + if ('undefined' === typeof opts.sameWidthCells) { + this.sameWidthCells = !!opts.sameWidthCells; + } }; /** @@ -46700,10 +46738,19 @@ if (!Array.prototype.indexOf) { * @see ChoiceTable.choicesCells */ ChoiceTable.prototype.renderChoice = function(choice, idx) { - var td, shortValue, value; + var td, shortValue, value, width; td = document.createElement('td'); if (this.tabbable) J.makeTabbable(td); + // Forces equal width. + if (this.sameWidthCells) { + debugger + width = this.left ? 70 : 100; + if (this.right) width = width - 30; + width = width / (this.choicesSetSize || this.choices.length); + td.style.width = width.toFixed(2) + '%'; + } + // Use custom renderer. if (this.renderer) { value = this.renderer(td, choice, idx); @@ -49361,7 +49408,7 @@ if (!Array.prototype.indexOf) { /** * # CustomInput - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a configurable input form with validation @@ -49376,7 +49423,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - CustomInput.version = '0.11.0'; + CustomInput.version = '0.12.0'; CustomInput.description = 'Creates a configurable input form'; CustomInput.title = false; @@ -49541,7 +49588,7 @@ if (!Array.prototype.indexOf) { res = '(Must be before ' + w.params.max + ')'; } } - return w.requiredChoice ? ((res || '') + ' *') : (res || false); + return w.required ? ((res || '') + ' *') : (res || false); }, numericErr: function(w) { var str, p; @@ -49756,9 +49803,20 @@ if (!Array.prototype.indexOf) { * If TRUE, the input form cannot be left empty * * Default: TRUE + * + * @deprecated Use CustomInput.required */ this.requiredChoice = null; + /** + * ### CustomInput.required + * + * If TRUE, the input form cannot be left empty + * + * Default: TRUE + */ + this.required = null; + /** * ### CustomInput.timeBegin * @@ -49843,7 +49901,22 @@ if (!Array.prototype.indexOf) { } this.orientation = tmp; - this.requiredChoice = !!opts.requiredChoice; + // Backward compatible checks. + // Option required will be used in the future. + if ('undefined' !== typeof opts.required) { + this.required = this.requiredChoice = !!opts.required; + } + if ('undefined' !== typeof opts.requiredChoice) { + if (!!this.required !== !!opts.requiredChoice) { + throw new TypeError('CustomInput.init: required and ' + + 'requiredChoice are incompatible. Option ' + + 'requiredChoice will be deprecated.'); + } + this.required = this.requiredChoice = !!opts.required; + } + if ('undefined' === typeof this.required) { + this.required = this.requiredChoice = !!opts.required; + } if (opts.userValidation) { if ('function' !== typeof opts.userValidation) { @@ -50300,7 +50373,7 @@ if (!Array.prototype.indexOf) { } this.params.minItems = tmp; } - else if (this.requiredChoice) { + else if (this.required) { this.params.minItems = 1; } if ('undefined' !== typeof opts.maxItems) { @@ -50424,7 +50497,7 @@ if (!Array.prototype.indexOf) { var res; res = { value: value }; if (value.trim() === '') { - if (that.requiredChoice) res.err = that.getText('emptyErr'); + if (that.required) res.err = that.getText('emptyErr'); } else if (tmp) { res = tmp(value); @@ -50543,7 +50616,7 @@ if (!Array.prototype.indexOf) { 'undefined. Found: ' + opts.hint); } this.hint = opts.hint; - if (this.requiredChoice) this.hint += ' *'; + if (this.required) this.hint += ' *'; } else { this.hint = this.getText('autoHint'); @@ -53325,7 +53398,7 @@ if (!Array.prototype.indexOf) { /** * # EmailForm - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Displays a form to input email @@ -53340,50 +53413,47 @@ if (!Array.prototype.indexOf) { // ## Meta-data - EmailForm.version = '0.12.0'; + EmailForm.version = '0.13.0'; EmailForm.description = 'Displays a configurable email form.'; - EmailForm.title = 'Email'; + EmailForm.title = false; EmailForm.className = 'emailform'; - EmailForm.texts.label = 'Enter your email:'; - EmailForm.texts.errString = 'Not a valid email address, ' + - 'please correct it and submit it again.'; - - // ## Dependencies - - EmailForm.dependencies = { JSUS: {} }; + EmailForm.texts = { + label: 'Enter your email:', + errString: 'Not a valid email address, ' + + 'please correct it and submit it again.', + sent: 'Sent!' + }; /** * ## EmailForm constructor * - * `EmailForm` sends a feedback message to the server - * * @param {object} options configuration option */ - function EmailForm(options) { + function EmailForm(opts) { /** * ### EmailForm.onsubmit * * Options passed to `getValues` when the submit button is pressed * - * @see Feedback.getValues + * @see EmailForm.getValues */ - if (!options.onsubmit) { + if (!opts.onsubmit) { this.onsubmit = { emailOnly: true, send: true, updateUI: true }; } - else if ('object' === typeof options.onsubmit) { - this.onsubmit = options.onsubmit; + else if ('object' === typeof opts.onsubmit) { + this.onsubmit = opts.onsubmit; } else { - throw new TypeError('EmailForm constructor: options.onsubmit ' + - 'must be string or object. Found: ' + - options.onsubmit); + throw new TypeError('EmailForm constructor: opts.onsubmit ' + + 'must be object or undefined. Found: ' + + opts.onsubmit); } /** @@ -53395,7 +53465,7 @@ if (!Array.prototype.indexOf) { * * @see EmailForm.createForm */ - this._email = options.email || null; + this._email = opts.email || null; /** * ### EmailForm.attempts @@ -53439,7 +53509,17 @@ if (!Array.prototype.indexOf) { * * Default: FALSE */ - this.setMsg = !!options.setMsg || false; + this.setMsg = !!opts.setMsg || false; + + /** + * ### EmailForm.showSubmitBtn + * + * If TRUE, a set message is sent instead of a data msg + * + * Default: FALSE + */ + this.showSubmitBtn = 'undefined' === typeof opts.showSubmitBtn ? + true : !!opts.showSubmitBtn; } // ## EmailForm methods @@ -53461,31 +53541,34 @@ if (!Array.prototype.indexOf) { inputElement.setAttribute('placeholder', 'Email'); inputElement.className = 'emailform-input form-control'; - buttonElement = document.createElement('input'); - buttonElement.setAttribute('type', 'submit'); - buttonElement.setAttribute('value', 'Submit email'); - buttonElement.className = 'btn btn-lg btn-primary ' + - 'emailform-submit'; - formElement.appendChild(labelElement); formElement.appendChild(inputElement); - formElement.appendChild(buttonElement); - - // Add listeners on input form. - J.addEvent(formElement, 'submit', function(event) { - event.preventDefault(); - that.getValues(that.onsubmit); - }, true); - J.addEvent(formElement, 'input', function() { - if (!that.timeInput) that.timeInput = J.now(); - if (that.isHighlighted()) that.unhighlight(); - }, true); - // Store references. this.formElement = formElement; this.inputElement = inputElement; - this.buttonElement = buttonElement; + + if (this.showSubmitBtn) { + buttonElement = document.createElement('input'); + buttonElement.setAttribute('type', 'submit'); + buttonElement.setAttribute('value', 'Submit email'); + buttonElement.className = 'btn btn-lg btn-primary ' + + 'emailform-submit'; + formElement.appendChild(buttonElement); + + // Add listeners on input form. + J.addEvent(formElement, 'submit', function(event) { + event.preventDefault(); + that.getValues(that.onsubmit); + }, true); + J.addEvent(formElement, 'input', function() { + if (!that.timeInput) that.timeInput = J.now(); + if (that.isHighlighted()) that.unhighlight(); + }, true); + + // Store reference. + this.buttonElement = buttonElement; + } // If a value was previously set, insert it in the form. if (this._email) this.formElement.value = this._email; @@ -53517,7 +53600,7 @@ if (!Array.prototype.indexOf) { if (this.inputElement) this.inputElement.disabled = true; if (this.buttonElement) { this.buttonElement.disabled = true; - this.buttonElement.value = 'Sent!'; + this.buttonElement.value = this.getText('sent'); } } else { @@ -53620,9 +53703,10 @@ if (!Array.prototype.indexOf) { email: email, attempts: this.attempts, }; - if (opts.markAttempt) email.isCorrect = res; } + if (opts.markAttempt) email.isCorrect = res; + if (res === false) { if (opts.updateUI || opts.highlight) this.highlight(); this.timeInput = null; @@ -59365,7 +59449,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - VisualStage.version = '0.9.0'; + VisualStage.version = '0.10.0'; VisualStage.description = 'Displays the name of the current, previous and next step of the game.'; @@ -59618,6 +59702,10 @@ if (!Array.prototype.indexOf) { if (this.capitalize) name = capitalize(name); } } + + // If function, executes it. + if ('function' === typeof name) name = name.call(node.game); + if (this.showRounds) { round = getRound(gameStage, curStage, mod); if (round) name += ' ' + round; diff --git a/index.browser.js b/index.browser.js index 63afd6c7..65cfa095 100644 --- a/index.browser.js +++ b/index.browser.js @@ -21,6 +21,6 @@ node.support = JSUS.compatibility(); // Auto-Generated. - node.version = '6.1.0'; + node.version = '6.2.0'; })(window); diff --git a/lib/core/Game.js b/lib/core/Game.js index 30ae0b85..32f16d8b 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1,6 +1,6 @@ /** * # Game - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Handles the flow of the game @@ -1276,13 +1276,19 @@ else { // Duplicated as below. this.execCallback(cb); - if (w) w.adjustFrameHeight(0, 120); + if (w) { + w.adjustFrameHeight(0, 120); + if (frame.scrollUp !== false) window.scrollTo(0,0); + } } } else { // Duplicated as above. this.execCallback(cb); - if (w) w.adjustFrameHeight(0, 120); + if (w) { + w.adjustFrameHeight(0, 120); + if (frame.scrollUp !== false) window.scrollTo(0, 0); + } } }; From 38ad1d2c0cd8d2db996e84660cd70b8b90b82c90 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 26 May 2021 16:10:57 +0200 Subject: [PATCH 10/51] fixed scrollup bug --- lib/core/Game.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/Game.js b/lib/core/Game.js index 32f16d8b..31c30d2a 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1287,7 +1287,7 @@ this.execCallback(cb); if (w) { w.adjustFrameHeight(0, 120); - if (frame.scrollUp !== false) window.scrollTo(0, 0); + window.scrollTo(0, 0); } } }; From 54682fa46b2b0f1035c4ed997afabbd9f2d3d3a1 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 26 May 2021 16:11:18 +0200 Subject: [PATCH 11/51] size-handler functions correctly receive the player object and not a game msg with player in the data field --- build/nodegame-full.js | 104 ++++++++++++++++++++++++++++------------ lib/core/SizeManager.js | 4 +- 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 0fe8998b..feae99d8 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -25482,7 +25482,7 @@ if (!Array.prototype.indexOf) { this.execCallback(cb); if (w) { w.adjustFrameHeight(0, 120); - if (frame.scrollUp !== false) window.scrollTo(0, 0); + window.scrollTo(0, 0); } } }; @@ -45876,7 +45876,7 @@ if (!Array.prototype.indexOf) { this.rightCell = null; /** - * ### CustomInput.errorBox + * ### ChoiceTable.errorBox * * An HTML element displayed when a validation error occurs */ @@ -46409,19 +46409,6 @@ if (!Array.prototype.indexOf) { this.freeText = 'string' === typeof opts.freeText ? opts.freeText : !!opts.freeText; - // Add the choices. - if ('undefined' !== typeof opts.choices) { - this.setChoices(opts.choices); - } - - // Add the correct choices. - if ('undefined' !== typeof opts.correctChoice) { - if (this.requiredChoice) { - throw new Error('ChoiceTable.init: cannot specify both ' + - 'opts requiredChoice and correctChoice'); - } - this.setCorrectChoice(opts.correctChoice); - } // Add the correct choices. if ('undefined' !== typeof opts.choicesSetSize) { @@ -46440,6 +46427,21 @@ if (!Array.prototype.indexOf) { this.choicesSetSize = opts.choicesSetSize; } + // Add the choices. + if ('undefined' !== typeof opts.choices) { + this.setChoices(opts.choices); + } + + // Add the correct choices. + if ('undefined' !== typeof opts.correctChoice) { + if (this.requiredChoice) { + throw new Error('ChoiceTable.init: cannot specify both ' + + 'opts requiredChoice and correctChoice'); + } + this.setCorrectChoice(opts.correctChoice); + } + + // Add the correct choices. if ('undefined' !== typeof opts.disabledChoices) { if (!J.isArray(opts.disabledChoices)) { @@ -46744,7 +46746,6 @@ if (!Array.prototype.indexOf) { // Forces equal width. if (this.sameWidthCells) { - debugger width = this.left ? 70 : 100; if (this.right) width = width - 30; width = width / (this.choicesSetSize || this.choices.length); @@ -46921,7 +46922,9 @@ if (!Array.prototype.indexOf) { * @see ChoiceTable.errorBox */ ChoiceTable.prototype.setError = function(err) { - this.errorBox.innerHTML = err || ''; + // TODO: the errorBox is added only if .append() is called. + // However, ChoiceTableGroup use the table without calling .append(). + if (this.errorBox) this.errorBox.innerHTML = err || ''; if (err) this.highlight(); else this.unhighlight(); }; @@ -47568,7 +47571,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceTableGroup.version = '1.7.0'; + ChoiceTableGroup.version = '1.8.0'; ChoiceTableGroup.description = 'Groups together and manages sets of ' + 'ChoiceTable widgets.'; @@ -47577,9 +47580,14 @@ if (!Array.prototype.indexOf) { ChoiceTableGroup.separator = '::'; - ChoiceTableGroup.texts.autoHint = function(w) { - if (w.requiredChoice) return '*'; - else return false; + ChoiceTableGroup.texts = { + + autoHint: function(w) { + if (w.requiredChoice) return '*'; + else return false; + }, + + error: 'Selection required.' }; // ## Dependencies @@ -47741,6 +47749,13 @@ if (!Array.prototype.indexOf) { */ this.hint = null; + /** + * ### ChoiceTableGroup.errorBox + * + * An HTML element displayed when a validation error occurs + */ + this.errorBox = null; + /** * ### ChoiceTableGroup.items * @@ -48411,6 +48426,8 @@ if (!Array.prototype.indexOf) { this.bodyDiv.appendChild(this.table); } + this.errorBox = W.append('div', this.bodyDiv, { className: 'errbox' }); + // Creates a free-text textarea, possibly with placeholder text. if (this.freeText) { this.textarea = document.createElement('textarea'); @@ -48579,6 +48596,7 @@ if (!Array.prototype.indexOf) { if (!this.table || this.highlighted !== true) return; this.table.style.border = ''; this.highlighted = false; + this.setError(); this.emit('unhighlighted'); }; @@ -48633,13 +48651,35 @@ if (!Array.prototype.indexOf) { toHighlight = true; } } - if (opts.highlight && toHighlight) this.highlight(); - else if (toReset) this.reset(toReset); + if (opts.highlight && toHighlight) { + this.setError(this.getText('error')); + } + else if (toReset) { + this.reset(toReset); + } opts.reset = toReset; if (this.textarea) obj.freetext = this.textarea.value; return obj; }; + + /** + * ### ChoiceTableGroup.setError + * + * Set the error msg inside the errorBox and call highlight + * + * @param {string} The error msg (can contain HTML) + * + * @see ChoiceTableGroup.highlight + * @see ChoiceTableGroup.errorBox + */ + ChoiceTableGroup.prototype.setError = function(err) { + this.errorBox.innerHTML = err || ''; + if (err) this.highlight(); + else this.unhighlight(); + }; + + /** * ### ChoiceTableGroup.setValues * @@ -56900,7 +56940,8 @@ if (!Array.prototype.indexOf) { }, bomb_sliderHint: - 'Move the slider below to change the number of boxes to open.', + 'Move the slider to choose the number of boxes to open, ' + + 'then click "Open Boxes"', bomb_boxValue: 'Prize per box: ', @@ -57334,12 +57375,6 @@ if (!Array.prototype.indexOf) { that.getText('bomb_mainText', probBomb) }); - // Table. - nRows = Math.ceil(that.totBoxes / that.boxesInRow); - W.add('div', that.bodyDiv, { - innerHTML: makeTable(nRows, that.boxesInRow, that.totBoxes) - }); - // Slider. slider = node.widgets.add('Slider', that.bodyDiv, { min: 0, @@ -57351,6 +57386,7 @@ if (!Array.prototype.indexOf) { displayNoChange: false, type: 'flat', required: true, + panel: false, // texts: { // currentValue: that.getText('sliderValue') // }, @@ -57394,6 +57430,12 @@ if (!Array.prototype.indexOf) { width: '100%' }); + // Table. + nRows = Math.ceil(that.totBoxes / that.boxesInRow); + W.add('div', that.bodyDiv, { + innerHTML: makeTable(nRows, that.boxesInRow, that.totBoxes) + }); + // Info div. infoDiv = W.add('div', that.bodyDiv, { className: 'risk-info', @@ -58342,7 +58384,7 @@ if (!Array.prototype.indexOf) { if (this.mainText) { mainText = this.mainText; } - else if ('undefined' === typeof this.mainText) { + else if (this.mainText !== false) { mainText = this.getText('mainText'); } gauge = node.widgets.get('ChoiceTableGroup', { diff --git a/lib/core/SizeManager.js b/lib/core/SizeManager.js index 99c32c33..4554ca51 100644 --- a/lib/core/SizeManager.js +++ b/lib/core/SizeManager.js @@ -428,10 +428,10 @@ var that; that = this; this.node.events.step.on('in.say.PCONNECT', function(p) { - that.changeHandler('pconnect', p); + that.changeHandler('pconnect', p.data); }, 'plManagerCon'); this.node.events.step.on('in.say.PDISCONNECT', function(p) { - that.changeHandler('pdisconnect', p); + that.changeHandler('pdisconnect', p.data); }, 'plManagerDis'); }; From 088ee2aa59c2393d8097357df79868347c4cb90a Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 26 May 2021 16:12:06 +0200 Subject: [PATCH 12/51] built --- build/nodegame-full.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index feae99d8..ca2b3483 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -15712,10 +15712,10 @@ if (!Array.prototype.indexOf) { var that; that = this; this.node.events.step.on('in.say.PCONNECT', function(p) { - that.changeHandler('pconnect', p); + that.changeHandler('pconnect', p.data); }, 'plManagerCon'); this.node.events.step.on('in.say.PDISCONNECT', function(p) { - that.changeHandler('pdisconnect', p); + that.changeHandler('pdisconnect', p.data); }, 'plManagerDis'); }; From 5d4184de23b56f13c83b7596e89898161b5541da Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 27 May 2021 15:52:04 +0200 Subject: [PATCH 13/51] CSV headers all by default (before only for flatten) --- lib/core/GameDB.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/core/GameDB.js b/lib/core/GameDB.js index 81c95a9e..fb944ac1 100644 --- a/lib/core/GameDB.js +++ b/lib/core/GameDB.js @@ -162,9 +162,14 @@ } } + if ('undefined' === typeof opts.header && + 'undefined' === typeof opts.headers) { + + opts.header = 'all'; + } + // Flatten. if (opts.flatten) { - if ('undefined' === typeof opts.headers) opts.headers = 'all'; opts.preprocess = function(item, current) { var s; s = item.stage.stage + '.' + item.stage.step + From c8f2163b71cf131479523d41e2af6e3f25c096de Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Tue, 1 Jun 2021 23:52:58 +0200 Subject: [PATCH 14/51] minor pushmanager --- lib/core/PushManager.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/core/PushManager.js b/lib/core/PushManager.js index 99c863e6..589258c9 100644 --- a/lib/core/PushManager.js +++ b/lib/core/PushManager.js @@ -3,7 +3,7 @@ * * Push players to advance to next step, otherwise disconnects them. * - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed */ (function(exports, parent) { @@ -14,7 +14,6 @@ exports.PushManager = PushManager; var GameStage = parent.GameStage; - var J = parent.JSUS; var DONE = parent.constants.stageLevels.DONE; var PUSH_STEP = parent.constants.gamecommands.push_step; @@ -125,7 +124,7 @@ conf = {}; } else if ('object' !== typeof conf) { - throw new TypError('PushManager.startTimer: conf must be ' + + throw new TypeError('PushManager.startTimer: conf must be ' + 'object, TRUE, or undefined. Found: ' + conf); } @@ -268,7 +267,6 @@ * wait before checking again the stage of a client. Default 0. */ function checkIfPushWorked(node, p, stage, milliseconds) { - var stage; node.info('push-manager: received reply from ' + p.id); From 04913b64c350b0be8f426a4632b8fb2127108b0c Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Tue, 1 Jun 2021 23:55:10 +0200 Subject: [PATCH 15/51] stager_require --- lib/stager/stager_require.js | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 lib/stager/stager_require.js diff --git a/lib/stager/stager_require.js b/lib/stager/stager_require.js new file mode 100644 index 00000000..0ff67c14 --- /dev/null +++ b/lib/stager/stager_require.js @@ -0,0 +1,52 @@ +/** + * # Stager blocks operations + * Copyright(c) 2021 Stefano Balietti + * MIT Licensed + */ +(function(exports, node) { + + const Stager = node.Stager; + const J = node.JSUS; + const path = require('path'); + + // Stager.prototype.require = function(...paths) { + // + // let myPath = path.join(...paths); + // let cb = require(myPath); + // + // let s = this.__shared; + // + // return cb(s.treatmentName, s.settings, this, s.setup, + // s.gameRoom, s.node, this.shared || {}); + // }; + + Stager.prototype.require = function(...paths) { + + let myPath = path.join(...paths); + let cb = require(myPath); + + return cb(this.shared); + }; + + // Stager.prototype.share = function(shared) { + // if (!this.shared) { + // let stager = this; + // this.shared = [ stager ] ; + // } + // if (Array.isArray(shared)) this.shared = [...this.shared, ...shared ]; + // else this.shared.push(shared); + // }; + + // Stager.prototype.share = function(obj) { + // if (!this.shared) this.shared = {}; + // J.mixin(this.shared, obj); + // }; + + Stager.prototype.share = function(obj) { + if (!this.shared) this.shared = { stager: this, J: J }; + J.mixin(this.shared, obj); + }; +})( + 'undefined' != typeof node ? node : module.exports, + 'undefined' != typeof node ? node : module.parent.exports +); From 26742cfbf0f7cdb13bd0573829fd6a652034cc15 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Tue, 1 Jun 2021 23:56:18 +0200 Subject: [PATCH 16/51] stager_require-index --- build/nodegame-full.js | 200 ++++++++++++++++++++++++++--------------- index.js | 3 +- listeners/aliases.js | 12 +++ 3 files changed, 141 insertions(+), 74 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index ca2b3483..d1705588 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -14911,7 +14911,7 @@ if (!Array.prototype.indexOf) { * * Push players to advance to next step, otherwise disconnects them. * - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed */ (function(exports, parent) { @@ -14922,7 +14922,6 @@ if (!Array.prototype.indexOf) { exports.PushManager = PushManager; var GameStage = parent.GameStage; - var J = parent.JSUS; var DONE = parent.constants.stageLevels.DONE; var PUSH_STEP = parent.constants.gamecommands.push_step; @@ -15033,7 +15032,7 @@ if (!Array.prototype.indexOf) { conf = {}; } else if ('object' !== typeof conf) { - throw new TypError('PushManager.startTimer: conf must be ' + + throw new TypeError('PushManager.startTimer: conf must be ' + 'object, TRUE, or undefined. Found: ' + conf); } @@ -15176,7 +15175,6 @@ if (!Array.prototype.indexOf) { * wait before checking again the stage of a client. Default 0. */ function checkIfPushWorked(node, p, stage, milliseconds) { - var stage; node.info('push-manager: received reply from ' + p.id); @@ -24172,9 +24170,14 @@ if (!Array.prototype.indexOf) { } } + if ('undefined' === typeof opts.header && + 'undefined' === typeof opts.headers) { + + opts.header = 'all'; + } + // Flatten. if (opts.flatten) { - if ('undefined' === typeof opts.headers) opts.headers = 'all'; opts.preprocess = function(item, current) { var s; s = item.stage.stage + '.' + item.stage.step + @@ -33211,6 +33214,18 @@ if (!Array.prototype.indexOf) { }; }); + // ### node.on.data + this.alias('done', ['in.say.DATA', 'in.set.DATA'], function(text, cb) { + if ('string' !== typeof text || text === '') { + throw new TypeError('node.on.data: text must be a non-empty ' + + 'string. Found: ' + text); + } + return function(msg) { + if (msg.text === text) cb.call(that.game, msg); + else return false; + }; + }); + // ### node.on.stage this.alias('stage', 'STEPPING', function(cb) { return function(curStep, newStep) { @@ -33608,7 +33623,7 @@ if (!Array.prototype.indexOf) { var DOM; var constants, windowLevels, screenLevels; - var CB_EXECUTED, WIN_LOADING, lockedUpdate; + var CB_EXECUTED, WIN_LOADING; if (!J) throw new Error('GameWindow: JSUS not found'); DOM = J.require('DOM'); @@ -33622,9 +33637,6 @@ if (!Array.prototype.indexOf) { WIN_LOADING = windowLevels.LOADING; - // Allows just one update at the time to the counter of loading frames. - lockedUpdate = false; - GameWindow.prototype = DOM; GameWindow.prototype.constructor = GameWindow; @@ -33648,10 +33660,7 @@ if (!Array.prototype.indexOf) { var iframeWin; iframeWin = iframe.contentWindow; - function completed(event) { - var iframeDoc; - iframeDoc = J.getIFrameDocument(iframe); - + function completed() { // Detaching the function to avoid double execution. iframe.removeEventListener('load', completed, false); iframeWin.removeEventListener('load', completed, false); @@ -34439,16 +34448,22 @@ if (!Array.prototype.indexOf) { * * Appends a configurable div element at to "top" of the page * - * @param {object} opts Optional. Configuration options: TODO + * @param {object} opts Optional. Configuration options: * - * - toggleBtn - * - toggleBtnLabel - * - toggleBtnRoot: - * - force: destroys current Info Panel + * - root: The HTML element (or its id) under which the Info Panel + * will be appended. Default: above the main frame, or below the + * the header, or under document.body. + * - innerHTML: the content of the Info Panel. + * - force: It destroys current frame, if existing. + * - toggleBtn: If TRUE, it creates a button to toggle the Info Panel. + * Default: TRUE. + * - toggleBtnRoot: the HTML element (or its id) under which the button + * to toggle the Info Panel will be appended. Default: the header. + * - toggleBtnLabel: The text on the button to toggle the Info Panel. + * Default: 'Info'. * - * @param {boolean} force Optional. Will create the frame even if an - * existing one is found. Deprecated, use force flag in options. - * Default: FALSE + * @param {boolean} force Optional. Deprecated, use force flag in + * options. Default: FALSE * * @return {InfoPanel} A reference to the InfoPanel object * @@ -34474,11 +34489,7 @@ if (!Array.prototype.indexOf) { opts.toggleBtn = false; } } - // if (!force) { - // throw new Error('GameWindow.generateInfoPanel: info panel is ' + - // 'already existing. Use force to regenerate.'); - // } - + node.warn('W.generateInfoPanel: Info Panel already existing.') } else { this.infoPanel = new node.InfoPanel(opts); @@ -34488,6 +34499,7 @@ if (!Array.prototype.indexOf) { root = opts.root; if (root) { + if ('string' === typeof root) root = W.gid(root); if (!J.isElement(root)) { throw new Error('GameWindow.generateInfoPanel: root must be ' + 'undefined or HTMLElement. Found: ' + root); @@ -35955,8 +35967,6 @@ if (!Array.prototype.indexOf) { * @param {GameWindow} that A reference to the GameWindow instance * @param {number} update The number to add to the counter * - * @see GameWindow.lockedUpdate - * * @api private */ function updateAreLoading(that, update) { @@ -36497,7 +36507,6 @@ if (!Array.prototype.indexOf) { countdown); } this.setScreenLevel('LOCKING'); - text = text || 'Screen locked. Please wait...'; this.waitScreen.lock(text, countdown); this.setScreenLevel('LOCKED'); }; @@ -36668,7 +36677,7 @@ if (!Array.prototype.indexOf) { /** * # WaitScreen - * Copyright(c) 2018 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Overlays the screen, disables inputs, and displays a message/timer @@ -36684,8 +36693,8 @@ if (!Array.prototype.indexOf) { // ## Meta-data - WaitScreen.version = '0.9.0'; - WaitScreen.description = 'Shows a standard waiting screen'; + WaitScreen.version = '0.10.0'; + WaitScreen.description = 'Shows a waiting screen'; // ## Helper functions @@ -36792,10 +36801,10 @@ if (!Array.prototype.indexOf) { * * Instantiates a new WaitScreen object * - * @param {object} options Optional. Configuration options + * @param {object} opts Optional. Configuration options */ - function WaitScreen(options) { - options = options || {}; + function WaitScreen(opts) { + opts = opts || {}; /** * ### WaitScreen.id @@ -36804,7 +36813,7 @@ if (!Array.prototype.indexOf) { * * @see WaitScreen.waitingDiv */ - this.id = options.id || 'ng_waitScreen'; + this.id = opts.id || 'ng_waitScreen'; /** * ### WaitScreen.root @@ -36813,7 +36822,7 @@ if (!Array.prototype.indexOf) { * * @see WaitScreen.waitingDiv */ - this.root = options.root || null; + this.root = opts.root || null; /** * ### WaitScreen.waitingDiv @@ -36871,23 +36880,60 @@ if (!Array.prototype.indexOf) { * ### WaitScreen.countdown * * Countdown of max waiting time - * - * @see WaitScreen.countdown */ this.countdown = null; + /** + * ### WaitScreen.displayCountdown + * + * If FALSE, countdown is never displayed by lock + * + * @see WaitScreen.lock + */ + this.displayCountdown = + 'undefined' !== typeof opts.displayCountdown ? + !!opts.displayCountdown : true; + /** * ### WaitScreen.text * * Default texts for default events */ this.defaultTexts = { - waiting: options.waitingText || + + // Default text for locked screen. + locked: opts.lockedText || + 'Screen locked. Please wait...', + + // When player is DONE and waiting for others. + waiting: opts.waitingText || 'Waiting for other players to be done...', - stepping: options.steppingText || + + // When entering a new step after DONE (displayed quickly usually). + stepping: opts.steppingText || 'Initializing game step, will be ready soon...', - paused: options.pausedText || - 'Game is paused. Please wait.' + + // Game paused. + paused: opts.pausedText || + 'Game is paused. Please wait.', + + // Countdown text displayed under waiting text. + countdown: opts.countdownResumingText || + '
Do not refresh the page!
Maximum Waiting Time: ', + + // Displayed after resuming from waiting. + countdownResuming: opts.countdownResumingText || + 'Resuming soon...', + + // Formats the countdown in minutes and seconds. + formatCountdown: function(time) { + var out; + out = ''; + time = J.parseMilliseconds(time); + if (time[2]) out += time[2] + ' min '; + if (time[3]) out += time[3] + ' sec'; + return out || 0; + } }; /** @@ -36954,9 +37000,11 @@ if (!Array.prototype.indexOf) { * @see WaitScren.updateText */ WaitScreen.prototype.lock = function(text, countdown) { - var frameDoc; + var frameDoc, t; + t = this.defaultTexts; + if ('undefined' === typeof text) text = t.locked; if ('undefined' === typeof document.getElementsByTagName) { - node.warn('WaitScreen.lock: cannot lock inputs.'); + node.warn('WaitScreen.lock: cannot lock inputs'); } // Disables all input forms in the page. lockUnlockedInputs(document); @@ -36978,20 +37026,20 @@ if (!Array.prototype.indexOf) { } this.contentDiv.innerHTML = text; - if (countdown) { + if (this.displayCountdown && countdown) { + if (!this.countdownDiv) { this.countdownDiv = W.add('div', this.waitingDiv, 'ng_waitscreen-countdown-div'); - this.countdownDiv.innerHTML = '
Do not refresh the page!' + - '
Maximum Waiting Time: '; + this.countdownDiv.innerHTML = t.countdown; this.countdownSpan = W.add('span', this.countdownDiv, 'ng_waitscreen-countdown-span'); } this.countdown = countdown; - this.countdownSpan.innerHTML = formatCountdown(countdown); + this.countdownSpan.innerHTML = t.formatCountdown(countdown); this.countdownDiv.style.display = ''; this.countdownInterval = setInterval(function() { @@ -37006,10 +37054,10 @@ if (!Array.prototype.indexOf) { if (w.countdown < 0) { clearInterval(w.countdownInterval); w.countdownDiv.style.display = 'none'; - w.contentDiv.innerHTML = 'Resuming soon...'; + w.contentDiv.innerHTML = t.countdownResuming; } else { - w.countdownSpan.innerHTML = formatCountdown(w.countdown); + w.countdownSpan.innerHTML = t.formatCountdown(w.countdown); } }, 1000); } @@ -37091,19 +37139,6 @@ if (!Array.prototype.indexOf) { this.disable(); }; - - // ## Helper functions. - - function formatCountdown(time) { - var out; - out = ''; - time = J.parseMilliseconds(time); - if (time[2]) out += time[2] + ' min '; - if (time[3]) out += time[3] + ' sec'; - return out || 0; - } - - })( ('undefined' !== typeof node) ? node : module.parent.exports.node, ('undefined' !== typeof window) ? window : module.parent.exports.window @@ -37111,7 +37146,7 @@ if (!Array.prototype.indexOf) { /** * # InfoPanel - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Adds a configurable extra panel at the top of the screen @@ -37420,7 +37455,7 @@ if (!Array.prototype.indexOf) { * @see InfoPanel.toggleBtn * @see InfoPanel.toggle */ - InfoPanel.prototype.createToggleBtn = + InfoPanel.prototype.createToggleBtn = InfoPanel.prototype.createToggleButton = function(label) { var that, button; @@ -37447,7 +37482,7 @@ if (!Array.prototype.indexOf) { })( ('undefined' !== typeof node) ? node : module.parent.exports.node, ('undefined' !== typeof window) ? window : module.parent.exports.window -);; +); /** * # selector @@ -42015,17 +42050,22 @@ if (!Array.prototype.indexOf) { // Locks the back button in case of a timeout. node.events.game.on('PLAYING', function() { var prop, step; - step = node.game.getPreviousStep(1, that.stepOptions); - // It might be enabled already, but we do it again. - if (step) that.enable(); + // Check options. + step = node.game.getPreviousStep(1, that.stepOptions); prop = node.game.getProperty('backbutton'); + if (!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 ('string' === typeof prop) that.button.value = prop; else if (prop && prop.text) that.button.value = prop.text; }); @@ -45504,6 +45544,9 @@ if (!Array.prototype.indexOf) { } obj._scrolledIntoView = true; obj.isCorrect = false; + // Adjust frame heights because of error msgs. + // TODO: error msgs should not change the height. + W.adjustFrameHeight(); } // if (obj.missValues.length) obj.isCorrect = false; if (this.textarea) obj.freetext = this.textarea.value; @@ -53958,6 +54001,8 @@ if (!Array.prototype.indexOf) { * The currency displayed after totalWin * * Default: 'USD' + * + * // TODO: deprecate and rename to currency. */ this.totalWinCurrency = 'USD'; @@ -54270,13 +54315,20 @@ if (!Array.prototype.indexOf) { } } + preWin = ''; + if ('undefined' !== typeof data.basePay) { + preWin = data.basePay + ' + ' + data.bonus; + } + if (data.partials) { if (!J.isArray(data.partials)) { node.err('EndScreen error, invalid partials win: ' + data.partials); } else { - preWin = data.partials.join(' + '); + // If there is a basePay we already have a preWin. + if (preWin !== '') preWin += ' + '; + preWin += data.partials.join(' + '); } } @@ -54304,10 +54356,12 @@ if (!Array.prototype.indexOf) { err = true; } } - if (!err) totalWin = preWin + ' = ' + totalWin; } - if (!err) totalWin += ' ' + this.totalWinCurrency; + if (!err) { + totalWin = preWin + ' = ' + totalWin; + totalWin += ' ' + this.totalWinCurrency; + } } exitCode = data.exit; diff --git a/index.js b/index.js index 6a04c5e4..c728bef6 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ /** * # nodegame-client build file - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Builds the different components together in one file for the browser @@ -43,6 +43,7 @@ require('./lib/stager/stager_extends.js'); require('./lib/stager/stager_blocks.js'); require('./lib/stager/stager_extract_info.js'); + require('./lib/stager/stager_require.js'); // Core. exports.GameStage = require('./lib/core/GameStage').GameStage; diff --git a/listeners/aliases.js b/listeners/aliases.js index a1dc1cad..1cd9fccd 100644 --- a/listeners/aliases.js +++ b/listeners/aliases.js @@ -47,6 +47,18 @@ }; }); + // ### node.on.data + this.alias('done', ['in.say.DATA', 'in.set.DATA'], function(text, cb) { + if ('string' !== typeof text || text === '') { + throw new TypeError('node.on.data: text must be a non-empty ' + + 'string. Found: ' + text); + } + return function(msg) { + if (msg.text === text) cb.call(that.game, msg); + else return false; + }; + }); + // ### node.on.stage this.alias('stage', 'STEPPING', function(cb) { return function(curStep, newStep) { From b6f1a0dee75b5b9bed9c4ae1f05af370773ad293 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 4 Jun 2021 14:51:59 +0200 Subject: [PATCH 17/51] added treatmentname and default name for node.game.memory is memory --- lib/core/GameDB.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/core/GameDB.js b/lib/core/GameDB.js index fb944ac1..1f1078d7 100644 --- a/lib/core/GameDB.js +++ b/lib/core/GameDB.js @@ -43,7 +43,7 @@ var that; that = this; options = options || {}; - options.name = options.name || 'gamedb'; + options.name = options.name || 'memory'; if (!options.update) options.update = {}; @@ -105,6 +105,8 @@ o.session = this.node.nodename; + o.treatment = this.node.game.settings.treatmentName; + this.insert(o); }; From 29ca834daaae3aa3b75955ad9c06bca0f83856c9 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 9 Jun 2021 15:15:00 +0200 Subject: [PATCH 18/51] chaining in Timer --- lib/core/Timer.js | 85 +++++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/lib/core/Timer.js b/lib/core/Timer.js index 6ea27491..a6a8bedf 100644 --- a/lib/core/Timer.js +++ b/lib/core/Timer.js @@ -1,6 +1,6 @@ /** * # Timer - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Timing-related utility functions @@ -454,14 +454,11 @@ * @see GameTimer */ Timer.prototype.setTimeout = function(timeup, milliseconds, validity) { - var t; - t = this.createTimer({ + return this.createTimer({ milliseconds: milliseconds || 1, timeup: timeup, validity: validity - }); - t.start(); - return t; + }).start(); }; /** @@ -1178,6 +1175,8 @@ * * @param {object} options Optional. Configuration object * + * @return {GameTimer} The game timer instance for chaining + * * @see GameTimer.addHook */ GameTimer.prototype.init = function(options) { @@ -1266,6 +1265,8 @@ if (checkInitialized(this) === null) { this.status = GameTimer.INITIALIZED; } + + return this; }; @@ -1278,6 +1279,8 @@ * otherwise it is called as a function. * * @param {mixed} h The hook to fire (object, function, or string) + * + * @return {GameTimer} The game timer instance for chaining */ GameTimer.prototype.fire = function(h) { var hook, ctx; @@ -1298,6 +1301,8 @@ throw new TypeError('GameTimer.fire: h must be function, string ' + 'or object. Found: ' + h); } + + return this; }; /** @@ -1312,6 +1317,8 @@ * When the timer expires the timeup event is fired, and the * timer is stopped * + * @return {GameTimer} The game timer instance for chaining + * * @see GameTimer.status * @see GameTimer.timeup * @see GameTimer.fire @@ -1333,7 +1340,7 @@ if (this.startPaused) { this.pause(); - return; + return this; } // Remember time of start (used by this.pause to compute remaining time) @@ -1345,7 +1352,7 @@ this.options.milliseconds <= 0) { this.doTimeup(); - return; + return this; } this.updateRemaining = this.update; @@ -1356,6 +1363,8 @@ this.timerId = setInterval(function() { updateCallback(that); }, this.update); + + return this; }; /** @@ -1366,14 +1375,14 @@ * The first parameter can be a string, a function, or an object * containing an hook property. * - * @params {string|function|object} hook The hook (string or function), + * @param {string|function|object} hook The hook (string or function), * or an object containing a `hook` property (others: `ctx` and `name`) - * @params {object} ctx The context wherein the hook is called. + * @param {object} ctx The context wherein the hook is called. * Default: node.game - * @params {string} name The name of the hook. Default: a random name + * @param {string} name The name of the hook. Default: a random name * starting with 'timerHook' * - * @returns {string} The name of the hook + * @return {string} The name of the hook */ GameTimer.prototype.addHook = function(hook, ctx, name) { checkDestroyed(this, 'addHook'); @@ -1396,10 +1405,11 @@ } this.hookNames[name] = true; this.hooks.push({hook: hook, ctx: ctx, name: name}); + return name; }; - /* + /** * ### GameTimer.removeHook * * Removes a hook by its name @@ -1429,6 +1439,8 @@ * * If the timer was running, clear the interval and sets the * status property to `GameTimer.PAUSED`. + * + * @return {GameTimer} The game timer instance for chaining */ GameTimer.prototype.pause = function() { var timestamp; @@ -1454,7 +1466,7 @@ } else if (this.status === GameTimer.STOPPED) { // If the timer was explicitly stopped, we ignore the pause: - return; + return this; } else if (!this.isPaused()) { // pause() was called before start(); remember it: @@ -1463,6 +1475,8 @@ else { throw new Error('GameTimer.pause: timer was already paused'); } + + return this; }; /** @@ -1472,6 +1486,8 @@ * * If the timer was paused, restarts it with the current configuration * + * @return {GameTimer} The game timer instance for chaining + * * @see GameTimer.restart */ GameTimer.prototype.resume = function() { @@ -1481,7 +1497,7 @@ // Don't start if the initialization is incomplete (invalid state): if (this.status === GameTimer.UNINITIALIZED) { this.startPaused = false; - return; + return this; } if (!this.isPaused() && !this.startPaused) { @@ -1507,6 +1523,8 @@ that.status = GameTimer.RUNNING; } }, this.updateRemaining); + + return this; }; /** @@ -1517,6 +1535,8 @@ * If the timer was paused or running, clear the interval, sets the * status property to `GameTimer.STOPPED`, and reset the time passed * and time left properties + * + * @return {GameTimer} The game timer instance for chaining */ GameTimer.prototype.stop = function() { checkDestroyed(this, 'stop'); @@ -1535,6 +1555,8 @@ this.startPaused = null; this.updateRemaining = 0; this.updateStart = 0; + + return this; }; /** @@ -1548,6 +1570,8 @@ * * Does **not** change properties: eventEmitterName, and * stagerSync. + * + * @return {GameTimer} The game timer instance for chaining */ GameTimer.prototype.reset = function() { checkDestroyed(this, 'reset'); @@ -1558,6 +1582,8 @@ this.timeup = 'TIMEUP'; this.hooks = []; this.hookNames = {}; + + return this; }; /** @@ -1570,13 +1596,15 @@ * * @param {object} options Optional. A configuration object * + * @return {GameTimer} The game timer instance for chaining + * * @see GameTimer.init */ GameTimer.prototype.restart = function(options) { checkDestroyed(this, 'restart'); if (!this.isStopped()) this.stop(); this.init(options); - this.start(); + return this.start(); }; /** @@ -1585,6 +1613,8 @@ * Returns whether timer is running * * Running means either LOADING or RUNNING. + * + * @return {boolean} TRUE if timer is running */ GameTimer.prototype.isRunning = function() { checkDestroyed(this, 'isRunning'); @@ -1598,25 +1628,23 @@ * * Stopped means either UNINITIALIZED, INITIALIZED or STOPPED. * + * @return {boolean} TRUE if timer is stopped + * * @see GameTimer.isPaused */ GameTimer.prototype.isStopped = function() { checkDestroyed(this, 'isStopped'); - if (this.status === GameTimer.UNINITIALIZED || + return (this.status === GameTimer.UNINITIALIZED || this.status === GameTimer.INITIALIZED || - this.status === GameTimer.STOPPED) { - - return true; - } - else { - return false; - } + this.status === GameTimer.STOPPED); }; /** * ### GameTimer.isPaused * * Returns whether timer is paused + * + * @return {boolean} TRUE if timer is paused */ GameTimer.prototype.isPaused = function() { checkDestroyed(this, 'isPaused'); @@ -1705,6 +1733,8 @@ * It will call timeup even if the game is paused/stopped, * but not if timeup was already called. * + * @return {GameTimer} The game timer instance for chaning + * * @see GameTimer.isTimeup * @see GameTimer.stop * @see GameTimer.fire @@ -1714,7 +1744,7 @@ if (this.isTimeup()) return; if (!this.isStopped()) this.stop(); this._timeup = true; - this.fire(this.timeup); + return this.fire(this.timeup); }; // TODO: improve. @@ -1863,9 +1893,8 @@ that.doTimeup(); return false; } - else { - return true; - } + + return true; } /** From 23952aeb5ba1eb43cd145b87ff9a9889dff218ba Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 9 Jun 2021 15:16:04 +0200 Subject: [PATCH 19/51] GameDB mods --- lib/core/GameDB.js | 48 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/lib/core/GameDB.js b/lib/core/GameDB.js index 1f1078d7..625c108e 100644 --- a/lib/core/GameDB.js +++ b/lib/core/GameDB.js @@ -1,6 +1,6 @@ /** * # GameDB - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Provides a simple, lightweight NO-SQL database for nodeGame @@ -50,6 +50,19 @@ // Auto build indexes by default. options.update.indexes = true; + // TODO: move on server-side only. + options.defaultCSVHeader = [ + 'session', 'treatment', 'player', 'stage', 'step', 'timestamp', + 'time', 'timeup' + ]; + + // Experimental. TODO. + options.skipCSVKeys = { + isCorrect: true, + id: true, + done: true + }; + NDDB.call(this, options, db); this.comparator('stage', function(o1, o2) { @@ -73,10 +86,24 @@ this.view('done'); - this.on('save', function(options, info) { - if (info.format === 'csv') decorateCSVSaveOptions(that, options); + // TODO: move on server-side only. + this.on('save', function(opts, info) { + if (opts.append) opts.flags = 'a'; + if (info.format === 'csv') decorateCSVSaveOptions(that, opts); }, true); + this.stepView = function(step) { + return this.view(step, function(item) { + if (that.node.game.isStep(step, item.stage)) return true; + }); + }; + + this.stageView = function(stage) { + return this.view(stage, function(item) { + if (that.node.game.isStage(stage, item.stage)) return true; + }); + }; + this.node = this.__shared.node; } @@ -131,8 +158,6 @@ if (!opts.adapter) opts.adapter = {}; - if (opts.append) opts.flags = 'a'; - if (split) { if ('undefined' === typeof opts.adapter.stage) { opts.adapter.stage = function(i) { @@ -164,14 +189,15 @@ } } - if ('undefined' === typeof opts.header && - 'undefined' === typeof opts.headers) { - - opts.header = 'all'; - } - // Flatten. if (opts.flatten) { + + if ('undefined' === typeof opts.header && + 'undefined' === typeof opts.headers) { + + opts.header = that.defaultCSVHeader || 'all'; + } + opts.preprocess = function(item, current) { var s; s = item.stage.stage + '.' + item.stage.step + From cf7947a23b9e8f4db8c5ccb9c56ac7688cf2ea3c Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 9 Jun 2021 15:16:21 +0200 Subject: [PATCH 20/51] alias on done --- listeners/aliases.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/listeners/aliases.js b/listeners/aliases.js index 1cd9fccd..7e024910 100644 --- a/listeners/aliases.js +++ b/listeners/aliases.js @@ -48,14 +48,14 @@ }); // ### node.on.data - this.alias('done', ['in.say.DATA', 'in.set.DATA'], function(text, cb) { - if ('string' !== typeof text || text === '') { - throw new TypeError('node.on.data: text must be a non-empty ' + - 'string. Found: ' + text); - } + this.alias('done', 'in.set.DATA', function(step, cb) { return function(msg) { - if (msg.text === text) cb.call(that.game, msg); - else return false; + if (!msg.data || !msg.data.done || + !that.game.isStep(step, msg.stage)) { + + return false; + } + cb.call(that.game, msg); }; }); From 748a547f5796f2700f850ecffdad55b837553cbd Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 9 Jun 2021 15:16:30 +0200 Subject: [PATCH 21/51] minor --- lib/modules/ssgd.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/modules/ssgd.js b/lib/modules/ssgd.js index c8defc0a..4ea8dccd 100644 --- a/lib/modules/ssgd.js +++ b/lib/modules/ssgd.js @@ -365,7 +365,9 @@ // Time and timeup. if (!o.time) o.time = stepTime; - if (!o.timeup) o.timeup = game.timer.isTimeup(); + if ('undefined' === typeof o.timeup) { + o.timeup = game.timer.isTimeup(); + } // Add role and partner info. if (game.role && !o.role) o.role = game.role; From afed618153f856b465d1a574f29fbddd5172f26f Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 9 Jun 2021 17:56:11 +0200 Subject: [PATCH 22/51] frame appears at once. --- build/nodegame-full.js | 275 +++++++++++++++++++++++++++++------------ listeners/internal.js | 11 ++ 2 files changed, 204 insertions(+), 82 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index d1705588..e50d7e51 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -1093,9 +1093,7 @@ if (!Array.prototype.indexOf) { var start = 0; var limit = S; var extracted = []; - if (!self) { - limit = S-1; - } + if (!self) limit = S-1; for (i=0; i < N; i++) { do { @@ -5330,6 +5328,8 @@ if (!Array.prototype.indexOf) { * encoded by `PARSE.stringify` * * @param {string} str The string to decode + * @param {function} cb Optional. A callback to apply to each decoded item + * * @return {mixed} The decoded value * * @see JSON.parse @@ -5345,6 +5345,8 @@ if (!Array.prototype.indexOf) { len_inf = PARSE.marker_inf.length, len_minus_inf = PARSE.marker_minus_inf.length; + var customCb; + function walker(o) { var i; if ('object' !== typeof o) return reviver(o); @@ -5386,12 +5388,15 @@ if (!Array.prototype.indexOf) { return -Infinity; } - } + + if (customCb) customCb(value); + return value; } - return function(str) { + return function(str, cb) { + customCb = cb; return walker(JSON.parse(str)); }; @@ -5921,6 +5926,8 @@ if (!Array.prototype.indexOf) { // ## Public properties. + this.name = options.name || 'nddb'; + // ### nddbid // A global index of all objects. this.nddbid = new NDDBIndex('nddbid', this); @@ -6957,25 +6964,28 @@ if (!Array.prototype.indexOf) { /** * ### NDDB.stringify * - * Returns a machine-readable representation of the database + * Stringifies the items in the database in an expanded JSON format * - * Cyclic objects are decycled. + * Cyclic objects are decycled, functions, null, undefined, are kept. * * Evaluates pending queries with `fetch`. * - * @param {boolean} TRUE, if compressed + * @param {boolean} compress Optional. If TRUE, JSON is pretty-printed + * @param {boolean} enclose Optional. If TRUE, items are enclosed in an + * array so that they can be read with a require statement. * * @return {string} out A machine-readable representation of the database * * @see JSUS.stringify */ - NDDB.prototype.stringify = function(compressed) { + NDDB.prototype.stringify = function(compress, enclose) { var db, spaces, out; var item, i, len; - if (!this.size()) return '[]'; - compressed = ('undefined' === typeof compressed) ? true : compressed; - spaces = compressed ? 0 : 4; - out = '['; + enclose = 'undefined' === typeof enclose ? true: enclose; + if (!this.size()) return enclose ? '[]' : ''; + compress = ('undefined' === typeof compress) ? true : compress; + spaces = compress ? 0 : 4; + out = enclose ? '[' : ''; db = this.fetch(); i = -1, len = db.length; for ( ; ++i < len ; ) { @@ -6984,7 +6994,7 @@ if (!Array.prototype.indexOf) { out += J.stringify(item, spaces); if (i !== len-1) out += ', '; } - out += ']'; + if (enclose) out += ']'; return out; }; @@ -7200,6 +7210,7 @@ if (!Array.prototype.indexOf) { this.throwErr('TypeError', 'view', 'idx is reserved word: ' + idx); } if ('undefined' === typeof func) { + // View checks for undefined later. func = function(item) { return item[idx]; }; } else if ('function' !== typeof func) { @@ -7209,8 +7220,9 @@ if (!Array.prototype.indexOf) { // Create a copy of the current settings, without the views and hooks // functions, else we create an infinite loop in the constructor or // hooks are executed multiple times. - settings = this.cloneSettings( { V: true, hooks: true } ); this.__V[idx] = func; + settings = this.cloneSettings( { V: true, hooks: true} ); + settings.name = idx; this[idx] = new NDDB(settings); // Reference to this instance. this[idx].__parentDb = this; @@ -7254,7 +7266,7 @@ if (!Array.prototype.indexOf) { this.throwErr('TypeError', 'hash', 'func must be function or ' + 'undefined. Found: ' + func); } - this[idx] = {}; + this[idx] = {}; // new NDDBHash(); this.__H[idx] = func; }; @@ -7436,6 +7448,7 @@ if (!Array.prototype.indexOf) { continue; } //this.__V[idx] = func, this[idx] = new this.constructor(); + // TODO: When is the view not already created? Check! if (!this[key]) { // Create a copy of the current settings, @@ -7443,6 +7456,9 @@ if (!Array.prototype.indexOf) { // we establish an infinite loop in the // constructor, and the hooks. settings = this.cloneSettings({ V: true, hooks: true }); + settings.name = key; + console.log('saving...', this.name, this.size()); + this[key] = new NDDB(settings); // Reference to this instance. this[key].__parentDb = this; @@ -7486,6 +7502,7 @@ if (!Array.prototype.indexOf) { // we create an infinite loop at first insert, // and the hooks (should be called only on main db). settings = this.cloneSettings({ H: true, hooks: true }); + settings.name = hash; this[key][hash] = new NDDB(settings); // Reference to this instance. this[key][hash].__parentDb = this; @@ -9662,7 +9679,6 @@ if (!Array.prototype.indexOf) { */ NDDB.prototype.addDefaultFormats = null; - // ## Helper Methods /** @@ -10120,6 +10136,31 @@ if (!Array.prototype.indexOf) { this.resolve = {}; }; + + // Inheriting from NDDB. + + // function NDDBHash(conf) { + // + // var len = 0; + // + // this.__add = function(key, nddb) { + // this[key] = nddb; + // + // if (conf) nddb.init(conf); + // + // len++; + // }; + // + // this.__size = function() { return len; }; + // + // } + + + + + + + /** * # NDDBIndex * @@ -24008,7 +24049,7 @@ if (!Array.prototype.indexOf) { /** * # GameDB - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Provides a simple, lightweight NO-SQL database for nodeGame @@ -24051,13 +24092,26 @@ if (!Array.prototype.indexOf) { var that; that = this; options = options || {}; - options.name = options.name || 'gamedb'; + options.name = options.name || 'memory'; if (!options.update) options.update = {}; // Auto build indexes by default. options.update.indexes = true; + // TODO: move on server-side only. + options.defaultCSVHeader = [ + 'session', 'treatment', 'player', 'stage', 'step', 'timestamp', + 'time', 'timeup' + ]; + + // Experimental. TODO. + options.skipCSVKeys = { + isCorrect: true, + id: true, + done: true + }; + NDDB.call(this, options, db); this.comparator('stage', function(o1, o2) { @@ -24081,10 +24135,24 @@ if (!Array.prototype.indexOf) { this.view('done'); - this.on('save', function(options, info) { - if (info.format === 'csv') decorateCSVSaveOptions(that, options); + // TODO: move on server-side only. + this.on('save', function(opts, info) { + if (opts.append) opts.flags = 'a'; + if (info.format === 'csv') decorateCSVSaveOptions(that, opts); }, true); + this.stepView = function(step) { + return this.view(step, function(item) { + if (that.node.game.isStep(step, item.stage)) return true; + }); + }; + + this.stageView = function(stage) { + return this.view(stage, function(item) { + if (that.node.game.isStage(stage, item.stage)) return true; + }); + }; + this.node = this.__shared.node; } @@ -24113,6 +24181,8 @@ if (!Array.prototype.indexOf) { o.session = this.node.nodename; + o.treatment = this.node.game.settings.treatmentName; + this.insert(o); }; @@ -24137,8 +24207,6 @@ if (!Array.prototype.indexOf) { if (!opts.adapter) opts.adapter = {}; - if (opts.append) opts.flags = 'a'; - if (split) { if ('undefined' === typeof opts.adapter.stage) { opts.adapter.stage = function(i) { @@ -24170,14 +24238,15 @@ if (!Array.prototype.indexOf) { } } - if ('undefined' === typeof opts.header && - 'undefined' === typeof opts.headers) { - - opts.header = 'all'; - } - // Flatten. if (opts.flatten) { + + if ('undefined' === typeof opts.header && + 'undefined' === typeof opts.headers) { + + opts.header = that.defaultCSVHeader || 'all'; + } + opts.preprocess = function(item, current) { var s; s = item.stage.stage + '.' + item.stage.step + @@ -26559,7 +26628,7 @@ if (!Array.prototype.indexOf) { /** * # Timer - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Timing-related utility functions @@ -27013,14 +27082,11 @@ if (!Array.prototype.indexOf) { * @see GameTimer */ Timer.prototype.setTimeout = function(timeup, milliseconds, validity) { - var t; - t = this.createTimer({ + return this.createTimer({ milliseconds: milliseconds || 1, timeup: timeup, validity: validity - }); - t.start(); - return t; + }).start(); }; /** @@ -27737,6 +27803,8 @@ if (!Array.prototype.indexOf) { * * @param {object} options Optional. Configuration object * + * @return {GameTimer} The game timer instance for chaining + * * @see GameTimer.addHook */ GameTimer.prototype.init = function(options) { @@ -27825,6 +27893,8 @@ if (!Array.prototype.indexOf) { if (checkInitialized(this) === null) { this.status = GameTimer.INITIALIZED; } + + return this; }; @@ -27837,6 +27907,8 @@ if (!Array.prototype.indexOf) { * otherwise it is called as a function. * * @param {mixed} h The hook to fire (object, function, or string) + * + * @return {GameTimer} The game timer instance for chaining */ GameTimer.prototype.fire = function(h) { var hook, ctx; @@ -27857,6 +27929,8 @@ if (!Array.prototype.indexOf) { throw new TypeError('GameTimer.fire: h must be function, string ' + 'or object. Found: ' + h); } + + return this; }; /** @@ -27871,6 +27945,8 @@ if (!Array.prototype.indexOf) { * When the timer expires the timeup event is fired, and the * timer is stopped * + * @return {GameTimer} The game timer instance for chaining + * * @see GameTimer.status * @see GameTimer.timeup * @see GameTimer.fire @@ -27892,7 +27968,7 @@ if (!Array.prototype.indexOf) { if (this.startPaused) { this.pause(); - return; + return this; } // Remember time of start (used by this.pause to compute remaining time) @@ -27904,7 +27980,7 @@ if (!Array.prototype.indexOf) { this.options.milliseconds <= 0) { this.doTimeup(); - return; + return this; } this.updateRemaining = this.update; @@ -27915,6 +27991,8 @@ if (!Array.prototype.indexOf) { this.timerId = setInterval(function() { updateCallback(that); }, this.update); + + return this; }; /** @@ -27925,14 +28003,14 @@ if (!Array.prototype.indexOf) { * The first parameter can be a string, a function, or an object * containing an hook property. * - * @params {string|function|object} hook The hook (string or function), + * @param {string|function|object} hook The hook (string or function), * or an object containing a `hook` property (others: `ctx` and `name`) - * @params {object} ctx The context wherein the hook is called. + * @param {object} ctx The context wherein the hook is called. * Default: node.game - * @params {string} name The name of the hook. Default: a random name + * @param {string} name The name of the hook. Default: a random name * starting with 'timerHook' * - * @returns {string} The name of the hook + * @return {string} The name of the hook */ GameTimer.prototype.addHook = function(hook, ctx, name) { checkDestroyed(this, 'addHook'); @@ -27955,10 +28033,11 @@ if (!Array.prototype.indexOf) { } this.hookNames[name] = true; this.hooks.push({hook: hook, ctx: ctx, name: name}); + return name; }; - /* + /** * ### GameTimer.removeHook * * Removes a hook by its name @@ -27988,6 +28067,8 @@ if (!Array.prototype.indexOf) { * * If the timer was running, clear the interval and sets the * status property to `GameTimer.PAUSED`. + * + * @return {GameTimer} The game timer instance for chaining */ GameTimer.prototype.pause = function() { var timestamp; @@ -28013,7 +28094,7 @@ if (!Array.prototype.indexOf) { } else if (this.status === GameTimer.STOPPED) { // If the timer was explicitly stopped, we ignore the pause: - return; + return this; } else if (!this.isPaused()) { // pause() was called before start(); remember it: @@ -28022,6 +28103,8 @@ if (!Array.prototype.indexOf) { else { throw new Error('GameTimer.pause: timer was already paused'); } + + return this; }; /** @@ -28031,6 +28114,8 @@ if (!Array.prototype.indexOf) { * * If the timer was paused, restarts it with the current configuration * + * @return {GameTimer} The game timer instance for chaining + * * @see GameTimer.restart */ GameTimer.prototype.resume = function() { @@ -28040,7 +28125,7 @@ if (!Array.prototype.indexOf) { // Don't start if the initialization is incomplete (invalid state): if (this.status === GameTimer.UNINITIALIZED) { this.startPaused = false; - return; + return this; } if (!this.isPaused() && !this.startPaused) { @@ -28066,6 +28151,8 @@ if (!Array.prototype.indexOf) { that.status = GameTimer.RUNNING; } }, this.updateRemaining); + + return this; }; /** @@ -28076,6 +28163,8 @@ if (!Array.prototype.indexOf) { * If the timer was paused or running, clear the interval, sets the * status property to `GameTimer.STOPPED`, and reset the time passed * and time left properties + * + * @return {GameTimer} The game timer instance for chaining */ GameTimer.prototype.stop = function() { checkDestroyed(this, 'stop'); @@ -28094,6 +28183,8 @@ if (!Array.prototype.indexOf) { this.startPaused = null; this.updateRemaining = 0; this.updateStart = 0; + + return this; }; /** @@ -28107,6 +28198,8 @@ if (!Array.prototype.indexOf) { * * Does **not** change properties: eventEmitterName, and * stagerSync. + * + * @return {GameTimer} The game timer instance for chaining */ GameTimer.prototype.reset = function() { checkDestroyed(this, 'reset'); @@ -28117,6 +28210,8 @@ if (!Array.prototype.indexOf) { this.timeup = 'TIMEUP'; this.hooks = []; this.hookNames = {}; + + return this; }; /** @@ -28129,13 +28224,15 @@ if (!Array.prototype.indexOf) { * * @param {object} options Optional. A configuration object * + * @return {GameTimer} The game timer instance for chaining + * * @see GameTimer.init */ GameTimer.prototype.restart = function(options) { checkDestroyed(this, 'restart'); if (!this.isStopped()) this.stop(); this.init(options); - this.start(); + return this.start(); }; /** @@ -28144,6 +28241,8 @@ if (!Array.prototype.indexOf) { * Returns whether timer is running * * Running means either LOADING or RUNNING. + * + * @return {boolean} TRUE if timer is running */ GameTimer.prototype.isRunning = function() { checkDestroyed(this, 'isRunning'); @@ -28157,25 +28256,23 @@ if (!Array.prototype.indexOf) { * * Stopped means either UNINITIALIZED, INITIALIZED or STOPPED. * + * @return {boolean} TRUE if timer is stopped + * * @see GameTimer.isPaused */ GameTimer.prototype.isStopped = function() { checkDestroyed(this, 'isStopped'); - if (this.status === GameTimer.UNINITIALIZED || + return (this.status === GameTimer.UNINITIALIZED || this.status === GameTimer.INITIALIZED || - this.status === GameTimer.STOPPED) { - - return true; - } - else { - return false; - } + this.status === GameTimer.STOPPED); }; /** * ### GameTimer.isPaused * * Returns whether timer is paused + * + * @return {boolean} TRUE if timer is paused */ GameTimer.prototype.isPaused = function() { checkDestroyed(this, 'isPaused'); @@ -28264,6 +28361,8 @@ if (!Array.prototype.indexOf) { * It will call timeup even if the game is paused/stopped, * but not if timeup was already called. * + * @return {GameTimer} The game timer instance for chaning + * * @see GameTimer.isTimeup * @see GameTimer.stop * @see GameTimer.fire @@ -28273,7 +28372,7 @@ if (!Array.prototype.indexOf) { if (this.isTimeup()) return; if (!this.isStopped()) this.stop(); this._timeup = true; - this.fire(this.timeup); + return this.fire(this.timeup); }; // TODO: improve. @@ -28422,9 +28521,8 @@ if (!Array.prototype.indexOf) { that.doTimeup(); return false; } - else { - return true; - } + + return true; } /** @@ -31534,7 +31632,9 @@ if (!Array.prototype.indexOf) { // Time and timeup. if (!o.time) o.time = stepTime; - if (!o.timeup) o.timeup = game.timer.isTimeup(); + if ('undefined' === typeof o.timeup) { + o.timeup = game.timer.isTimeup(); + } // Add role and partner info. if (game.role && !o.role) o.role = game.role; @@ -32473,10 +32573,18 @@ if (!Array.prototype.indexOf) { * @emit PLAYING */ this.events.ng.on('LOADED', function() { + var frame; node.game.setStageLevel(constants.stageLevels.LOADED); if (node.socket.shouldClearBuffer()) { node.socket.clearBuffer(); } + + // Make the frame visibile (if any). + if (node.window) { + frame = node.window.getFrame(); + if (frame) frame.style.visibility = ''; + } + if (node.game.shouldEmitPlaying()) { node.emit('PLAYING'); } @@ -33215,14 +33323,14 @@ if (!Array.prototype.indexOf) { }); // ### node.on.data - this.alias('done', ['in.say.DATA', 'in.set.DATA'], function(text, cb) { - if ('string' !== typeof text || text === '') { - throw new TypeError('node.on.data: text must be a non-empty ' + - 'string. Found: ' + text); - } + this.alias('done', 'in.set.DATA', function(step, cb) { return function(msg) { - if (msg.text === text) cb.call(that.game, msg); - else return false; + if (!msg.data || !msg.data.done || + !that.game.isStep(step, msg.stage)) { + + return false; + } + cb.call(that.game, msg); }; }); @@ -35367,6 +35475,9 @@ if (!Array.prototype.indexOf) { // Keep track of nested call to loadFrame. updateAreLoading(this, 1); + // Hide iframe content while loading. + iframe.style.visibility = 'hidden'; + // Add the onLoad event listener: if (!loadCache || !frameReady) { onLoad(iframe, function() { @@ -36551,7 +36662,7 @@ if (!Array.prototype.indexOf) { /** * # listeners - * Copyright(c) 2015 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * GameWindow listeners @@ -36562,22 +36673,22 @@ if (!Array.prototype.indexOf) { "use strict"; - var J = node.JSUS; - - function getElement(idOrObj, prefix) { - var el; - if ('string' === typeof idOrObj) { - el = W.getElementById(idOrObj); - } - else if (J.isElement(idOrObj)) { - el = idOrObj; - } - else { - throw new TypeError(prefix + ': idOrObj must be string ' + - ' or HTML Element.'); - } - return el; - } + // var J = node.JSUS; + + // function getElement(idOrObj, prefix) { + // var el; + // if ('string' === typeof idOrObj) { + // el = W.getElementById(idOrObj); + // } + // else if (J.isElement(idOrObj)) { + // el = idOrObj; + // } + // else { + // throw new TypeError(prefix + ': idOrObj must be string ' + + // ' or HTML Element.'); + // } + // return el; + // } var GameWindow = node.GameWindow; diff --git a/listeners/internal.js b/listeners/internal.js index 6942fb39..821338a8 100644 --- a/listeners/internal.js +++ b/listeners/internal.js @@ -96,10 +96,21 @@ * @emit PLAYING */ this.events.ng.on('LOADED', function() { + var frame; node.game.setStageLevel(constants.stageLevels.LOADED); if (node.socket.shouldClearBuffer()) { node.socket.clearBuffer(); } + + // Make the frame visibile (if any). + // The Window hides it with every new load, so that if the page + // is manipulated in the step callback, the user still sees it + // appearing all at once. + if (node.window) { + frame = node.window.getFrame(); + if (frame) frame.style.visibility = ''; + } + if (node.game.shouldEmitPlaying()) { node.emit('PLAYING'); } From b2d75081231207e917d0417fb161a14317734c93 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 16 Jun 2021 12:34:29 +0200 Subject: [PATCH 23/51] preprocess to CSV uses ids instead of numbers of stage/steps --- lib/core/GameDB.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/core/GameDB.js b/lib/core/GameDB.js index 625c108e..952163a1 100644 --- a/lib/core/GameDB.js +++ b/lib/core/GameDB.js @@ -200,8 +200,12 @@ opts.preprocess = function(item, current) { var s; - s = item.stage.stage + '.' + item.stage.step + - '.' + item.stage.round; + // s = item.stage.stage + '.' + item.stage.step + + // '.' + item.stage.round; + s = that.node.game.plot.getStage(item.stage).id; + s += '.' + that.node.game.plot.getStep(item.stage).id; + s += '.' + item.stage.round; + that.node.game.plot.getStage() if (item.time) item['time_' + s] = item.time; if (item.timeup) item['timeup_' + s] = item.timeup; if (item.timestamp) item['timestamp_' + s] = item.timestamp; From 9e962d339d586edc88a970ad5ddc32d0f7a30f36 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 16 Jun 2021 12:35:10 +0200 Subject: [PATCH 24/51] stepId and stageId added in DONE messages --- lib/modules/ssgd.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/modules/ssgd.js b/lib/modules/ssgd.js index 4ea8dccd..f03c4430 100644 --- a/lib/modules/ssgd.js +++ b/lib/modules/ssgd.js @@ -373,6 +373,9 @@ if (game.role && !o.role) o.role = game.role; if (game.partner && !o.partner) o.partner = game.partner; + o.stepId = game.getStepId(); + o.stageId = game.getStageId(); + // Mark done msg. o.done = true; From d599f31001b266a3c7f0194353445a9fefc3ca04 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 16 Jun 2021 12:35:42 +0200 Subject: [PATCH 25/51] alias DONE fixed --- listeners/aliases.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/listeners/aliases.js b/listeners/aliases.js index 7e024910..fbc71ba4 100644 --- a/listeners/aliases.js +++ b/listeners/aliases.js @@ -49,9 +49,13 @@ // ### node.on.data this.alias('done', 'in.set.DATA', function(step, cb) { + if ('undefined' === typeof cb && 'function' === typeof step) { + cb = step; + step = null; + } return function(msg) { if (!msg.data || !msg.data.done || - !that.game.isStep(step, msg.stage)) { + (step && !that.game.isStep(step, msg.stage))) { return false; } From cbdff18f8adb505ca69769dde7cd133436128ceb Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 16 Jun 2021 12:36:10 +0200 Subject: [PATCH 26/51] built --- build/nodegame-full.js | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index e50d7e51..153fc1ac 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -7457,7 +7457,7 @@ if (!Array.prototype.indexOf) { // constructor, and the hooks. settings = this.cloneSettings({ V: true, hooks: true }); settings.name = key; - console.log('saving...', this.name, this.size()); + // console.log('saving...', this.name, this.size()); this[key] = new NDDB(settings); // Reference to this instance. @@ -9732,6 +9732,9 @@ if (!Array.prototype.indexOf) { res = this.emit('insert', o, this.db.length); // Stop inserting elements if one callback returned FALSE. if (res === false) return false; + // Replace element with return value if object. + + this.db.push(o); if (doUpdate) { this._indexIt(o, (this.db.length-1)); @@ -24249,8 +24252,12 @@ if (!Array.prototype.indexOf) { opts.preprocess = function(item, current) { var s; - s = item.stage.stage + '.' + item.stage.step + - '.' + item.stage.round; + // s = item.stage.stage + '.' + item.stage.step + + // '.' + item.stage.round; + s = that.node.game.plot.getStage(item.stage).id; + s += '.' + that.node.game.plot.getStep(item.stage).id; + s += '.' + item.stage.round; + that.node.game.plot.getStage() if (item.time) item['time_' + s] = item.time; if (item.timeup) item['timeup_' + s] = item.timeup; if (item.timestamp) item['timestamp_' + s] = item.timestamp; @@ -31640,6 +31647,9 @@ if (!Array.prototype.indexOf) { if (game.role && !o.role) o.role = game.role; if (game.partner && !o.partner) o.partner = game.partner; + o.stepId = game.getStepId(); + o.stageId = game.getStageId(); + // Mark done msg. o.done = true; @@ -32580,6 +32590,9 @@ if (!Array.prototype.indexOf) { } // Make the frame visibile (if any). + // The Window hides it with every new load, so that if the page + // is manipulated in the step callback, the user still sees it + // appearing all at once. if (node.window) { frame = node.window.getFrame(); if (frame) frame.style.visibility = ''; @@ -33324,9 +33337,13 @@ if (!Array.prototype.indexOf) { // ### node.on.data this.alias('done', 'in.set.DATA', function(step, cb) { + if ('undefined' === typeof cb && 'function' === typeof step) { + cb = step; + step = null; + } return function(msg) { if (!msg.data || !msg.data.done || - !that.game.isStep(step, msg.stage)) { + (step && !that.game.isStep(step, msg.stage))) { return false; } @@ -35476,6 +35493,10 @@ if (!Array.prototype.indexOf) { updateAreLoading(this, 1); // Hide iframe content while loading. + // This way if the page is manipulated in the step callback, + // the user still sees it appearing all at once. + // The iframe visibility is reset by nodegame-client listener + // on LOADED (avoiding registering two listeners this way.) iframe.style.visibility = 'hidden'; // Add the onLoad event listener: @@ -48956,6 +48977,9 @@ if (!Array.prototype.indexOf) { if ('string' === typeof s) { s = { id: s }; } + else if (J.isArray(s)) { + s = { id: s[0], left: s[1] }; + } else if ('object' !== typeof s) { throw new TypeError('ChoiceTableGroup.buildTable: item must be ' + 'string or object. Found: ' + s); @@ -54054,7 +54078,6 @@ if (!Array.prototype.indexOf) { // Checked when the widget is created. EndScreen.dependencies = { - JSUS: {}, Feedback: {}, EmailForm: {} }; From f5e0ff6d29df755e02564ed44cc16a8d0971d633 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 26 Aug 2021 21:22:59 +0200 Subject: [PATCH 27/51] handling gracefully redirect for bots --- lib/core/Socket.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/core/Socket.js b/lib/core/Socket.js index e9596d3b..2f9a95d5 100644 --- a/lib/core/Socket.js +++ b/lib/core/Socket.js @@ -522,7 +522,9 @@ if (msg.to === parent.constants.UNAUTH_PLAYER) { this.node.warn('connection was not authorized.'); if (msg.text === 'redirect') { - window.location = msg.data; + if ('undefined' !== typeof window) { + window.location = msg.data; + } } else { this.disconnect(); From 66b4d9f29e254d5bc23957f31b74d3adc121a803 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 26 Aug 2021 21:23:37 +0200 Subject: [PATCH 28/51] error messages displayed in wait screen --- build/nodegame-full.js | 581 +++++++++++++++++++++++++++------------ lib/core/ErrorManager.js | 10 + package.json | 2 +- 3 files changed, 414 insertions(+), 179 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 153fc1ac..3575c9c4 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -4984,7 +4984,7 @@ if (!Array.prototype.indexOf) { /** * # TIME - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Collection of static functions related to the generation, @@ -4996,28 +4996,34 @@ if (!Array.prototype.indexOf) { function TIME() {} + function pad(number) { + return (number < 10) ? '0' + number : number; + } + + function _getTime(ms) { + var d, res; + d = new Date(); + res = pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + + pad(d.getSeconds()); + if (ms) res += ':' + pad(d.getMilliseconds()); + return res; + } + // Polyfill for Date.toISOString (IE7, IE8, IE9) // Kudos: https://developer.mozilla.org/en-US/docs/Web/ // JavaScript/Reference/Global_Objects/Date/toISOString if (!Date.prototype.toISOString) { - (function() { - - function pad(number) { - return (number < 10) ? '0' + number : number; - } - - Date.prototype.toISOString = function() { - var ms = (this.getUTCMilliseconds() / 1000).toFixed(3); - return this.getUTCFullYear() + - '-' + pad(this.getUTCMonth() + 1) + - '-' + pad(this.getUTCDate()) + - 'T' + pad(this.getUTCHours()) + - ':' + pad(this.getUTCMinutes()) + - ':' + pad(this.getUTCSeconds()) + - '.' + ms.slice(2, 5) + 'Z'; - }; - }()); + Date.prototype.toISOString = function() { + var ms = (this.getUTCMilliseconds() / 1000).toFixed(3); + return this.getUTCFullYear() + + '-' + pad(this.getUTCMonth() + 1) + + '-' + pad(this.getUTCDate()) + + 'T' + pad(this.getUTCHours()) + + ':' + pad(this.getUTCMinutes()) + + ':' + pad(this.getUTCSeconds()) + + '.' + ms.slice(2, 5) + 'Z'; + }; } /** @@ -5049,9 +5055,7 @@ if (!Array.prototype.indexOf) { * @see TIME.getTimeM */ TIME.getTime = function() { - var d; - d = new Date(); - return d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds(); + return _getTime(); }; /** @@ -5068,10 +5072,7 @@ if (!Array.prototype.indexOf) { * @see TIME.getTime */ TIME.getTimeM = function() { - var d; - d = new Date(); - return d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds() + - ':' + d.getMilliseconds(); + return _getTime(true); }; /** @@ -5105,7 +5106,6 @@ if (!Array.prototype.indexOf) { return result; }; - /** * ## TIME.now * @@ -5356,6 +5356,8 @@ if (!Array.prototype.indexOf) { else o[i] = reviver(o[i]); } } + // On the full object. + if (customCb) customCb(o); return o; } @@ -5390,8 +5392,6 @@ if (!Array.prototype.indexOf) { } } - if (customCb) customCb(value); - return value; } @@ -5837,7 +5837,7 @@ if (!Array.prototype.indexOf) { /** * # NDDB: N-Dimensional Database - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * NDDB is a powerful and versatile object database for node.js and the browser. @@ -5875,6 +5875,22 @@ if (!Array.prototype.indexOf) { */ var df = J.compatibility().defineProperty; + /** + * ### NDDB.db + * + * Returns a new db + * + * @param {object} options Optional. Configuration options + * @param {db} db Optional. An initial set of items to import + * + * @return {object} A new database + */ + NDDB.db = function(opts, db) { return new NDDB(opts, db); }; + + // Might get overwritten in index.js. + NDDB.lineBreak = '\n'; + + /** * ### NDDB.decycle * @@ -5887,7 +5903,7 @@ if (!Array.prototype.indexOf) { * @see https://github.com/douglascrockford/JSON-js/ */ NDDB.decycle = function(e) { - if (JSON && JSON.decycle && 'function' === typeof JSON.decycle) { + if (JSON && 'function' === typeof JSON.decycle) { e = JSON.decycle(e); } return e; @@ -5905,7 +5921,7 @@ if (!Array.prototype.indexOf) { * @see https://github.com/douglascrockford/JSON-js/ */ NDDB.retrocycle = function(e) { - if (JSON && JSON.retrocycle && 'function' === typeof JSON.retrocycle) { + if (JSON && 'function' === typeof JSON.retrocycle) { e = JSON.retrocycle(e); } return e; @@ -5919,14 +5935,14 @@ if (!Array.prototype.indexOf) { * @param {object} options Optional. Configuration options * @param {db} db Optional. An initial set of items to import */ - function NDDB(options, db) { + function NDDB(opts, db) { var that; that = this; - options = options || {}; + opts = opts || {}; // ## Public properties. - this.name = options.name || 'nddb'; + this.name = opts.name || 'nddb'; // ### nddbid // A global index of all objects. @@ -5985,6 +6001,7 @@ if (!Array.prototype.indexOf) { // ### filters // Available db filters + this.filters = {}; this.addDefaultFilters(); // ### __userDefinedFilters @@ -6050,8 +6067,8 @@ if (!Array.prototype.indexOf) { // ### log // Std out for log messages // - // It can be overriden in options by another function (`options.log`). - // `options.logCtx` specif the context of execution. + // It can be overriden in options by another function (`opts.log`). + // `opts.logCtx` specif the context of execution. // @see NDDB.initLog this.log = console.log; @@ -6107,10 +6124,14 @@ if (!Array.prototype.indexOf) { this.__cache = {}; // Mixing in user options and defaults. - this.init(options); + this.init(opts); // Importing items, if any. if (db) this.importDB(db); + + if (opts.journal && 'function' === typeof NDDB.prototype.journal) { + this.journal({ filename: opts.journal, load: true, cb: opts.cb }); + } } /** @@ -6167,7 +6188,6 @@ if (!Array.prototype.indexOf) { * @see NDDB.filters */ NDDB.prototype.addDefaultFilters = function() { - if (!this.filters) this.filters = {}; var that; that = this; @@ -6964,39 +6984,74 @@ if (!Array.prototype.indexOf) { /** * ### NDDB.stringify * - * Stringifies the items in the database in an expanded JSON format + * Stringifies the items in the database in *JSON format * * Cyclic objects are decycled, functions, null, undefined, are kept. * * Evaluates pending queries with `fetch`. * - * @param {boolean} compress Optional. If TRUE, JSON is pretty-printed - * @param {boolean} enclose Optional. If TRUE, items are enclosed in an - * array so that they can be read with a require statement. + * @param {object} opts Configuration options: + * - enclose: adds [] around all items. Default: false. + * - comma: separates items with a comma. Default: false. + * - pretty: pretty-print items. Default: false + * - lineBreak: line-break separator. Default: os.EOL or '\n'; + * - decycle: Decycle ciclic objects. Default: true. * * @return {string} out A machine-readable representation of the database * * @see JSUS.stringify */ - NDDB.prototype.stringify = function(compress, enclose) { - var db, spaces, out; - var item, i, len; - enclose = 'undefined' === typeof enclose ? true: enclose; - if (!this.size()) return enclose ? '[]' : ''; - compress = ('undefined' === typeof compress) ? true : compress; - spaces = compress ? 0 : 4; - out = enclose ? '[' : ''; - db = this.fetch(); - i = -1, len = db.length; - for ( ; ++i < len ; ) { - // Decycle, if possible. - item = NDDB.decycle(db[i]); - out += J.stringify(item, spaces); - if (i !== len-1) out += ', '; - } - if (enclose) out += ']'; - return out; - }; + NDDB.prototype.stringify = (function() { + + function stringifyItem(item, lineBreak, spaces, comma, decycle) { + var item, res, re; + // TODO: merge stringify and decycle in one. + if (decycle) item = NDDB.decycle(item); + res = J.stringify(item, spaces); + // Auto-escaped. + // if (stripLineBreaks) { + // re = new RegExp(lineBreak, 'g'); + // res = res.replace(re, lineBreakReplace); + // } + if (comma) res += ', '; + if (lineBreak) res += lineBreak; + return res; + }; + + return function(opts) { + var db, i, len, out; + var spaces, lineBreak, decycle; + + opts = opts || {}; + + if (!this.size()) return opts.enclose ? '[]' : ''; + + decycle = opts.decycle !== false; + lineBreak = opts.lineBreak || NDDB.lineBreak; + + spaces = opts.pretty ? 4 : 0; + out = opts.enclose ? '[' + lineBreak : ''; + + db = this.fetch(); + + + // Main loop. + i = -1, len = (db.length -1); + for ( ; ++i < len ; ) { + out += stringifyItem(db[i], lineBreak, spaces, + opts.comma, decycle); + } + // Last item (no comma). + out += stringifyItem(db[i], lineBreak, spaces, false, decycle); + + if (opts.enclose) out += ']'; + return out; + }; + })(); + + + + /** * ### NDDB.comparator @@ -8189,6 +8244,33 @@ if (!Array.prototype.indexOf) { // ## Custom callbacks + /** + * ### NDDB.table + * + * Returns the frequency table for the specified indexes + * + * TODO: support multiple indexes, at least two. + * TODO: support returning a sorted array. + * TODO: keep table in memory if key is already an index + * + * @param {string} idx The name of first index + * + * @return {object} res An object containing the frequency table + */ + NDDB.prototype.table = function(idx) { + var res, db, i, v; + db = this.fetch(); + res = {}; + for (i = 0; i < db.length; i++) { + v = db[i][idx]; + if ('undefined' !== typeof v) { + if ('undefined' === typeof res[v]) res[v] = 1; + else res[v]++; + } + } + return res; + }; + /** * ### NDDB.filter * @@ -9517,7 +9599,7 @@ if (!Array.prototype.indexOf) { * Reads items in the specified format and loads them into db asynchronously * * @param {string} file The name of the file or other persistent storage - * @param {object} options Optional. A configuration object. Available + * @param {object} opts Optional. A configuration object. Available * options are format-dependent. * @param {function} cb Optional. A callback function to execute at * the end of the operation. If options is not specified, @@ -9525,12 +9607,8 @@ if (!Array.prototype.indexOf) { * * @see NDDB.loadSync */ - NDDB.prototype.load = function(file, options, cb) { - if (arguments.length === 2 && 'function' === typeof options) { - cb = options; - options = undefined; - } - executeSaveLoad(this, 'load', file, cb, options); + NDDB.prototype.load = function(file, opts, cb) { + return executeSaveLoad(this, 'load', file, cb, opts); }; /** @@ -9540,12 +9618,8 @@ if (!Array.prototype.indexOf) { * * @see NDDB.saveSync */ - NDDB.prototype.save = function(file, options, cb) { - if (arguments.length === 2 && 'function' === typeof options) { - cb = options; - options = undefined; - } - executeSaveLoad(this, 'save', file, cb, options); + NDDB.prototype.save = function(file, opts, cb) { + return executeSaveLoad(this, 'save', file, cb, opts); }; /** @@ -9555,12 +9629,8 @@ if (!Array.prototype.indexOf) { * * @see NDDB.load */ - NDDB.prototype.loadSync = function(file, options, cb) { - if (arguments.length === 2 && 'function' === typeof options) { - cb = options; - options = undefined; - } - executeSaveLoad(this, 'loadSync', file, cb, options); + NDDB.prototype.loadSync = function(file, opts, cb) { + return executeSaveLoad(this, 'loadSync', file, cb, opts); }; /** @@ -9570,12 +9640,8 @@ if (!Array.prototype.indexOf) { * * @see NDDB.save */ - NDDB.prototype.saveSync = function(file, options, cb) { - if (arguments.length === 2 && 'function' === typeof options) { - cb = options; - options = undefined; - } - executeSaveLoad(this, 'saveSync', file, cb, options); + NDDB.prototype.saveSync = function(file, opts, cb) { + return executeSaveLoad(this, 'saveSync', file, cb, opts); }; // ## Formats. @@ -9626,13 +9692,7 @@ if (!Array.prototype.indexOf) { */ NDDB.prototype.getFormat = function(format, method) { var f; - if ('string' !== typeof format) { - this.throwErr('TypeError', 'getFormat', 'format must be string'); - } - if (method && 'string' !== typeof method) { - this.throwErr('TypeError', 'getFormat', 'method must be string ' + - 'or undefined'); - } + f = this.__formats[format]; if (f && method) f = f[method]; return f || null; @@ -9732,9 +9792,6 @@ if (!Array.prototype.indexOf) { res = this.emit('insert', o, this.db.length); // Stop inserting elements if one callback returned FALSE. if (res === false) return false; - // Replace element with return value if object. - - this.db.push(o); if (doUpdate) { this._indexIt(o, (this.db.length-1)); @@ -9765,13 +9822,15 @@ if (!Array.prototype.indexOf) { 'or undefined. Found: ' + cb); } if (options && 'object' !== typeof options) { - that.throwErr('TypeError', method, 'options must be object ' + - 'or undefined. Found: ' + options); + if ('function' !== typeof options || 'undefined' !== typeof cb) { + that.throwErr('TypeError', method, 'options must be object ' + + 'or undefined. Found: ' + options); + } } } /** - * ### extractExtension + * ### getExtension * * Extracts the extension from a file name * @@ -9779,7 +9838,7 @@ if (!Array.prototype.indexOf) { * * @return {string} The extension or NULL if not found */ - function extractExtension(file) { + function getExtension(file) { var format; format = file.lastIndexOf('.'); return format < 0 ? null : file.substr(format+1); @@ -9798,17 +9857,28 @@ if (!Array.prototype.indexOf) { * @param {string} method The name of the method invoking validation * @param {string} file The file parameter * @param {function} cb The callback parameter - * @param {object} The options parameter + * @param {object} options The options parameter + * + * @return {NDDB} that The current instance for chaining */ function executeSaveLoad(that, method, file, cb, options) { var ff, format; if (!that.storageAvailable()) { that.throwErr('Error', 'save', 'no persistent storage available'); } + // Cb not specified. + if ('undefined' === typeof options && 'object' === typeof cb) { + options = cb; + cb = undefined; + } + else if ('undefined' === typeof cb && 'function' === typeof options) { + cb = options; + options = undefined; + } validateSaveLoadParameters(that, method, file, cb, options); options = options || {}; - format = extractExtension(file); - // If try to get the format function based on the extension, + format = options.format || getExtension(file); + // Try to get the format function based on the extension, // otherwise try to use the default one. Throws errors. ff = findFormatFunction(that, method, format); // Emit save or load. Options can be modified. @@ -9818,6 +9888,8 @@ if (!Array.prototype.indexOf) { cb: cb }); ff(that, file, cb, options); + + return that; } /** @@ -10306,6 +10378,7 @@ if (!Array.prototype.indexOf) { */ NDDBIndex.prototype.update = function(idx, update) { var o, dbidx, nddb, res; + if ('undefined' === typeof update) return false; dbidx = this.resolve[idx]; if ('undefined' === typeof dbidx) return false; nddb = this.nddb; @@ -10895,12 +10968,22 @@ if (!Array.prototype.indexOf) { that = this; if (!J.isNodeJS()) { window.onerror = function(msg, url, lineno, colno, error) { + var str; msg = node.game.getCurrentGameStage().toString() + '@' + J.getTime() + '> ' + url + ' ' + lineno + ',' + colno + ': ' + msg; if (error) msg + ' - ' + JSON.stringify(error); that.lastError = msg; node.err(msg); + if (node.debug) { + W.init({ waitScreen: true }); + str = 'DEBUG mode: client-side error ' + + 'detected

'; + str += msg; + str += '

' + + 'This message will not be shown in production mode.'; + W.lockScreen(str); + } return !node.debug; }; } @@ -20528,7 +20611,9 @@ if (!Array.prototype.indexOf) { if (msg.to === parent.constants.UNAUTH_PLAYER) { this.node.warn('connection was not authorized.'); if (msg.text === 'redirect') { - window.location = msg.data; + if ('undefined' !== typeof window) { + window.location = msg.data; + } } else { this.disconnect(); @@ -40459,7 +40544,9 @@ if (!Array.prototype.indexOf) { if (!this.headingDiv) { // Add heading. if (!options) { - options = { className: 'panel-heading' }; + // Bootstrap 3 + // options = { className: 'panel-heading' }; + options = { className: 'card-header' }; } else if ('object' !== typeof options) { throw new TypeError('Widget.setTitle: options must ' + @@ -40517,6 +40604,23 @@ if (!Array.prototype.indexOf) { that.headingDiv.appendChild(link); })(this); } + if (this.info) { + (function(that) { + var link, img, a; + + link = W.add('span', that.headingDiv); + link.className = 'panel-collapse-link'; + + // link.style['margin-right'] = '8px'; + a = W.add('a', link); + a.href = that.info; + a.target = '_blank'; + + img = W.add('img', a); + img.src = '/images/info.png'; + + })(this); + } } }; @@ -40551,7 +40655,10 @@ if (!Array.prototype.indexOf) { if (!this.footerDiv) { // Add footer. if (!options) { - options = { className: 'panel-footer' }; + // Bootstrap 3. + // options = { className: 'panel-footer' }; + // Bootstrap 5. + options = { className: 'card-footer' }; } else if ('object' !== typeof options) { throw new TypeError('Widget.setFooter: options must ' + @@ -40573,7 +40680,7 @@ if (!Array.prototype.indexOf) { else { throw new TypeError(J.funcName(this.constructor) + '.setFooter: footer must be string, ' + - 'HTML element or falsy. Found: ' + title); + 'HTML element or falsy. Found: ' + footer); } } }; @@ -40581,20 +40688,11 @@ if (!Array.prototype.indexOf) { /** * ### Widget.setContext * - * Changes the default context of the class 'panel-' + context - * - * Context are defined in Bootstrap framework. - * - * @param {string} context The type of the context + * @deprecated */ - Widget.prototype.setContext = function(context) { - if ('string' !== typeof context) { - throw new TypeError(J.funcName(this.constructor) + '.setContext: ' + - 'context must be string. Found: ' + context); - - } - W.removeClass(this.panelDiv, 'panel-[a-z]*'); - W.addClass(this.panelDiv, 'panel-' + context); + Widget.prototype.setContext = function() { + console.log('*** Deprecation warning: setContext no longer ' + + 'available in Bootstrap5.'); }; /** @@ -41077,7 +41175,7 @@ if (!Array.prototype.indexOf) { /** * # Widgets - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Helper class to interact with nodeGame widgets @@ -41402,6 +41500,8 @@ if (!Array.prototype.indexOf) { widget.closable = options.closable || false; widget.collapseTarget = options.collapseTarget || this.collapseTarget || null; + widget.info = options.info || false; + widget.hooks = { hidden: [], shown: [], @@ -41440,7 +41540,7 @@ if (!Array.prototype.indexOf) { widget.highlighted = null; widget.collapsed = null; widget.hidden = null; - widget.docked = null + widget.docked = null; // Properties that will modify the UI of the widget once appended. @@ -41632,10 +41732,21 @@ if (!Array.prototype.indexOf) { // Add panelDiv (with or without panel). tmp = options.panel === false ? true : w.panel === false; - tmp = { - className: tmp ? [ 'ng_widget', 'no-panel', w.className ] : - [ 'ng_widget', 'panel', 'panel-default', w.className ] - }; + + if (options.bootstrap5) { + // Bootstrap 5 + tmp = { + className: tmp ? [ 'ng_widget', 'no-panel', w.className ] : + [ 'ng_widget', 'card', w.className ] + }; + } + else { + // Bootstrap 3 + tmp = { + className: tmp ? [ 'ng_widget', 'no-panel', w.className ] : + [ 'ng_widget', 'panel', 'panel-default', w.className ] + }; + } // Dock it. if (options.docked || w._docked) { @@ -41649,19 +41760,46 @@ if (!Array.prototype.indexOf) { // Optionally add title (and div). if (options.title !== false && w.title) { - tmp = options.panel === false ? - 'no-panel-heading' : 'panel-heading'; + + if (options.bootstrap5) { + // Bootstrap 5. + tmp = options.panel === false ? + 'no-panel-heading' : 'card-header'; + } + else { + // Bootstrap 3. + tmp = options.panel === false ? + 'no-panel-heading' : 'panel-heading'; + } + w.setTitle(w.title, { className: tmp }); } // Add body (with or without panel). - tmp = options.panel !== false ? 'panel-body' : 'no-panel-body'; + if (options.bootstrap5) { + // Bootstrap 5. + tmp = options.panel !== false ? 'card-body' : 'no-panel-body'; + } + else { + // Bootstrap 3. + tmp = options.panel !== false ? 'panel-body' : 'no-panel-body'; + } + w.bodyDiv = W.append('div', w.panelDiv, { className: tmp }); // Optionally add footer. if (w.footer) { - tmp = options.panel === false ? - 'no-panel-heading' : 'panel-heading'; + if (options.bootstrap5) { + // Bootstrap 5. + tmp = options.panel === false ? + 'no-panel-heading' : 'card-footer'; + } + else { + // Bootstrap 3. + tmp = options.panel === false ? + 'no-panel-heading' : 'panel-heading'; + } + w.setFooter(w.footer); } @@ -47481,7 +47619,7 @@ if (!Array.prototype.indexOf) { 'built yet.'); } - // Value this.correctChoice can undefined, string or array. + // Value this.correctChoice can be undefined, string or array. // If no correct choice is set, we simply ignore the correct param. if (options.correct && this.correctChoice !== null) { @@ -47536,16 +47674,32 @@ if (!Array.prototype.indexOf) { } else { // How many random choices? - if (!this.selectMultiple) len = 1; - else len = J.randomInt(0, this.choicesCells.length); + len = 1; + if (this.selectMultiple) { + // Max random cells. + len = 'number' === typeof this.selectMultiple ? + this.selectMultiple : this.choicesCells.length; + // Min random cells. + tmp = this.requiredChoice; + len = J.randomInt('number' === typeof tmp ? (tmp-1) : 0, len); + } for ( ; ++i < len ; ) { - // This is the positional index. - j = J.randomInt(-1, (this.choicesCells.length-1)); - // If shuffled, we need to resolve it. - choice = this.shuffleChoices ? this.choicesValues[j] : j; + // This is the choice idx. + choice = J.randomInt(-1, (this.choicesCells.length-1)); + // Do not click it again if it is already selected. - if (!this.isChoiceCurrent(choice)) this.choicesCells[j].click(); + // Else increment len and try again (until 300 failsafe). + if (this.disabledChoices[choice] || + this.isChoiceCurrent(choice)) { + // Failsafe. + if (len < 300) len++; + } + else { + // Resolve to cell idx (might differ if shuffled). + j = this.choicesValues[choice]; + this.choicesCells[j].click(); + } } } @@ -50130,10 +50284,10 @@ if (!Array.prototype.indexOf) { 'requiredChoice are incompatible. Option ' + 'requiredChoice will be deprecated.'); } - this.required = this.requiredChoice = !!opts.required; + this.required = this.requiredChoice = !!opts.requiredChoice; } if ('undefined' === typeof this.required) { - this.required = this.requiredChoice = !!opts.required; + this.required = this.requiredChoice = false; } if (opts.userValidation) { @@ -52835,7 +52989,7 @@ if (!Array.prototype.indexOf) { /** * # DebugWall - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a wall where all incoming and outgoing messages are printed @@ -52850,7 +53004,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - DebugWall.version = '1.0.0'; + DebugWall.version = '1.1.0'; DebugWall.description = 'Intercepts incoming and outgoing messages, and ' + 'logs and prints them numbered and timestamped. Warning! Modifies ' + 'core functions, therefore its usage in production is ' + @@ -52950,7 +53104,7 @@ if (!Array.prototype.indexOf) { * * Initializes the instance * - * @param {object} options Optional. Configuration options + * @param {object} opts Optional. Configuration options * * - msgIn: If FALSE, incoming messages are ignored. * - msgOut: If FALSE, outgoing messages are ignored. @@ -52958,24 +53112,24 @@ if (!Array.prototype.indexOf) { * - hiddenTypes: An object containing what is currently hidden * in the wall. */ - DebugWall.prototype.init = function(options) { + DebugWall.prototype.init = function(opts) { var that; that = this; - if (options.msgIn !== false) { + if (opts.msgIn !== false) { this.origMsgInCb = node.socket.onMessage; node.socket.onMessage = function(msg) { that.write('in', that.makeTextIn(msg)); that.origMsgInCb.call(node.socket, msg); }; } - if (options.msgOut !== false) { + if (opts.msgOut !== false) { this.origMsgOutCb = node.socket.send; node.socket.send = function(msg) { that.write('out', that.makeTextOut(msg)); that.origMsgOutCb.call(node.socket, msg); }; } - if (options.log !== false) { + if (opts.log !== false) { this.origLogCb = node.log; node.log = function(txt, level, prefix) { that.write(level || 'info', @@ -52984,12 +53138,12 @@ if (!Array.prototype.indexOf) { }; } - if (options.hiddenTypes) { - if ('object' !== typeof hiddenTypes) { + if (opts.hiddenTypes) { + if ('object' !== typeof opts.hiddenTypes) { throw new TypeError('DebugWall.init: hiddenTypes must be ' + - 'object. Found: ' + hiddenTypes); + 'object. Found: ' + opts.hiddenTypes); } - this.hiddenTypes = hiddenTypes; + this.hiddenTypes = opts.hiddenTypes; } this.on('destroyed', function() { @@ -53002,37 +53156,76 @@ if (!Array.prototype.indexOf) { DebugWall.prototype.append = function() { var displayIn, displayOut, displayLog, that; - var btnGroup, cb, div; + var btnGroup, cb; + this.buttonsDiv = W.add('div', this.bodyDiv, { className: 'wallbuttonsdiv' }); - btnGroup = document.createElement('div'); - btnGroup.role = 'group'; - btnGroup['aria-label'] = 'Toggle visibility'; - btnGroup.className = 'btn-group'; + btnGroup = W.add('div', this.buttonsDiv, { + className: 'btn-group', + role: 'group', + 'aria-label': 'Toggle visibility of messages on wall' + }); - displayIn = W.add('button', btnGroup, { - innerHTML: 'Incoming', - className: 'btn btn-secondary' + // Incoming. + W.add('input', btnGroup, { + id: 'debug-wall-incoming', + // name: 'debug-wall-check', + className: 'btn-check', + autocomplete: "off", + checked: true, + type: 'checkbox' }); - displayOut = W.add('button', btnGroup, { - innerHTML: 'Outgoing', - className: 'btn btn-secondary' + displayIn = W.add('label', btnGroup, { + className: "btn btn-outline-primary", + 'for': "debug-wall-incoming", + innerHTML: 'Incoming' }); - displayLog = W.add('button', btnGroup, { - innerHTML: 'Log', - className: 'btn btn-secondary' + // Outgoing. + W.add('input', btnGroup, { + id: 'debug-wall-outgoing', + className: 'btn-check', + // name: 'debug-wall-check', + autocomplete: "off", + checked: true, + type: 'checkbox' + }); + displayOut = W.add('label', btnGroup, { + className: "btn btn-outline-primary", + 'for': "debug-wall-outgoing", + innerHTML: 'Outgoing' + }); + // Log. + W.add('input', btnGroup, { + id: 'debug-wall-log', + className: 'btn-check', + // name: 'debug-wall-check', + autocomplete: "off", + checked: true, + type: 'checkbox' + }); + displayLog = W.add('label', btnGroup, { + className: "btn btn-outline-primary", + 'for': "debug-wall-log", + innerHTML: 'Log' }); - - this.buttonsDiv.appendChild(btnGroup); that = this; + W.add('button', this.buttonsDiv, { + className: "btn btn-outline-danger me-2", + innerHTML: 'Clear' + }) + .onclick = function() { that.clear(); }; + + this.buttonsDiv.appendChild(btnGroup); + cb = function(type) { var items, i, vis, className; className = 'wall_' + type; items = that.wall.getElementsByClassName(className); + if (!items || !items.length) return; vis = items[0].style.display === '' ? 'none' : ''; for (i = 0; i < items.length; i++) { items[i].style.display = vis; @@ -53056,9 +53249,22 @@ if (!Array.prototype.indexOf) { * @param {string} type 'in', 'out', or 'log' (different levels) * @param {string} text The text to write */ - DebugWall.prototype.shouldHide = function(type, text) { + DebugWall.prototype.shouldHide = function(type) { return this.hiddenTypes[type]; }; + + /** + * ### DebugWall.write + * + * Writes argument as first entry of this.wall if document is fully loaded + * + * @param {string} type 'in', 'out', or 'log' (different levels) + * @param {string} text The text to write + */ + DebugWall.prototype.clear = function() { + this.wall.innerHTML = ''; + }; + /** * ### DebugWall.write * @@ -53147,7 +53353,7 @@ if (!Array.prototype.indexOf) { return text; }; - DebugWall.prototype.makeTextLog = function(text, level, prefix) { + DebugWall.prototype.makeTextLog = function(text) { return text; }; @@ -53631,7 +53837,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - EmailForm.version = '0.13.0'; + EmailForm.version = '0.13.1'; EmailForm.description = 'Displays a configurable email form.'; EmailForm.title = false; @@ -53921,10 +54127,9 @@ if (!Array.prototype.indexOf) { email: email, attempts: this.attempts, }; + if (opts.markAttempt) email.isCorrect = res; } - if (opts.markAttempt) email.isCorrect = res; - if (res === false) { if (opts.updateUI || opts.highlight) this.highlight(); this.timeInput = null; @@ -54450,8 +54655,15 @@ if (!Array.prototype.indexOf) { } preWin = ''; + if ('undefined' !== typeof data.basePay) { - preWin = data.basePay + ' + ' + data.bonus; + preWin = data.basePay; + + } + + if (data.showBonus !== false) { + if (preWin !== '') preWin += ' + '; + preWin += data.bonus; } if (data.partials) { @@ -54493,7 +54705,7 @@ if (!Array.prototype.indexOf) { } if (!err) { - totalWin = preWin + ' = ' + totalWin; + if (totalWin !== preWin) totalWin = preWin + ' = ' + totalWin; totalWin += ' ' + this.totalWinCurrency; } } @@ -58107,6 +58319,17 @@ if (!Array.prototype.indexOf) { } this.hoverColor = opts.hoverColor; } + + if ('undefined' !== typeof opts.correctValue) { + if (false === J.isNumber(opts.correctValue, + this.min, this.max, true, true)) { + + throw new Error(e + 'correctValue must be a number between ' + + this.min + ' and ' + this.max + '. Found: ' + + opts.correctValue); + } + this.correctValue = opts.correctValue; + } }; /** @@ -58287,7 +58510,7 @@ if (!Array.prototype.indexOf) { * * @see SVOGauge.init */ - function SVOGauge(options) { + function SVOGauge() { /** * ### SVOGauge.methods @@ -61310,7 +61533,7 @@ if (!Array.prototype.indexOf) { ul.className = 'dropdown-menu'; ul.style['text-align'] = 'left'; - var li, a, t, liT1, liT2; + var li, a, t, liT1, liT2, liT3; if (conf.availableTreatments) { li = document.createElement('li'); li.innerHTML = w.getText('gameTreatments'); @@ -61325,7 +61548,8 @@ if (!Array.prototype.indexOf) { a.innerHTML = '' + t + ': ' + conf.availableTreatments[t]; li.appendChild(a); - if (t === 'treatment_rotate') liT1 = li; + 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); } @@ -61340,6 +61564,7 @@ if (!Array.prototype.indexOf) { ul.appendChild(li); ul.appendChild(liT1); ul.appendChild(liT2); + ul.appendChild(liT3); } btnGroupTreatments.appendChild(btnTreatment); diff --git a/lib/core/ErrorManager.js b/lib/core/ErrorManager.js index c883b19a..af4d5680 100644 --- a/lib/core/ErrorManager.js +++ b/lib/core/ErrorManager.js @@ -50,12 +50,22 @@ that = this; if (!J.isNodeJS()) { window.onerror = function(msg, url, lineno, colno, error) { + var str; msg = node.game.getCurrentGameStage().toString() + '@' + J.getTime() + '> ' + url + ' ' + lineno + ',' + colno + ': ' + msg; if (error) msg + ' - ' + JSON.stringify(error); that.lastError = msg; node.err(msg); + if (node.debug) { + W.init({ waitScreen: true }); + str = 'DEBUG mode: client-side error ' + + 'detected.

'; + str += msg; + str += '

' + + 'This message will not be shown in production mode.'; + W.lockScreen(str); + } return !node.debug; }; } diff --git a/package.json b/package.json index fc745750..776e722a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "https://github.com/nodeGame/nodegame-client.git" }, "dependencies": { - "socket.io-client": "2.4.0", + "socket.io-client": "4.1.3", "commander": "^7.0.0", "JSUS": "^1.1.0", "NDDB": "^2.0.0", From 64acede0578eb6e86806ec1e7aad14dae54d6f51 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 10:23:38 +0200 Subject: [PATCH 29/51] built --- build/nodegame-full.js | 490 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 486 insertions(+), 4 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 3575c9c4..061ead5e 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -10978,7 +10978,7 @@ if (!Array.prototype.indexOf) { if (node.debug) { W.init({ waitScreen: true }); str = 'DEBUG mode: client-side error ' + - 'detected

'; + 'detected.

'; str += msg; str += '

' + 'This message will not be shown in production mode.'; @@ -41820,8 +41820,13 @@ if (!Array.prototype.indexOf) { if (options.highlighted || w._highlighted) w.highlight(); if (options.disabled || w._disabled) w.disable(); - // Make sure the distance from the right side is correct. - if (w.docked) setRightStyle(w); + if (w.docked) { + // Make sure the distance from the right side is correct. + setRightStyle(w); + } + else if (!w.isHidden() && !w.isCollapsed()) { + W.adjustFrameHeight(undefined, 150); + } // Store reference of last appended widget (.get method set storeRef). if (w.storeRef !== false) this.lastAppended = w; @@ -47058,7 +47063,7 @@ if (!Array.prototype.indexOf) { if (this.tabbable) J.makeTabbable(td); // Forces equal width. - if (this.sameWidthCells) { + if (this.sameWidthCells && this.orientation === 'H') { width = this.left ? 70 : 100; if (this.right) width = width - 30; width = width / (this.choicesSetSize || this.choices.length); @@ -55672,6 +55677,198 @@ if (!Array.prototype.indexOf) { })(node); +/** + * # GroupMalleability + * Copyright(c) 2021 Stefano Balietti + * MIT Licensed + * + * Displays an interface to measure users' perception of group malleability. + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('GroupMalleability', GroupMalleability); + + // ## Meta-data + + GroupMalleability.version = '0.1.0'; + GroupMalleability.description = 'Displays an interface to measure ' + + 'perception for group malleability.'; + + GroupMalleability.title = 'Group Malleability'; + GroupMalleability.className = 'group-malleability'; + + + var items = [ + '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.' + ]; + + var choices = [ 1,2,3,4,5,6,7 ]; + + var header = [ + 'Strongly Oppose', + 'Somewhat Oppose', + 'Slightly Oppose', + 'Neutral', + 'Slightly Favor', + 'Somewhat Favor', + 'Strongly Favor' + ]; + + GroupMalleability.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.' + }; + + // ## Dependencies + + GroupMalleability.dependencies = {}; + + /** + * ## GroupMalleability constructor + * + * Creates a new instance of GroupMalleability + * + * @param {object} options Optional. Configuration options + * which is forwarded to GroupMalleability.init. + * + * @see GroupMalleability.init + */ + function GroupMalleability() { + + /** + * ## GroupMalleability.ct + * + * The ChoiceTableGroup widget containing the items + */ + this.ctg = null; + + /** + * ## GroupMalleability.choices + * + * The numerical scale used + */ + this.choices = choices; + + /** + * ## GroupMalleability.header + * + * The categorical scale used + */ + this.header = header; + + /** + * ### GroupMalleability.mainText + * + * A text preceeding the GroupMalleability scale + */ + this.mainText = null; + } + + // ## GroupMalleability methods. + + /** + * ### GroupMalleability.init + * + * Initializes the widget + * + * @param {object} opts Optional. Configuration options. + */ + GroupMalleability.prototype.init = function(opts) { + opts = opts || {}; + + if (opts.choices) { + if (!J.isArray(opts.choices) || opts.choices.length < 2) { + throw new Error('GroupMalleability.init: choices must be an ' + + 'array of length > 1 or undefined. Found: ' + + opts.choices); + } + this.choices = opts.choices; + } + + if (opts.header) { + if (!J.isArray(opts.header) || + opts.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: ' + opts.header); + } + this.header = opts.header; + } + + if (opts.mainText) { + if ('string' !== typeof opts.mainText && opts.mainText !== false) { + throw new Error('GroupMalleability.init: mainText must be ' + + 'string, false, or undefined. Found: ' + + opts.mainText); + } + this.mainText = opts.mainText; + } + else if (opts.mainText !== false) { + this.mainText = this.getText('mainText'); + } + }; + + GroupMalleability.prototype.append = function() { + this.ctg = node.widgets.add('ChoiceTableGroup', this.panelDiv, { + id: this.id || 'groupmalleability_choicetable', + items: items.map(function(item, i) { + return [('GM_' + (i+1)), item ]; + }), + choices: this.choices, + mainText: this.mainText, + title: false, + panel: false, + requiredChoice: this.required, + header: this.header + }); + }; + + GroupMalleability.prototype.getValues = function(opts) { + opts = opts || {}; + return this.ctg.getValues(opts); + }; + + GroupMalleability.prototype.setValues = function(opts) { + opts = opts || {}; + return this.ctg.setValues(opts); + }; + + GroupMalleability.prototype.enable = function(opts) { + return this.ctg.enable(opts); + }; + + GroupMalleability.prototype.disable = function(opts) { + return this.ctg.disable(opts); + }; + + GroupMalleability.prototype.highlight = function(opts) { + return this.ctg.highlight(opts); + }; + + GroupMalleability.prototype.unhighlight = function(opts) { + return this.ctg.unhighlight(opts); + }; + +})(node); + /** * # LanguageSelector * Copyright(c) 2017 Stefano Balietti @@ -57937,6 +58134,291 @@ if (!Array.prototype.indexOf) { })(node); +/** + * # SDO + * Copyright(c) 2021 Stefano Balietti + * MIT Licensed + * + * Displays an interface to measure users' social dominance orientation (S.D.O.) + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('SDO', SDO); + + // ## Meta-data + + SDO.version = '0.3.0'; + SDO.description = 'Displays an interface to measure Social ' + + 'Dominance Orientation (S.D.O.).'; + + SDO.title = 'SDO'; + SDO.className = 'SDO'; + + + var scales = { + + SDO7: [ + // Dominance Sub-Scale. + '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.', + // Reverse-scored: + '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.', + + // Anti-Egalitarianism Sub-Scale. + '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.', + // Reverse-scored: + '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.' + ] + }; + + scales.SDO7s = [ + scales.SDO7[2], scales.SDO7[3], scales.SDO7[5], scales.SDO7[6], + scales.SDO7[11], scales.SDO7[10], scales.SDO7[13], scales.SDO7[12] + ]; + + // var choices = [ + // '1 ' + '
' + 'Strongly Oppose', + // '2 ' + '
' + 'Somewhat Oppose', + // '3 ' + '
' + 'Slightly Oppose', + // '4 ' + '
' + 'Neutral', + // '5 ' + '
' + 'Slightly Favor', + // '6 ' + '
' + 'Somewhat Favor', + // '7 ' + '
' + 'Strongly Favor' + // ]; + + var choices = [ 1,2,3,4,5,6,7 ]; + + var header = [ + 'Strongly Oppose', + 'Somewhat Oppose', + 'Slightly Oppose', + 'Neutral', + 'Slightly Favor', + 'Somewhat Favor', + 'Strongly Favor' + ]; + + SDO.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.', + }; + + // ## Dependencies + + SDO.dependencies = {}; + + /** + * ## SDO constructor + * + * Creates a new instance of SDO + * + * @param {object} options Optional. Configuration options + * which is forwarded to SDO.init. + * + * @see SDO.init + */ + function SDO() { + + /** + * ## SDO.sdo + * + * The ChoiceTableGroup widget containing the items + */ + this.sdo = null; + + /** + * ## SDO.scale + * + * The scale used to measure SDO + * + * Available methods: SDO16, SDO7, SDO7s (default). + * + * References: + * + * SDO7 + * Ho et al. (2015). "The nature of social dominance orientation: + * Theorizing and measuring preferences for intergroup inequality + * using the new SDO₇ scale". + * Journal of Personality and Social Psychology. 109 (6): 1003–1028. + * + * SDO16 + * Sidanius and Pratto (1999). Social Dominance: An Intergroup + * Theory of Social Hierarchy and Oppression. + * Cambridge: Cambridge University Press. + */ + this.scale = 'SDO7s'; + + /** + * ## SDO.choices + * + * The numerical scale used + */ + this.choices = choices; + + /** + * ## SDO.header + * + * The categorical scale used + */ + this.header = header; + + /** + * ### SDO.mainText + * + * A text preceeding the SDO scale + */ + this.mainText = null; + } + + // ## SDO methods. + + /** + * ### SDO.init + * + * Initializes the widget + * + * @param {object} opts Optional. Configuration options. + */ + SDO.prototype.init = function(opts) { + opts = opts || {}; + + if (opts.scale) { + if (opts.scale !== 'SDO16' && + opts.scale !== 'SDO7' && opts.scale !== 'SDO7s') { + + throw new Error('SDO.init: scale must be SDO16, SDO7, SDO7s ' + + 'or undefined. Found: ' + opts.scale); + } + + this.scale = opts.scale; + } + + if (opts.choices) { + if (!J.isArray(opts.choices) || opts.choices.length < 2) { + throw new Error('SDO.init: choices must be an array ' + + 'of length > 1 or undefined. Found: ' + + opts.choices); + } + this.choices = opts.choices; + } + + if (opts.header) { + if (!J.isArray(opts.header) || + opts.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: ' + opts.header); + } + this.header = opts.header; + } + + if (opts.mainText) { + if ('string' !== typeof opts.mainText && opts.mainText !== false) { + throw new Error('SDO.init: mainText must be string, ' + + 'false, or undefined. Found: ' + opts.mainText); + } + this.mainText = opts.mainText; + } + }; + + SDO.prototype.append = function() { + this.sdo = node.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: false, + panel: false, + requiredChoice: this.required, + header: this.header + }); + }; + + SDO.prototype.getItems = function() { + // E.g., ID: SDO7_1. + var s = this.scale; + return scales[s].map(function(item, idx) { + return [ s + '_' + (idx+1), item ]; + }); + }; + + SDO.prototype.getValues = function(opts) { + opts = opts || {}; + return this.sdo.getValues(opts); + }; + + SDO.prototype.setValues = function(opts) { + opts = opts || {}; + return this.sdo.setValues(opts); + }; + + SDO.prototype.enable = function(opts) { + return this.sdo.enable(opts); + }; + + SDO.prototype.disable = function(opts) { + return this.sdo.disable(opts); + }; + + SDO.prototype.highlight = function(opts) { + return this.sdo.highlight(opts); + }; + + SDO.prototype.unhighlight = function(opts) { + return this.sdo.unhighlight(opts); + }; + +})(node); + /** * # Slider * Copyright(c) 2020 Stefano Balietti From 74e2ea26a27e52f6f2aa1f6375bf0d2ecab19142 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 12:20:02 +0200 Subject: [PATCH 30/51] moved GETTING_DONE inside done method --- lib/core/Game.js | 3 ++- lib/modules/ssgd.js | 7 ++++++- listeners/internal.js | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/core/Game.js b/lib/core/Game.js index 31c30d2a..c5d11aa6 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1671,7 +1671,6 @@ if (this.paused) return false; stateLevel = this.getStateLevel(); - stageLevel = this.getStageLevel(); switch (stateLevel) { case constants.stateLevels.UNINITIALIZED: @@ -1684,6 +1683,8 @@ return false; case constants.stateLevels.PLAYING_STEP: + + stageLevel = this.getStageLevel(); switch (stageLevel) { case constants.stageLevels.EXECUTING_CALLBACK: case constants.stageLevels.CALLBACK_EXECUTED: diff --git a/lib/modules/ssgd.js b/lib/modules/ssgd.js index f03c4430..16520c6b 100644 --- a/lib/modules/ssgd.js +++ b/lib/modules/ssgd.js @@ -13,7 +13,8 @@ var NGC = parent.NodeGameClient; var J = parent.JSUS; - var GETTING_DONE = parent.constants.stageLevels.GETTING_DONE; + var stageLevels = parent.constants.stageLevels; + var GETTING_DONE = stageLevels.GETTING_DONE; /** * ### NodeGameClient.say @@ -383,6 +384,10 @@ this.set(o, 'SERVER', 'done'); } + // Prevents messages in reply to DONE, to be executed before + // the asyn stepping procedure starts. + this.setStageLevel(stageLevels.GETTING_DONE); + that = this; setTimeout(function() { that.events.emit('DONE', param); }, 0); diff --git a/listeners/internal.js b/listeners/internal.js index 821338a8..0bf24e15 100644 --- a/listeners/internal.js +++ b/listeners/internal.js @@ -47,7 +47,6 @@ function done() { var res; - node.game.setStageLevel(stageLevels.GETTING_DONE); node.game.willBeDone = false; node.game.beDone = false; node.emit('REALLY_DONE'); From d4a644134f86b2db140525ca1377e47adac6bb54 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 12:21:42 +0200 Subject: [PATCH 31/51] fix --- lib/modules/ssgd.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/ssgd.js b/lib/modules/ssgd.js index 16520c6b..490a73c7 100644 --- a/lib/modules/ssgd.js +++ b/lib/modules/ssgd.js @@ -386,7 +386,7 @@ // Prevents messages in reply to DONE, to be executed before // the asyn stepping procedure starts. - this.setStageLevel(stageLevels.GETTING_DONE); + this.game.setStageLevel(stageLevels.GETTING_DONE); that = this; setTimeout(function() { that.events.emit('DONE', param); }, 0); From cae46049d4dd47df694610aa578f0cd2bb387f32 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 12:30:52 +0200 Subject: [PATCH 32/51] simplified constants in Game; testing isReady --- build/nodegame-full.js | 11 ++-- lib/core/Game.js | 118 +++++++++++++++++++++-------------------- lib/core/Socket.js | 5 ++ lib/modules/ssgd.js | 2 +- 4 files changed, 74 insertions(+), 62 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 061ead5e..6adb36c2 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -26030,7 +26030,6 @@ if (!Array.prototype.indexOf) { if (this.paused) return false; stateLevel = this.getStateLevel(); - stageLevel = this.getStageLevel(); switch (stateLevel) { case constants.stateLevels.UNINITIALIZED: @@ -26043,6 +26042,8 @@ if (!Array.prototype.indexOf) { return false; case constants.stateLevels.PLAYING_STEP: + + stageLevel = this.getStageLevel(); switch (stageLevel) { case constants.stageLevels.EXECUTING_CALLBACK: case constants.stageLevels.CALLBACK_EXECUTED: @@ -31372,7 +31373,8 @@ if (!Array.prototype.indexOf) { var NGC = parent.NodeGameClient; var J = parent.JSUS; - var GETTING_DONE = parent.constants.stageLevels.GETTING_DONE; + var stageLevels = parent.constants.stageLevels; + var GETTING_DONE = stageLevels.GETTING_DONE; /** * ### NodeGameClient.say @@ -31742,6 +31744,10 @@ if (!Array.prototype.indexOf) { this.set(o, 'SERVER', 'done'); } + // Prevents messages in reply to DONE, to be executed before + // the asyn stepping procedure starts. + this.game.setStageLevel(stageLevels.GETTING_DONE); + that = this; setTimeout(function() { that.events.emit('DONE', param); }, 0); @@ -32619,7 +32625,6 @@ if (!Array.prototype.indexOf) { function done() { var res; - node.game.setStageLevel(stageLevels.GETTING_DONE); node.game.willBeDone = false; node.game.beDone = false; node.emit('REALLY_DONE'); diff --git a/lib/core/Game.js b/lib/core/Game.js index c5d11aa6..aeaebebd 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -26,6 +26,8 @@ J = parent.JSUS; var constants = parent.constants; + var stageLevels = constants.stageLevels; + var stateLevels = constants.stateLevels; /** * ## Game constructor @@ -39,8 +41,8 @@ this.node = node; // This updates are never published. - this.setStateLevel(constants.stateLevels.UNINITIALIZED, 'S'); - this.setStageLevel(constants.stageLevels.UNINITIALIZED, 'S'); + this.setStateLevel(stateLevels.UNINITIALIZED, 'S'); + this.setStageLevel(stageLevels.UNINITIALIZED, 'S'); // ## Properties @@ -193,7 +195,7 @@ // Setting to stage 0.0.0 and starting. this.setCurrentGameStage(new GameStage(), 'S'); - this.setStateLevel(constants.stateLevels.STARTING, 'S'); + this.setStateLevel(stateLevels.STARTING, 'S'); /** * ### Game.paused @@ -334,12 +336,12 @@ // INIT the game. onInit = this.plot.stager.getOnInit(); if (onInit) { - this.setStateLevel(constants.stateLevels.INITIALIZING); + this.setStateLevel(stateLevels.INITIALIZING); node.emit('INIT'); onInit.call(node.game); } - this.setStateLevel(constants.stateLevels.INITIALIZED); + this.setStateLevel(stateLevels.INITIALIZED); this.setCurrentGameStage(startStage, 'S'); @@ -398,8 +400,8 @@ if (node.window) node.window.reset(); // Update state/stage levels and game stage. - this.setStateLevel(constants.stateLevels.STARTING, 'S'); - this.setStageLevel(constants.stageLevels.UNINITIALIZED, 'S'); + this.setStateLevel(stateLevels.STARTING, 'S'); + this.setStageLevel(stageLevels.UNINITIALIZED, 'S'); // This command is notifying the server. this.setCurrentGameStage(new GameStage()); @@ -425,7 +427,7 @@ var onGameover, node; node = this.node; - if (this.getStateLevel() >= constants.stateLevels.FINISHING) { + if (this.getStateLevel() >= stateLevels.FINISHING) { node.warn('Game.gameover called on a finishing game.'); return; } @@ -435,12 +437,12 @@ // Call gameover callback, if it exists. onGameover = this.plot.stager.getOnGameover(); if (onGameover) { - this.setStateLevel(constants.stateLevels.FINISHING); + this.setStateLevel(stateLevels.FINISHING); onGameover.call(node.game); } - this.setStateLevel(constants.stateLevels.GAMEOVER); - this.setStageLevel(constants.stageLevels.DONE); + this.setStateLevel(stateLevels.GAMEOVER); + this.setStageLevel(stageLevels.DONE); node.log('game over.'); node.emit('GAME_OVER'); @@ -820,8 +822,8 @@ // Calling exit function of the step. if (curStepExitCb) { - this.setStateLevel(constants.stateLevels.STEP_EXIT); - this.setStageLevel(constants.stageLevels.EXITING); + this.setStateLevel(stateLevels.STEP_EXIT); + this.setStageLevel(stageLevels.EXITING); curStepExitCb.call(this); } @@ -845,8 +847,8 @@ // Calling exit function of the stage. // Note: stage.exit is not inherited. if (curStageObj && curStageObj.exit) { - this.setStateLevel(constants.stateLevels.STAGE_EXIT); - this.setStageLevel(constants.stageLevels.EXITING); + this.setStateLevel(stateLevels.STAGE_EXIT); + this.setStageLevel(stageLevels.EXITING); curStageObj.exit.call(this); } @@ -893,8 +895,8 @@ // Calling exit function. // Note: stage.exit is not inherited. if (curStageObj && curStageObj.exit) { - this.setStateLevel(constants.stateLevels.STAGE_EXIT); - this.setStageLevel(constants.stageLevels.EXITING); + this.setStateLevel(stateLevels.STAGE_EXIT); + this.setStageLevel(stageLevels.EXITING); curStageObj.exit.call(this); } @@ -908,7 +910,7 @@ // stageLevel needs to be changed (silent), otherwise it stays // DONE for a short time in the new game stage: - this.setStageLevel(constants.stageLevels.UNINITIALIZED, 'S'); + this.setStageLevel(stageLevels.UNINITIALIZED, 'S'); this.setCurrentGameStage(nextStep); // Process options before calling any init function. Sets a role also. @@ -957,8 +959,8 @@ // Clear the previous stage listeners. node.events.ee.stage.clear(); - this.setStateLevel(constants.stateLevels.STAGE_INIT); - this.setStageLevel(constants.stageLevels.INITIALIZING); + this.setStateLevel(stateLevels.STAGE_INIT); + this.setStageLevel(stageLevels.INITIALIZING); // Execute the init function of the stage, if any: // Note: this property is not inherited. @@ -976,13 +978,13 @@ // Execute the init function of the step, if any. if (stepInitCb) { - this.setStateLevel(constants.stateLevels.STEP_INIT); - this.setStageLevel(constants.stageLevels.INITIALIZING); + this.setStateLevel(stateLevels.STEP_INIT); + this.setStageLevel(stageLevels.INITIALIZING); stepInitCb.call(node.game); } - this.setStateLevel(constants.stateLevels.PLAYING_STEP); - this.setStageLevel(constants.stageLevels.INITIALIZED); + this.setStateLevel(stateLevels.PLAYING_STEP); + this.setStageLevel(stageLevels.INITIALIZED); // Updating the globals object. this.updateGlobals(nextStep); @@ -1308,7 +1310,7 @@ */ Game.prototype.execCallback = function(cb) { var res; - this.setStageLevel(constants.stageLevels.EXECUTING_CALLBACK); + this.setStageLevel(stageLevels.EXECUTING_CALLBACK); // Execute custom callback. Can throw errors. res = cb.call(this.node.game); @@ -1318,7 +1320,7 @@ 'of stage ' + this.getCurrentGameStage()); } - this.setStageLevel(constants.stageLevels.CALLBACK_EXECUTED); + this.setStageLevel(stageLevels.CALLBACK_EXECUTED); this.node.emit('STEP_CALLBACK_EXECUTED'); // Internal listeners will check whether we need to emit PLAYING. }; @@ -1447,13 +1449,13 @@ * * Returns the state of the nodeGame engine * - * The engine states are defined in `node.constants.stateLevels`, + * The engine states are defined in `node.stateLevels`, * and it is of the type: STAGE_INIT, PLAYING_STEP, GAMEOVER, etc. * The return value is a reference to `node.player.stateLevel`. * * @return {number} The state of the engine. * @see node.player.stateLevel - * @see node.constants.stateLevels + * @see node.stateLevels */ Game.prototype.getStateLevel = function() { return this.node.player.stateLevel; @@ -1466,7 +1468,7 @@ * * The value is actually stored in `node.player.stateLevel`. * - * Stage levels are defined in `node.constants.stageLevels`, for example: + * Stage levels are defined in `node.stageLevels`, for example: * STAGE_INIT, PLAYING_STEP, GAMEOVER, etc. * * By default, it does not send the update to the server if the @@ -1479,7 +1481,7 @@ * behavior ('F' = force, 'S' = silent'). * * @see Game.publishUpdate - * @see node.constants.stageLevels + * @see node.stageLevels */ Game.prototype.setStateLevel = function(stateLevel, mod) { var node; @@ -1502,13 +1504,13 @@ * * Return the execution level of the current game stage * - * The execution level is defined in `node.constants.stageLevels`, + * The execution level is defined in `node.stageLevels`, * and it is of the type INITIALIZED, CALLBACK_EXECUTED, etc. * The return value is a reference to `node.player.stageLevel`. * * @return {number} The level of the stage execution. * @see node.player.stageLevel - * @see node.constants.stageLevels + * @see node.stageLevels */ Game.prototype.getStageLevel = function() { return this.node.player.stageLevel; @@ -1521,7 +1523,7 @@ * * The value is actually stored in `node.player.stageLevel`. * - * Stage levels are defined in `node.constants.stageLevels`, for example: + * Stage levels are defined in `node.stageLevels`, for example: * PLAYING, DONE, etc. * * By default, it does not send the update to the server if the @@ -1534,7 +1536,7 @@ * behavior ('F' = force, 'S' = silent'). * * @see Game.publishUpdate - * @see node.constants.stageLevels + * @see node.stageLevels */ Game.prototype.setStageLevel = function(stageLevel, mod) { var node; @@ -1614,7 +1616,7 @@ myStage = this.getCurrentGameStage(); levels = constants.publishLevels; - stageLevels = constants.stageLevels; + stageLevels = stageLevels; myPublishLevel = this.plot.getProperty(myStage, 'publishLevel'); @@ -1658,12 +1660,12 @@ * Returns TRUE if a game is set and interactive * * A game is ready unless a stage or step is currently being - * loaded or DONE procedure has been started, i.e. between the + * loaded or a DONE procedure has been started, i.e. between the * stage levels: PLAYING and GETTING_DONE. * * If a game is paused, it is also NOT ready. * - * @see node.constants.stageLevels + * @see node.stageLevels */ Game.prototype.isReady = function() { var stageLevel, stateLevel; @@ -1673,24 +1675,24 @@ stateLevel = this.getStateLevel(); switch (stateLevel) { - case constants.stateLevels.UNINITIALIZED: - case constants.stateLevels.INITIALIZING: - case constants.stateLevels.STAGE_INIT: - case constants.stateLevels.STEP_INIT: - case constants.stateLevels.FINISHING: - case constants.stateLevels.STAGE_EXIT: - case constants.stateLevels.STEP_EXIT: + case stateLevels.UNINITIALIZED: + case stateLevels.INITIALIZING: + case stateLevels.STAGE_INIT: + case stateLevels.STEP_INIT: + case stateLevels.FINISHING: + case stateLevels.STAGE_EXIT: + case stateLevels.STEP_EXIT: return false; - case constants.stateLevels.PLAYING_STEP: + case stateLevels.PLAYING_STEP: stageLevel = this.getStageLevel(); switch (stageLevel) { - case constants.stageLevels.EXECUTING_CALLBACK: - case constants.stageLevels.CALLBACK_EXECUTED: - case constants.stageLevels.PAUSING: - case constants.stageLevels.RESUMING: - case constants.stageLevels.GETTING_DONE: + case stageLevels.EXECUTING_CALLBACK: + case stageLevels.CALLBACK_EXECUTED: + case stageLevels.PAUSING: + case stageLevels.RESUMING: + case stageLevels.GETTING_DONE: return false; } break; @@ -1707,7 +1709,7 @@ */ Game.prototype.isStartable = function() { return this.plot.isReady() && - this.getStateLevel() < constants.stateLevels.INITIALIZING; + this.getStateLevel() < stateLevels.INITIALIZING; }; @@ -1719,7 +1721,7 @@ * @return {boolean} TRUE if the game can be stopped. */ Game.prototype.isStoppable = function() { - return this.getStateLevel() > constants.stateLevels.INITIALIZING; + return this.getStateLevel() > stateLevels.INITIALIZING; }; @@ -1732,7 +1734,7 @@ */ Game.prototype.isPausable = function() { return !this.paused && - this.getStateLevel() > constants.stateLevels.INITIALIZING; + this.getStateLevel() > stateLevels.INITIALIZING; }; @@ -1745,7 +1747,7 @@ */ Game.prototype.isResumable = function() { return this.paused && - this.getStateLevel() > constants.stateLevels.INITIALIZING; + this.getStateLevel() > stateLevels.INITIALIZING; }; @@ -1760,8 +1762,8 @@ var stateLevel; stateLevel = this.getStateLevel(); - return stateLevel > constants.stateLevels.INITIALIZING && - stateLevel < constants.stateLevels.FINISHING; + return stateLevel > stateLevels.INITIALIZING && + stateLevel < stateLevels.FINISHING; }; /** @@ -1772,7 +1774,7 @@ * @return {boolean} TRUE if is game over */ Game.prototype.isGameover = Game.prototype.isGameOver = function() { - return this.getStateLevel() === constants.stateLevels.GAMEOVER; + return this.getStateLevel() === stateLevels.GAMEOVER; }; /** @@ -1800,7 +1802,7 @@ if ('undefined' === typeof strict || strict) { // Should emit PLAYING only after LOADED. curStageLevel = this.getStageLevel(); - if (curStageLevel !== constants.stageLevels.LOADED) return false; + if (curStageLevel !== stageLevels.LOADED) return false; } node = this.node; curGameStage = this.getCurrentGameStage(); diff --git a/lib/core/Socket.js b/lib/core/Socket.js index 2f9a95d5..140e0bf0 100644 --- a/lib/core/Socket.js +++ b/lib/core/Socket.js @@ -561,6 +561,11 @@ msg = this.validateIncomingMsg(msg); if (!msg) return; + console.log('---------------------') + console.log(this.node.game.getStateLevel()) + console.log(this.node.game.getStageLevel()) + console.log('---------------------') + // Message with high priority are executed immediately. if (msg.priority > 0 || this.node.game.isReady()) { this.node.emit(msg.toInEvent(), msg); diff --git a/lib/modules/ssgd.js b/lib/modules/ssgd.js index 490a73c7..709a703b 100644 --- a/lib/modules/ssgd.js +++ b/lib/modules/ssgd.js @@ -385,7 +385,7 @@ } // Prevents messages in reply to DONE, to be executed before - // the asyn stepping procedure starts. + // the async stepping procedure starts. this.game.setStageLevel(stageLevels.GETTING_DONE); that = this; From ad17b33474d8d5997fb639be48586456de5bd46a Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 12:33:58 +0200 Subject: [PATCH 33/51] fix --- lib/core/Game.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/Game.js b/lib/core/Game.js index aeaebebd..4c174098 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1616,7 +1616,6 @@ myStage = this.getCurrentGameStage(); levels = constants.publishLevels; - stageLevels = stageLevels; myPublishLevel = this.plot.getProperty(myStage, 'publishLevel'); From 79fc94aa0502fbf6d02a2bd3e3dfab9fb1235a03 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 12:35:23 +0200 Subject: [PATCH 34/51] fix --- lib/core/Game.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/core/Game.js b/lib/core/Game.js index 4c174098..b18c5410 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1607,8 +1607,7 @@ * @return {boolean} TRUE, if the update should be sent */ Game.prototype.shouldPublishUpdate = function(type, value) { - var myStage; - var levels, myPublishLevel, stageLevels; + var myStage, levels, myPublishLevel; if ('string' !== typeof type) { throw new TypeError( 'Game.shouldPublishUpdate: type must be string.'); From ce458bc8dfe935fa0b4fec41994c7a2287d4cee2 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 13:19:46 +0200 Subject: [PATCH 35/51] When DONE messages are buffered --- build/nodegame-full.js | 128 +++++++++++++++++++++-------------------- lib/core/Game.js | 1 + 2 files changed, 68 insertions(+), 61 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 6adb36c2..5e011718 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -20650,6 +20650,11 @@ if (!Array.prototype.indexOf) { msg = this.validateIncomingMsg(msg); if (!msg) return; + console.log('---------------------') + console.log(this.node.game.getStateLevel()) + console.log(this.node.game.getStageLevel()) + console.log('---------------------') + // Message with high priority are executed immediately. if (msg.priority > 0 || this.node.game.isReady()) { this.node.emit(msg.toInEvent(), msg); @@ -24385,6 +24390,8 @@ if (!Array.prototype.indexOf) { J = parent.JSUS; var constants = parent.constants; + var stageLevels = constants.stageLevels; + var stateLevels = constants.stateLevels; /** * ## Game constructor @@ -24398,8 +24405,8 @@ if (!Array.prototype.indexOf) { this.node = node; // This updates are never published. - this.setStateLevel(constants.stateLevels.UNINITIALIZED, 'S'); - this.setStageLevel(constants.stageLevels.UNINITIALIZED, 'S'); + this.setStateLevel(stateLevels.UNINITIALIZED, 'S'); + this.setStageLevel(stageLevels.UNINITIALIZED, 'S'); // ## Properties @@ -24552,7 +24559,7 @@ if (!Array.prototype.indexOf) { // Setting to stage 0.0.0 and starting. this.setCurrentGameStage(new GameStage(), 'S'); - this.setStateLevel(constants.stateLevels.STARTING, 'S'); + this.setStateLevel(stateLevels.STARTING, 'S'); /** * ### Game.paused @@ -24693,12 +24700,12 @@ if (!Array.prototype.indexOf) { // INIT the game. onInit = this.plot.stager.getOnInit(); if (onInit) { - this.setStateLevel(constants.stateLevels.INITIALIZING); + this.setStateLevel(stateLevels.INITIALIZING); node.emit('INIT'); onInit.call(node.game); } - this.setStateLevel(constants.stateLevels.INITIALIZED); + this.setStateLevel(stateLevels.INITIALIZED); this.setCurrentGameStage(startStage, 'S'); @@ -24757,8 +24764,8 @@ if (!Array.prototype.indexOf) { if (node.window) node.window.reset(); // Update state/stage levels and game stage. - this.setStateLevel(constants.stateLevels.STARTING, 'S'); - this.setStageLevel(constants.stageLevels.UNINITIALIZED, 'S'); + this.setStateLevel(stateLevels.STARTING, 'S'); + this.setStageLevel(stageLevels.UNINITIALIZED, 'S'); // This command is notifying the server. this.setCurrentGameStage(new GameStage()); @@ -24784,7 +24791,7 @@ if (!Array.prototype.indexOf) { var onGameover, node; node = this.node; - if (this.getStateLevel() >= constants.stateLevels.FINISHING) { + if (this.getStateLevel() >= stateLevels.FINISHING) { node.warn('Game.gameover called on a finishing game.'); return; } @@ -24794,12 +24801,12 @@ if (!Array.prototype.indexOf) { // Call gameover callback, if it exists. onGameover = this.plot.stager.getOnGameover(); if (onGameover) { - this.setStateLevel(constants.stateLevels.FINISHING); + this.setStateLevel(stateLevels.FINISHING); onGameover.call(node.game); } - this.setStateLevel(constants.stateLevels.GAMEOVER); - this.setStageLevel(constants.stageLevels.DONE); + this.setStateLevel(stateLevels.GAMEOVER); + this.setStageLevel(stageLevels.DONE); node.log('game over.'); node.emit('GAME_OVER'); @@ -25179,8 +25186,8 @@ if (!Array.prototype.indexOf) { // Calling exit function of the step. if (curStepExitCb) { - this.setStateLevel(constants.stateLevels.STEP_EXIT); - this.setStageLevel(constants.stageLevels.EXITING); + this.setStateLevel(stateLevels.STEP_EXIT); + this.setStageLevel(stageLevels.EXITING); curStepExitCb.call(this); } @@ -25204,8 +25211,8 @@ if (!Array.prototype.indexOf) { // Calling exit function of the stage. // Note: stage.exit is not inherited. if (curStageObj && curStageObj.exit) { - this.setStateLevel(constants.stateLevels.STAGE_EXIT); - this.setStageLevel(constants.stageLevels.EXITING); + this.setStateLevel(stateLevels.STAGE_EXIT); + this.setStageLevel(stageLevels.EXITING); curStageObj.exit.call(this); } @@ -25252,8 +25259,8 @@ if (!Array.prototype.indexOf) { // Calling exit function. // Note: stage.exit is not inherited. if (curStageObj && curStageObj.exit) { - this.setStateLevel(constants.stateLevels.STAGE_EXIT); - this.setStageLevel(constants.stageLevels.EXITING); + this.setStateLevel(stateLevels.STAGE_EXIT); + this.setStageLevel(stageLevels.EXITING); curStageObj.exit.call(this); } @@ -25267,7 +25274,7 @@ if (!Array.prototype.indexOf) { // stageLevel needs to be changed (silent), otherwise it stays // DONE for a short time in the new game stage: - this.setStageLevel(constants.stageLevels.UNINITIALIZED, 'S'); + this.setStageLevel(stageLevels.UNINITIALIZED, 'S'); this.setCurrentGameStage(nextStep); // Process options before calling any init function. Sets a role also. @@ -25316,8 +25323,8 @@ if (!Array.prototype.indexOf) { // Clear the previous stage listeners. node.events.ee.stage.clear(); - this.setStateLevel(constants.stateLevels.STAGE_INIT); - this.setStageLevel(constants.stageLevels.INITIALIZING); + this.setStateLevel(stateLevels.STAGE_INIT); + this.setStageLevel(stageLevels.INITIALIZING); // Execute the init function of the stage, if any: // Note: this property is not inherited. @@ -25335,13 +25342,13 @@ if (!Array.prototype.indexOf) { // Execute the init function of the step, if any. if (stepInitCb) { - this.setStateLevel(constants.stateLevels.STEP_INIT); - this.setStageLevel(constants.stageLevels.INITIALIZING); + this.setStateLevel(stateLevels.STEP_INIT); + this.setStageLevel(stageLevels.INITIALIZING); stepInitCb.call(node.game); } - this.setStateLevel(constants.stateLevels.PLAYING_STEP); - this.setStageLevel(constants.stageLevels.INITIALIZED); + this.setStateLevel(stateLevels.PLAYING_STEP); + this.setStageLevel(stageLevels.INITIALIZED); // Updating the globals object. this.updateGlobals(nextStep); @@ -25667,7 +25674,7 @@ if (!Array.prototype.indexOf) { */ Game.prototype.execCallback = function(cb) { var res; - this.setStageLevel(constants.stageLevels.EXECUTING_CALLBACK); + this.setStageLevel(stageLevels.EXECUTING_CALLBACK); // Execute custom callback. Can throw errors. res = cb.call(this.node.game); @@ -25677,7 +25684,7 @@ if (!Array.prototype.indexOf) { 'of stage ' + this.getCurrentGameStage()); } - this.setStageLevel(constants.stageLevels.CALLBACK_EXECUTED); + this.setStageLevel(stageLevels.CALLBACK_EXECUTED); this.node.emit('STEP_CALLBACK_EXECUTED'); // Internal listeners will check whether we need to emit PLAYING. }; @@ -25806,13 +25813,13 @@ if (!Array.prototype.indexOf) { * * Returns the state of the nodeGame engine * - * The engine states are defined in `node.constants.stateLevels`, + * The engine states are defined in `node.stateLevels`, * and it is of the type: STAGE_INIT, PLAYING_STEP, GAMEOVER, etc. * The return value is a reference to `node.player.stateLevel`. * * @return {number} The state of the engine. * @see node.player.stateLevel - * @see node.constants.stateLevels + * @see node.stateLevels */ Game.prototype.getStateLevel = function() { return this.node.player.stateLevel; @@ -25825,7 +25832,7 @@ if (!Array.prototype.indexOf) { * * The value is actually stored in `node.player.stateLevel`. * - * Stage levels are defined in `node.constants.stageLevels`, for example: + * Stage levels are defined in `node.stageLevels`, for example: * STAGE_INIT, PLAYING_STEP, GAMEOVER, etc. * * By default, it does not send the update to the server if the @@ -25838,7 +25845,7 @@ if (!Array.prototype.indexOf) { * behavior ('F' = force, 'S' = silent'). * * @see Game.publishUpdate - * @see node.constants.stageLevels + * @see node.stageLevels */ Game.prototype.setStateLevel = function(stateLevel, mod) { var node; @@ -25861,13 +25868,13 @@ if (!Array.prototype.indexOf) { * * Return the execution level of the current game stage * - * The execution level is defined in `node.constants.stageLevels`, + * The execution level is defined in `node.stageLevels`, * and it is of the type INITIALIZED, CALLBACK_EXECUTED, etc. * The return value is a reference to `node.player.stageLevel`. * * @return {number} The level of the stage execution. * @see node.player.stageLevel - * @see node.constants.stageLevels + * @see node.stageLevels */ Game.prototype.getStageLevel = function() { return this.node.player.stageLevel; @@ -25880,7 +25887,7 @@ if (!Array.prototype.indexOf) { * * The value is actually stored in `node.player.stageLevel`. * - * Stage levels are defined in `node.constants.stageLevels`, for example: + * Stage levels are defined in `node.stageLevels`, for example: * PLAYING, DONE, etc. * * By default, it does not send the update to the server if the @@ -25893,7 +25900,7 @@ if (!Array.prototype.indexOf) { * behavior ('F' = force, 'S' = silent'). * * @see Game.publishUpdate - * @see node.constants.stageLevels + * @see node.stageLevels */ Game.prototype.setStageLevel = function(stageLevel, mod) { var node; @@ -25964,8 +25971,7 @@ if (!Array.prototype.indexOf) { * @return {boolean} TRUE, if the update should be sent */ Game.prototype.shouldPublishUpdate = function(type, value) { - var myStage; - var levels, myPublishLevel, stageLevels; + var myStage, levels, myPublishLevel; if ('string' !== typeof type) { throw new TypeError( 'Game.shouldPublishUpdate: type must be string.'); @@ -25973,7 +25979,6 @@ if (!Array.prototype.indexOf) { myStage = this.getCurrentGameStage(); levels = constants.publishLevels; - stageLevels = constants.stageLevels; myPublishLevel = this.plot.getProperty(myStage, 'publishLevel'); @@ -26017,12 +26022,12 @@ if (!Array.prototype.indexOf) { * Returns TRUE if a game is set and interactive * * A game is ready unless a stage or step is currently being - * loaded or DONE procedure has been started, i.e. between the + * loaded or a DONE procedure has been started, i.e. between the * stage levels: PLAYING and GETTING_DONE. * * If a game is paused, it is also NOT ready. * - * @see node.constants.stageLevels + * @see node.stageLevels */ Game.prototype.isReady = function() { var stageLevel, stateLevel; @@ -26032,24 +26037,25 @@ if (!Array.prototype.indexOf) { stateLevel = this.getStateLevel(); switch (stateLevel) { - case constants.stateLevels.UNINITIALIZED: - case constants.stateLevels.INITIALIZING: - case constants.stateLevels.STAGE_INIT: - case constants.stateLevels.STEP_INIT: - case constants.stateLevels.FINISHING: - case constants.stateLevels.STAGE_EXIT: - case constants.stateLevels.STEP_EXIT: + case stateLevels.UNINITIALIZED: + case stateLevels.INITIALIZING: + case stateLevels.STAGE_INIT: + case stateLevels.STEP_INIT: + case stateLevels.FINISHING: + case stateLevels.STAGE_EXIT: + case stateLevels.STEP_EXIT: return false; - case constants.stateLevels.PLAYING_STEP: + case stateLevels.PLAYING_STEP: stageLevel = this.getStageLevel(); switch (stageLevel) { - case constants.stageLevels.EXECUTING_CALLBACK: - case constants.stageLevels.CALLBACK_EXECUTED: - case constants.stageLevels.PAUSING: - case constants.stageLevels.RESUMING: - case constants.stageLevels.GETTING_DONE: + case stageLevels.EXECUTING_CALLBACK: + case stageLevels.CALLBACK_EXECUTED: + case stageLevels.PAUSING: + case stageLevels.RESUMING: + case stageLevels.GETTING_DONE: + case stageLevels.DONE: return false; } break; @@ -26066,7 +26072,7 @@ if (!Array.prototype.indexOf) { */ Game.prototype.isStartable = function() { return this.plot.isReady() && - this.getStateLevel() < constants.stateLevels.INITIALIZING; + this.getStateLevel() < stateLevels.INITIALIZING; }; @@ -26078,7 +26084,7 @@ if (!Array.prototype.indexOf) { * @return {boolean} TRUE if the game can be stopped. */ Game.prototype.isStoppable = function() { - return this.getStateLevel() > constants.stateLevels.INITIALIZING; + return this.getStateLevel() > stateLevels.INITIALIZING; }; @@ -26091,7 +26097,7 @@ if (!Array.prototype.indexOf) { */ Game.prototype.isPausable = function() { return !this.paused && - this.getStateLevel() > constants.stateLevels.INITIALIZING; + this.getStateLevel() > stateLevels.INITIALIZING; }; @@ -26104,7 +26110,7 @@ if (!Array.prototype.indexOf) { */ Game.prototype.isResumable = function() { return this.paused && - this.getStateLevel() > constants.stateLevels.INITIALIZING; + this.getStateLevel() > stateLevels.INITIALIZING; }; @@ -26119,8 +26125,8 @@ if (!Array.prototype.indexOf) { var stateLevel; stateLevel = this.getStateLevel(); - return stateLevel > constants.stateLevels.INITIALIZING && - stateLevel < constants.stateLevels.FINISHING; + return stateLevel > stateLevels.INITIALIZING && + stateLevel < stateLevels.FINISHING; }; /** @@ -26131,7 +26137,7 @@ if (!Array.prototype.indexOf) { * @return {boolean} TRUE if is game over */ Game.prototype.isGameover = Game.prototype.isGameOver = function() { - return this.getStateLevel() === constants.stateLevels.GAMEOVER; + return this.getStateLevel() === stateLevels.GAMEOVER; }; /** @@ -26159,7 +26165,7 @@ if (!Array.prototype.indexOf) { if ('undefined' === typeof strict || strict) { // Should emit PLAYING only after LOADED. curStageLevel = this.getStageLevel(); - if (curStageLevel !== constants.stageLevels.LOADED) return false; + if (curStageLevel !== stageLevels.LOADED) return false; } node = this.node; curGameStage = this.getCurrentGameStage(); @@ -31745,7 +31751,7 @@ if (!Array.prototype.indexOf) { } // Prevents messages in reply to DONE, to be executed before - // the asyn stepping procedure starts. + // the async stepping procedure starts. this.game.setStageLevel(stageLevels.GETTING_DONE); that = this; diff --git a/lib/core/Game.js b/lib/core/Game.js index b18c5410..a122102e 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1691,6 +1691,7 @@ case stageLevels.PAUSING: case stageLevels.RESUMING: case stageLevels.GETTING_DONE: + case stageLevels.DONE: return false; } break; From 9c0187b918fa88aa896f00ec3bfe55fe5f3ef8e3 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 13:37:00 +0200 Subject: [PATCH 36/51] removed-debug --- build/nodegame-full.js | 5 ----- lib/core/Socket.js | 5 ----- 2 files changed, 10 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 5e011718..02f18978 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -20650,11 +20650,6 @@ if (!Array.prototype.indexOf) { msg = this.validateIncomingMsg(msg); if (!msg) return; - console.log('---------------------') - console.log(this.node.game.getStateLevel()) - console.log(this.node.game.getStageLevel()) - console.log('---------------------') - // Message with high priority are executed immediately. if (msg.priority > 0 || this.node.game.isReady()) { this.node.emit(msg.toInEvent(), msg); diff --git a/lib/core/Socket.js b/lib/core/Socket.js index 140e0bf0..2f9a95d5 100644 --- a/lib/core/Socket.js +++ b/lib/core/Socket.js @@ -561,11 +561,6 @@ msg = this.validateIncomingMsg(msg); if (!msg) return; - console.log('---------------------') - console.log(this.node.game.getStateLevel()) - console.log(this.node.game.getStageLevel()) - console.log('---------------------') - // Message with high priority are executed immediately. if (msg.priority > 0 || this.node.game.isReady()) { this.node.emit(msg.toInEvent(), msg); From 550f29a29e3376887ffe4f47479224f31acf7a78 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 13:53:18 +0200 Subject: [PATCH 37/51] undo: DONE does not block isReady --- lib/core/Game.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/core/Game.js b/lib/core/Game.js index a122102e..bdf14e90 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1691,7 +1691,8 @@ case stageLevels.PAUSING: case stageLevels.RESUMING: case stageLevels.GETTING_DONE: - case stageLevels.DONE: + // TODO: should this be commented? See issue #168 + // case stageLevels.DONE: return false; } break; From 253ed80898568f401243baffb9366f37e9bc7c22 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 27 Aug 2021 13:53:40 +0200 Subject: [PATCH 38/51] built --- build/nodegame-full.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 02f18978..e7550268 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -26050,7 +26050,8 @@ if (!Array.prototype.indexOf) { case stageLevels.PAUSING: case stageLevels.RESUMING: case stageLevels.GETTING_DONE: - case stageLevels.DONE: + // TODO: should this be commented? See issue #168 + // case stageLevels.DONE: return false; } break; From c80fd25eb64b727e23c15e94ccaa1f6aff522333 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 6 Sep 2021 10:24:29 +0200 Subject: [PATCH 39/51] cleanup in matcher --- lib/matcher/Matcher.js | 114 ---------------------------------- lib/matcher/MatcherManager.js | 3 - 2 files changed, 117 deletions(-) diff --git a/lib/matcher/Matcher.js b/lib/matcher/Matcher.js index 797e9fa1..cc6054b1 100644 --- a/lib/matcher/Matcher.js +++ b/lib/matcher/Matcher.js @@ -1037,120 +1037,6 @@ * * @return {array} matches The matches according to the algorithm */ - function pairMatcherOld(alg, n, options) { - var ps, matches, bye; - var i, lenI, j, lenJ, jj; - var id1, id2; - var roundsLimit, cycle, cycleI, skipBye; - - if ('number' === typeof n && n > 1) { - ps = J.seq(0, (n-1)); - } - else if (J.isArray(n) && n.length > 1) { - ps = n.slice(); - n = ps.length; - } - else { - throw new TypeError('pairMatcher.' + alg + ': n must be ' + - 'number > 1 or array of length > 1.'); - } - options = options || {}; - - bye = 'undefined' !== typeof options.bye ? options.bye : -1; - skipBye = options.skipBye || false; - - // Make sure we have even numbers. - if ((n % 2) === 1) { - ps.push(bye); - n += 1; - } - - // Limit rounds. - if ('number' === typeof options.rounds) { - if (options.rounds <= 0) { - throw new Error('pairMatcher.' + alg + ': options.rounds ' + - 'must be a positive number or undefined. ' + - 'Found: ' + options.rounds); - } - if (options.rounds > (n-1)) { - throw new Error('pairMatcher.' + alg + ': ' + - 'options.rounds cannot be greater than ' + - (n-1) + '. Found: ' + options.rounds); - } - // Here roundsLimit does not depend on n (must be smaller). - roundsLimit = options.rounds; - } - else { - roundsLimit = n-1; - } - - if ('undefined' !== typeof options.cycle) { - cycle = options.cycle; - if (cycle !== 'mirror_invert' && cycle !== 'mirror' && - cycle !== 'repeat_invert' && cycle !== 'repeat') { - - throw new Error('pairMatcher.' + alg + ': options.cycle ' + - 'must be equal to "mirror"/"mirror_invert", ' + - '"repeat"/"repeat_invert" or undefined . ' + - 'Found: ' + options.cycle); - } - - matches = new Array(roundsLimit*2); - } - else { - matches = new Array(roundsLimit); - } - - i = -1, lenI = roundsLimit; - for ( ; ++i < lenI ; ) { - // Shuffle list of ids for random. - if (alg === 'random') ps = J.shuffle(ps); - // Create a new array for round i. - lenJ = n / 2; - matches[i] = skipBye ? new Array(lenJ-1) : new Array(lenJ); - // Check if new need to cycle. - if (cycle) { - if (cycle === 'mirror' || cycle === 'mirror_invert') { - cycleI = (roundsLimit*2) -i -1; - } - else { - cycleI = i+roundsLimit; - } - matches[cycleI] = skipBye ? - new Array(lenJ-1) : new Array(lenJ); - } - // Counter jj is updated only if not skipBye, - // otherwise we create holes in the matches array. - jj = j = -1; - for ( ; ++j < lenJ ; ) { - id1 = ps[j]; - id2 = ps[n - 1 - j]; - if (!skipBye || (id1 !== bye && id2 !== bye)) { - jj++; - // Insert match. - matches[i][jj] = [ id1, id2 ]; - // Insert cycle match (if any). - if (cycle === 'repeat') { - matches[cycleI][jj] = [ id1, id2 ]; - } - else if (cycle === 'repeat_invert') { - matches[cycleI][jj] = [ id2, id1 ]; - } - else if (cycle === 'mirror') { - matches[cycleI][jj] = [ id1, id2 ]; - } - else if (cycle === 'mirror_invert') { - matches[cycleI][jj] = [ id2, id1 ]; - } - } - } - // Permutate for next round. - ps.splice(1, 0, ps.pop()); - } - return matches; - } - - function pairMatcher(alg, n, options) { var ps, matches, bye; var i, lenI, j, lenJ, jj; diff --git a/lib/matcher/MatcherManager.js b/lib/matcher/MatcherManager.js index b851bbf7..9b7f5090 100644 --- a/lib/matcher/MatcherManager.js +++ b/lib/matcher/MatcherManager.js @@ -12,9 +12,6 @@ "use strict"; - // ## Global scope - var J = parent.JSUS; - exports.MatcherManager = MatcherManager; /** From c7550b13300010e231df6c921b4912ed9112dc59 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 6 Sep 2021 10:24:49 +0200 Subject: [PATCH 40/51] improved error msg ErrorManager --- lib/core/ErrorManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/core/ErrorManager.js b/lib/core/ErrorManager.js index af4d5680..a18df4b9 100644 --- a/lib/core/ErrorManager.js +++ b/lib/core/ErrorManager.js @@ -62,7 +62,8 @@ str = 'DEBUG mode: client-side error ' + 'detected.

'; str += msg; - str += '

' + + str += '

Open the DevTools in your browser ' + + 'for details.
' + 'This message will not be shown in production mode.'; W.lockScreen(str); } From f7f961a8c150e0278bca2b10d96bfd8f6fbab2df Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 6 Sep 2021 10:25:42 +0200 Subject: [PATCH 41/51] widgetStep add to options widgetStep = true, so that widgets know about it --- lib/core/Game.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/core/Game.js b/lib/core/Game.js index bdf14e90..54f4c12c 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1068,6 +1068,7 @@ if (!widget.options.className) { widget.options.className = 'centered'; } + widget.options.widgetStep = true; // Default id 'container' (as in default.html). if ('string' === typeof widget.root) { From d145d9e8d520c474a8048b2b69c42e2665638b21 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 6 Sep 2021 10:26:02 +0200 Subject: [PATCH 42/51] Timer.isDestroyed --- lib/core/Timer.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/core/Timer.js b/lib/core/Timer.js index a6a8bedf..5891a451 100644 --- a/lib/core/Timer.js +++ b/lib/core/Timer.js @@ -1651,6 +1651,17 @@ return this.status === GameTimer.PAUSED; }; + /** + * ### GameTimer.isDestroyed + * + * Returns TRUE if the timer is destroyed + * + * @return {boolean} TRUE if timer is destroyed + */ + GameTimer.prototype.isDestroyed = function() { + return this.status === GameTimer.DESTROYED; + }; + /** * ### GameTimer.isTimeUp | isTimeup * From a68b864d2d56af3a7af5a0d4219490f7f5ee8d46 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 6 Sep 2021 10:26:08 +0200 Subject: [PATCH 43/51] built --- build/nodegame-full.js | 369 +++++++++++------------------------------ 1 file changed, 95 insertions(+), 274 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index e7550268..7f9d9a2e 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -10980,7 +10980,8 @@ if (!Array.prototype.indexOf) { str = 'DEBUG mode: client-side error ' + 'detected.

'; str += msg; - str += '

' + + str += '

Open the DevTools in your browser ' + + 'for details.
' + 'This message will not be shown in production mode.'; W.lockScreen(str); } @@ -23079,120 +23080,6 @@ if (!Array.prototype.indexOf) { * * @return {array} matches The matches according to the algorithm */ - function pairMatcherOld(alg, n, options) { - var ps, matches, bye; - var i, lenI, j, lenJ, jj; - var id1, id2; - var roundsLimit, cycle, cycleI, skipBye; - - if ('number' === typeof n && n > 1) { - ps = J.seq(0, (n-1)); - } - else if (J.isArray(n) && n.length > 1) { - ps = n.slice(); - n = ps.length; - } - else { - throw new TypeError('pairMatcher.' + alg + ': n must be ' + - 'number > 1 or array of length > 1.'); - } - options = options || {}; - - bye = 'undefined' !== typeof options.bye ? options.bye : -1; - skipBye = options.skipBye || false; - - // Make sure we have even numbers. - if ((n % 2) === 1) { - ps.push(bye); - n += 1; - } - - // Limit rounds. - if ('number' === typeof options.rounds) { - if (options.rounds <= 0) { - throw new Error('pairMatcher.' + alg + ': options.rounds ' + - 'must be a positive number or undefined. ' + - 'Found: ' + options.rounds); - } - if (options.rounds > (n-1)) { - throw new Error('pairMatcher.' + alg + ': ' + - 'options.rounds cannot be greater than ' + - (n-1) + '. Found: ' + options.rounds); - } - // Here roundsLimit does not depend on n (must be smaller). - roundsLimit = options.rounds; - } - else { - roundsLimit = n-1; - } - - if ('undefined' !== typeof options.cycle) { - cycle = options.cycle; - if (cycle !== 'mirror_invert' && cycle !== 'mirror' && - cycle !== 'repeat_invert' && cycle !== 'repeat') { - - throw new Error('pairMatcher.' + alg + ': options.cycle ' + - 'must be equal to "mirror"/"mirror_invert", ' + - '"repeat"/"repeat_invert" or undefined . ' + - 'Found: ' + options.cycle); - } - - matches = new Array(roundsLimit*2); - } - else { - matches = new Array(roundsLimit); - } - - i = -1, lenI = roundsLimit; - for ( ; ++i < lenI ; ) { - // Shuffle list of ids for random. - if (alg === 'random') ps = J.shuffle(ps); - // Create a new array for round i. - lenJ = n / 2; - matches[i] = skipBye ? new Array(lenJ-1) : new Array(lenJ); - // Check if new need to cycle. - if (cycle) { - if (cycle === 'mirror' || cycle === 'mirror_invert') { - cycleI = (roundsLimit*2) -i -1; - } - else { - cycleI = i+roundsLimit; - } - matches[cycleI] = skipBye ? - new Array(lenJ-1) : new Array(lenJ); - } - // Counter jj is updated only if not skipBye, - // otherwise we create holes in the matches array. - jj = j = -1; - for ( ; ++j < lenJ ; ) { - id1 = ps[j]; - id2 = ps[n - 1 - j]; - if (!skipBye || (id1 !== bye && id2 !== bye)) { - jj++; - // Insert match. - matches[i][jj] = [ id1, id2 ]; - // Insert cycle match (if any). - if (cycle === 'repeat') { - matches[cycleI][jj] = [ id1, id2 ]; - } - else if (cycle === 'repeat_invert') { - matches[cycleI][jj] = [ id2, id1 ]; - } - else if (cycle === 'mirror') { - matches[cycleI][jj] = [ id1, id2 ]; - } - else if (cycle === 'mirror_invert') { - matches[cycleI][jj] = [ id2, id1 ]; - } - } - } - // Permutate for next round. - ps.splice(1, 0, ps.pop()); - } - return matches; - } - - function pairMatcher(alg, n, options) { var ps, matches, bye; var i, lenI, j, lenJ, jj; @@ -23540,9 +23427,6 @@ if (!Array.prototype.indexOf) { "use strict"; - // ## Global scope - var J = parent.JSUS; - exports.MatcherManager = MatcherManager; /** @@ -25427,6 +25311,7 @@ if (!Array.prototype.indexOf) { if (!widget.options.className) { widget.options.className = 'centered'; } + widget.options.widgetStep = true; // Default id 'container' (as in default.html). if ('string' === typeof widget.root) { @@ -28374,6 +28259,17 @@ if (!Array.prototype.indexOf) { return this.status === GameTimer.PAUSED; }; + /** + * ### GameTimer.isDestroyed + * + * Returns TRUE if the timer is destroyed + * + * @return {boolean} TRUE if timer is destroyed + */ + GameTimer.prototype.isDestroyed = function() { + return this.status === GameTimer.DESTROYED; + }; + /** * ### GameTimer.isTimeUp | isTimeup * @@ -29700,120 +29596,6 @@ if (!Array.prototype.indexOf) { * * @return {array} matches The matches according to the algorithm */ - function pairMatcherOld(alg, n, options) { - var ps, matches, bye; - var i, lenI, j, lenJ, jj; - var id1, id2; - var roundsLimit, cycle, cycleI, skipBye; - - if ('number' === typeof n && n > 1) { - ps = J.seq(0, (n-1)); - } - else if (J.isArray(n) && n.length > 1) { - ps = n.slice(); - n = ps.length; - } - else { - throw new TypeError('pairMatcher.' + alg + ': n must be ' + - 'number > 1 or array of length > 1.'); - } - options = options || {}; - - bye = 'undefined' !== typeof options.bye ? options.bye : -1; - skipBye = options.skipBye || false; - - // Make sure we have even numbers. - if ((n % 2) === 1) { - ps.push(bye); - n += 1; - } - - // Limit rounds. - if ('number' === typeof options.rounds) { - if (options.rounds <= 0) { - throw new Error('pairMatcher.' + alg + ': options.rounds ' + - 'must be a positive number or undefined. ' + - 'Found: ' + options.rounds); - } - if (options.rounds > (n-1)) { - throw new Error('pairMatcher.' + alg + ': ' + - 'options.rounds cannot be greater than ' + - (n-1) + '. Found: ' + options.rounds); - } - // Here roundsLimit does not depend on n (must be smaller). - roundsLimit = options.rounds; - } - else { - roundsLimit = n-1; - } - - if ('undefined' !== typeof options.cycle) { - cycle = options.cycle; - if (cycle !== 'mirror_invert' && cycle !== 'mirror' && - cycle !== 'repeat_invert' && cycle !== 'repeat') { - - throw new Error('pairMatcher.' + alg + ': options.cycle ' + - 'must be equal to "mirror"/"mirror_invert", ' + - '"repeat"/"repeat_invert" or undefined . ' + - 'Found: ' + options.cycle); - } - - matches = new Array(roundsLimit*2); - } - else { - matches = new Array(roundsLimit); - } - - i = -1, lenI = roundsLimit; - for ( ; ++i < lenI ; ) { - // Shuffle list of ids for random. - if (alg === 'random') ps = J.shuffle(ps); - // Create a new array for round i. - lenJ = n / 2; - matches[i] = skipBye ? new Array(lenJ-1) : new Array(lenJ); - // Check if new need to cycle. - if (cycle) { - if (cycle === 'mirror' || cycle === 'mirror_invert') { - cycleI = (roundsLimit*2) -i -1; - } - else { - cycleI = i+roundsLimit; - } - matches[cycleI] = skipBye ? - new Array(lenJ-1) : new Array(lenJ); - } - // Counter jj is updated only if not skipBye, - // otherwise we create holes in the matches array. - jj = j = -1; - for ( ; ++j < lenJ ; ) { - id1 = ps[j]; - id2 = ps[n - 1 - j]; - if (!skipBye || (id1 !== bye && id2 !== bye)) { - jj++; - // Insert match. - matches[i][jj] = [ id1, id2 ]; - // Insert cycle match (if any). - if (cycle === 'repeat') { - matches[cycleI][jj] = [ id1, id2 ]; - } - else if (cycle === 'repeat_invert') { - matches[cycleI][jj] = [ id2, id1 ]; - } - else if (cycle === 'mirror') { - matches[cycleI][jj] = [ id1, id2 ]; - } - else if (cycle === 'mirror_invert') { - matches[cycleI][jj] = [ id2, id1 ]; - } - } - } - // Permutate for next round. - ps.splice(1, 0, ps.pop()); - } - return matches; - } - - function pairMatcher(alg, n, options) { var ps, matches, bye; var i, lenI, j, lenJ, jj; @@ -41419,7 +41201,7 @@ if (!Array.prototype.indexOf) { * @see Widgets.instances */ Widgets.prototype.get = function(widgetName, options) { - var WidgetPrototype, widget, changes; + var WidgetPrototype, widget, changes, tmp; if ('string' !== typeof widgetName) { throw new TypeError('Widgets.get: widgetName must be string.' + @@ -41459,17 +41241,37 @@ if (!Array.prototype.indexOf) { widget = new WidgetPrototype(options); // Set ID. - if ('undefined' !== typeof options.id) { - if ('number' === typeof options.id) options.id += ''; - if ('string' === typeof options.id) { - widget.id = options.id; + tmp = options.id; + if ('undefined' !== typeof tmp) { + if ('number' === typeof tmp) tmp += ''; + if ('string' === typeof tmp) { + + if ('undefined' !== typeof options.idPrefix) { + if ('string' === typeof options.idPrefix && + 'number' !== typeof options.idPrefix) { + + tmp = options.idPrefix + tmp; + } + else { + throw new TypeError('Widgets.get: options.idPrefix ' + + 'must be string, number or ' + + 'undefined. Found: ' + + options.idPrefix); + } + } + + widget.id = tmp; } else { throw new TypeError('Widgets.get: options.id must be ' + 'string, number or undefined. Found: ' + - options.id); + tmp); } } + // Assign step id as widget id, if widget step and no custom id. + else if (options.widgetStep) { + widget.id = node.game.getStepId(); + } // Set prototype values or options values. if ('undefined' !== typeof options.title) { @@ -45186,7 +44988,7 @@ if (!Array.prototype.indexOf) { /** * # ChoiceManager - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates and manages a set of selectable choices forms (e.g., ChoiceTable). @@ -45201,18 +45003,16 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceManager.version = '1.2.1'; + ChoiceManager.version = '1.4.0'; ChoiceManager.description = 'Groups together and manages a set of ' + - 'selectable choices forms (e.g. ChoiceTable).'; + 'survey forms (e.g., ChoiceTable).'; ChoiceManager.title = false; ChoiceManager.className = 'choicemanager'; // ## Dependencies - ChoiceManager.dependencies = { - JSUS: {} - }; + ChoiceManager.dependencies = {}; /** * ## ChoiceManager constructor @@ -45306,6 +45106,16 @@ if (!Array.prototype.indexOf) { storeRef: false }; + + /** + * ### ChoiceManager.simplify + * + * If TRUE, it returns getValues() returns forms.values + * + * @see ChoiceManager.getValue + */ + this.simplify = null; + /** * ### ChoiceManager.freeText * @@ -45421,6 +45231,9 @@ if (!Array.prototype.indexOf) { this.required = !!options.required; } + // If TRUE, it returns getValues returns forms.values. + this.simplify = !!options.simplify; + // After all configuration options are evaluated, add forms. if ('undefined' !== typeof options.forms) this.setForms(options.forms); @@ -45455,7 +45268,7 @@ if (!Array.prototype.indexOf) { * @see ChoiceManager.buildTableAndForms */ ChoiceManager.prototype.setForms = function(forms) { - var form, formsById, i, len, parsedForms; + var form, formsById, i, len, parsedForms, name; if ('function' === typeof forms) { parsedForms = forms.call(node.game); if (!J.isArray(parsedForms)) { @@ -45484,16 +45297,11 @@ if (!Array.prototype.indexOf) { for ( ; ++i < len ; ) { form = parsedForms[i]; if (!node.widgets.isWidget(form)) { - if ('string' === typeof form.name) { - // Add defaults. - J.mixout(form, this.formsOptions); - form = node.widgets.get(form.name, form); - } - if (!node.widgets.isWidget(form)) { - throw new Error('ChoiceManager.setForms: one of the ' + - 'forms is not a widget-like element: ' + - 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) { @@ -45832,6 +45640,9 @@ if (!Array.prototype.indexOf) { } // if (obj.missValues.length) obj.isCorrect = false; if (this.textarea) obj.freetext = this.textarea.value; + + // Simplify everything, if requested. + if (opts.simplify || this.simplify) obj = obj.forms; return obj; }; @@ -60763,7 +60574,7 @@ if (!Array.prototype.indexOf) { /** * # VisualTimer - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Display a configurable timer for the game @@ -60780,7 +60591,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - VisualTimer.version = '0.9.2'; + VisualTimer.version = '0.9.3'; VisualTimer.description = 'Display a configurable timer for the game. ' + 'Can trigger events. Only for countdown smaller than 1h.'; @@ -61070,13 +60881,7 @@ if (!Array.prototype.indexOf) { options = options || {}; oldOptions = this.options; - if (this.internalTimer) { - node.timer.destroyTimer(this.gameTimer); - this.internalTimer = null; - } - else { - this.gameTimer.removeHook(this.updateHookName); - } + destroyTimer(this); this.gameTimer = null; this.activeBox = null; @@ -61143,12 +60948,10 @@ if (!Array.prototype.indexOf) { * * Stops the timer display and stores the time left in `activeBox.timeLeft` * - * @param {object} options Configuration object - * * @see GameTimer.isStopped * @see GameTimer.stop */ - VisualTimer.prototype.stop = function(options) { + VisualTimer.prototype.stop = function() { if (!this.gameTimer.isStopped()) { this.activeBox.timeLeft = this.gameTimer.timeLeft; this.gameTimer.stop(); @@ -61320,13 +61123,7 @@ if (!Array.prototype.indexOf) { // Handle destroy. this.on('destroyed', function() { - if (that.internalTimer) { - node.timer.destroyTimer(that.gameTimer); - that.internalTimer = null; - } - else { - that.gameTimer.removeHook('VisualTimer_' + that.wid); - } + destroyTimer(that); that.bodyDiv.removeChild(that.mainBox.boxDiv); that.bodyDiv.removeChild(that.waitBox.boxDiv); }); @@ -61506,6 +61303,30 @@ if (!Array.prototype.indexOf) { this.bodyDiv.className = className; }; + // Helper function. + + function destroyTimer(that) { + if (that.internalTimer) { + if (!that.gameTimer.isDestroyed()) { + node.timer.destroyTimer(that.gameTimer); + } + that.internalTimer = null; + } + else { + that.gameTimer.removeHook('VisualTimer_' + that.wid); + } + } + + // if (this.internalTimer) { + // if (!this.gameTimer.isDestroyed()) { + // node.timer.destroyTimer(this.gameTimer); + // } + // this.internalTimer = null; + // } + // else { + // this.gameTimer.removeHook(this.updateHookName); + // } + })(node); /** From c195b93a0d0a2e2c3672a421289092a8292e0c41 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Tue, 19 Oct 2021 22:07:53 +0200 Subject: [PATCH 44/51] minor --- build/nodegame-full.js | 354 +++++++++++++++++++++++++++++++++-------- lib/core/Game.js | 2 +- 2 files changed, 286 insertions(+), 70 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 7f9d9a2e..84f1177d 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -25332,7 +25332,7 @@ if (!Array.prototype.indexOf) { widgetRoot, widget.options); } - this[widget.ref] = widgetObj; + node.game[widget.ref] = widgetObj; }; // Make the step callback. @@ -41669,7 +41669,11 @@ if (!Array.prototype.indexOf) { if (strict) return w instanceof node.Widget; return ('object' === typeof w && 'function' === typeof w.append && - 'function' === typeof w.getValues); + 'function' === typeof w.getValues && + // Used by widgets.append + 'function' === typeof w.isHidden && + 'function' === typeof w.isCollapsed + ); }; /** @@ -45003,7 +45007,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceManager.version = '1.4.0'; + ChoiceManager.version = '1.4.1'; ChoiceManager.description = 'Groups together and manages a set of ' + 'survey forms (e.g., ChoiceTable).'; @@ -45586,7 +45590,7 @@ if (!Array.prototype.indexOf) { * @see ChoiceManager.verifyChoice */ ChoiceManager.prototype.getValues = function(opts) { - var obj, i, len, form, lastErrored; + var obj, i, len, form, lastErrored, res; obj = { order: this.order, forms: {}, @@ -45602,13 +45606,17 @@ if (!Array.prototype.indexOf) { form = this.forms[i]; // If it is hidden or disabled we do not do validation. if (form.isHidden() || form.isDisabled()) { - obj.forms[form.id] = form.getValues({ + res = form.getValues({ markAttempt: false, highlight: false }); + if (res) obj.forms[form.id] = res; } else { - obj.forms[form.id] = form.getValues(opts); + // 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 || @@ -45693,7 +45701,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceTable.version = '1.8.0'; + ChoiceTable.version = '1.8.1'; ChoiceTable.description = 'Creates a configurable table where ' + 'each cell is a selectable choice.'; @@ -45796,7 +45804,7 @@ if (!Array.prototype.indexOf) { * @see ChoiceTable.onclick */ this.listener = function(e) { - var name, value, td; + var name, value, td, tr; var i, len, removed; e = e || window.event; @@ -45806,8 +45814,12 @@ if (!Array.prototype.indexOf) { if ('undefined' === typeof that.choicesIds[td.id]) { // It might be a nested element, try the parent. td = td.parentNode; - if (!td || 'undefined' === typeof that.choicesIds[td.id]) { - return; + if (!td) return; + if ('undefined' === typeof that.choicesIds[td.id]) { + td = td.parentNode; + if (!td || 'undefined' === typeof that.choicesIds[td.id]) { + return; + } } } @@ -45828,7 +45840,8 @@ if (!Array.prototype.indexOf) { if (value.length === 1) return; name = value[0]; - value = value[1]; + value = parseInt(value[1], 10); + // value = value[1]; // Choice disabled. // console.log('VALUE: ', value); @@ -46912,6 +46925,9 @@ if (!Array.prototype.indexOf) { else if (J.isElement(choice) || J.isNode(choice)) { td.appendChild(choice); } + else if (node.widgets.isWidget(choice)) { + node.widgets.append(choice, td); + } else { throw new Error('ChoiceTable.renderChoice: invalid choice: ' + choice); @@ -47254,10 +47270,10 @@ if (!Array.prototype.indexOf) { */ ChoiceTable.prototype.isChoiceCurrent = function(choice) { var i, len; - if ('number' === typeof choice) { - choice = '' + choice; + if ('string' === typeof choice) { + choice = parseInt(choice, 10); } - else if ('string' !== typeof choice) { + else if ('number' !== typeof choice) { throw new TypeError('ChoiceTable.isChoiceCurrent: choice ' + 'must be string or number. Found: ' + choice); } @@ -47635,8 +47651,8 @@ if (!Array.prototype.indexOf) { * @return {string} The checked choice */ function checkCorrectChoiceParam(that, choice) { - if ('number' === typeof choice) choice = '' + choice; - if ('string' !== typeof choice) { + if ('string' === typeof choice) choice = parseInt(choice, 10); + if ('number' !== typeof choice) { throw new TypeError('ChoiceTable.setCorrectChoice: each choice ' + 'must be number or string. Found: ' + choice); } @@ -49059,6 +49075,213 @@ if (!Array.prototype.indexOf) { })(node); +/** + * # Consent + * Copyright(c) 2021 Stefano Balietti + * MIT Licensed + * + * Displays a consent form with buttons to accept/reject it + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('Consent', Consent); + + // ## Meta-data + + Consent.version = '0.3.0'; + Consent.description = 'Displays a configurable consent form.'; + + Consent.title = false; + Consent.panel = false; + Consent.className = 'consent'; + + Consent.texts = { + + areYouSure: 'You did not consent and are about to leave the ' + + 'study. Are you sure?', + + printText: + '

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', + + }; + + /** + * ## Consent constructor + * + * 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 + * + * The object containing the variables to substitute + * + * Default: node.game.settings.CONSENT + */ + this.consent = null; + + /** + * ## Consent.showPrint + * + * If TRUE, the print button is shown + * + * Default: TRUE + */ + this.showPrint = null; + } + + // ## Consent methods. + + /** + * ### Consent.init + * + * Initializes the widget + * + * @param {object} opts Optional. Configuration options. + */ + Consent.prototype.init = function(opts) { + opts = opts || {}; + + this.consent = opts.consent || node.game.settings.CONSENT; + + if ('object' !== typeof this.consent) { + throw new TypeError('Consent: consent must be object. Found: ' + + this.consent); + } + + this.showPrint = opts.showPrint === false ? false : true; + }; + + 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; + }; + + 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; + }; + + Consent.prototype.append = function() { + var consent, html; + consent = W.gid('consent'); + html = ''; + + // Print. + if (this.showPrint) { + html = this.getText('printText'); + html += '

'; + } + + // Header for buttons. + html += '' + this.getText('consentTerms') + '
'; + + // Buttons. + html += '
' + + '
'; + + consent.innerHTML += html; + setTimeout(function() { W.adjustFrameHeight(); }); + }; + + Consent.prototype.listeners = function() { + var that = this; + var consent = this.consent; + node.on('FRAME_LOADED', function() { + var a, na, p, id; + + // Replace all texts. + for (p in consent) { + if (consent.hasOwnProperty(p)) { + // Making lower-case and replacing underscores with dashes. + id = p.toLowerCase(); + id = id.replace(new RegExp("_", 'g'), "-"); + W.setInnerHTML(id, consent[p]); + } + } + + // 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(); }; + na.onclick = function() { + var showIt, confirmed; + + confirmed = confirm(that.getText('areYouSure')); + if (!confirmed) return; + + node.emit('CONSENT_REJECTING'); + + that.notAgreed = true; + node.set({ + consent: false, + // Need to send these two because it's not a DONE msg. + time: node.timer.getTimeSince('step'), + timeup: false + }); + a.disabled = true; + na.disabled = true; + a.onclick = null; + na.onclick = null; + + node.socket.disconnect(); + W.hide('consent'); + W.show('notAgreed'); + + // If a show-consent button is found enable it. + showIt = W.gid('show-consent'); + if (showIt) { + showIt.onclick = function() { + var div, s; + div = W.toggle('consent'); + s = div.style.display === '' ? 'hide' : 'show'; + this.innerHTML = that.getText('showHideConsent', s); + }; + } + node.emit('CONSENT_REJECTED'); + }; + }); + }; + +})(node); + /** * # ContentBox * Copyright(c) 2019 Stefano Balietti @@ -54081,7 +54304,7 @@ if (!Array.prototype.indexOf) { // ## Add Meta-data - EndScreen.version = '0.7.1'; + EndScreen.version = '0.7.2'; EndScreen.description = 'Game end screen. With end game message, ' + 'email form, and exit code.'; @@ -54484,7 +54707,9 @@ if (!Array.prototype.indexOf) { } - if (data.showBonus !== false) { + if ('undefined' !== typeof data.bonus && + data.showBonus !== false) { + if (preWin !== '') preWin += ' + '; preWin += data.bonus; } @@ -54528,7 +54753,9 @@ if (!Array.prototype.indexOf) { } if (!err) { - if (totalWin !== preWin) totalWin = preWin + ' = ' + totalWin; + if (totalWin !== preWin & preWin !== '') { + totalWin = preWin + ' = ' + totalWin; + } totalWin += ' ' + this.totalWinCurrency; } } @@ -57452,7 +57679,7 @@ if (!Array.prototype.indexOf) { * @param {object} opts Optional. Configuration options. */ RiskGauge.prototype.init = function(opts) { - var gauge; + var gauge, that; if ('undefined' !== typeof opts.method) { if ('string' !== typeof opts.method) { throw new TypeError('RiskGauge.init: method must be string ' + @@ -57474,6 +57701,11 @@ if (!Array.prototype.indexOf) { // Call method. gauge = this.methods[this.method].call(this, opts); + // Add defaults. + that = this; + gauge.isHidden = function() { return that.isHidden(); }; + gauge.isCollapsed = function() { return that.isCollapsed(); }; + // Check properties. if (!node.widgets.isWidget(gauge)) { throw new Error('RiskGauge.init: method ' + this.method + @@ -58873,7 +59105,7 @@ if (!Array.prototype.indexOf) { * @param {object} opts Optional. Configuration options. */ SVOGauge.prototype.init = function(opts) { - var gauge; + var gauge, that; if ('undefined' !== typeof opts.method) { if ('string' !== typeof opts.method) { throw new TypeError('SVOGauge.init: method must be string ' + @@ -58896,8 +59128,18 @@ if (!Array.prototype.indexOf) { // Call method. gauge = this.methods[this.method].call(this, opts); + + // Add defaults. + that = this; + gauge.isHidden = function() { return that.isHidden(); }; + gauge.isCollapsed = function() { return that.isCollapsed(); }; + // Check properties. - checkGauge(this.method, gauge); + if (!node.widgets.isWidget(gauge)) { + throw new Error('SVOGauge.init: method ' + this.method + + ' created invalid gauge: missing default widget ' + + 'methods.') + } // Approved. this.gauge = gauge; @@ -58961,41 +59203,6 @@ if (!Array.prototype.indexOf) { return this.gauge.setValues(opts); }; - // ## Helper functions. - - /** - * ### checkGauge - * - * Checks if a gauge is properly constructed, throws an error otherwise - * - * @param {string} method The name of the method creating it - * @param {object} gauge The object to check - * - * @see ModdGauge.init - */ - function checkGauge(method, gauge) { - if (!gauge) { - throw new Error('SVOGauge.init: method ' + method + - 'did not create element gauge.'); - } - if ('function' !== typeof gauge.getValues) { - throw new Error('SVOGauge.init: method ' + method + - ': gauge missing function getValues.'); - } - if ('function' !== typeof gauge.enable) { - throw new Error('SVOGauge.init: method ' + method + - ': gauge missing function enable.'); - } - if ('function' !== typeof gauge.disable) { - throw new Error('SVOGauge.init: method ' + method + - ': gauge missing function disable.'); - } - if ('function' !== typeof gauge.append) { - throw new Error('SVOGauge.init: method ' + method + - ': gauge missing function append.'); - } - } - // ## Available methods. // ### SVO_Slider @@ -60202,7 +60409,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - VisualStage.version = '0.10.0'; + VisualStage.version = '0.11.0'; VisualStage.description = 'Displays the name of the current, previous and next step of the game.'; @@ -60262,10 +60469,10 @@ if (!Array.prototype.indexOf) { // Default display settings. - // ### VisualStage.showRounds + // ### VisualStage.addRound // // If TRUE, round number is added to the name of steps in repeat stages - this.showRounds = true; + this.addRound = true; // ### VisualStage.showPrevious // @@ -60297,8 +60504,8 @@ if (!Array.prototype.indexOf) { } this.displayMode = opts.displayMode; } - if ('undefined' !== typeof opts.rounds) { - this.showRounds = !!opts.rounds; + if ('undefined' !== typeof opts.addRound) { + this.addRound = !!opts.addRound; } if ('undefined' !== typeof opts.previous) { this.showPrevious = !!opts.previous; @@ -60440,10 +60647,19 @@ if (!Array.prototype.indexOf) { * @return {string} name The name of the step */ VisualStage.prototype.getStepName = function(gameStage, curStage, mod) { - var name, round; + var name, round, preprocess, addRound; // Get the name. If no step property is defined, use the id and // do some text replacing. name = node.game.plot.getProperty(gameStage, 'name'); + if ('function' === typeof name) { + preprocess = name; + name = null; + } + else if ('object' === typeof name && name !== null) { + preprocess = name.preprocess; + addRound = name.addRound; + name = name.name; + } if (!name) { name = node.game.plot.getStep(gameStage); if (!name) { @@ -60455,15 +60671,15 @@ if (!Array.prototype.indexOf) { if (this.capitalize) name = capitalize(name); } } + if (!preprocess) preprocess = this.preprocess; + if ('undefined' === typeof addRound) addRound = this.addRound; + + round = getRound(gameStage, curStage, mod); // If function, executes it. - if ('function' === typeof name) name = name.call(node.game); + if (preprocess) name = preprocess.call(node.game, name, mod, round); + if (addRound && round) name += ' ' + round; - if (this.showRounds) { - round = getRound(gameStage, curStage, mod); - if (round) name += ' ' + round; - } - if (this.preprocess) name = this.preprocess(name, mod, round); return name; }; diff --git a/lib/core/Game.js b/lib/core/Game.js index 54f4c12c..bb0bd7d5 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1089,7 +1089,7 @@ widgetRoot, widget.options); } - this[widget.ref] = widgetObj; + node.game[widget.ref] = widgetObj; }; // Make the step callback. From 3cd1925d866c735a0e4eb1798c337cb1fc4eb84f Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 20 Oct 2021 10:37:20 +0200 Subject: [PATCH 45/51] changelog --- CHANGELOG | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 1f9483b9..b89bd08f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,17 @@ # nodegame-client change log +## 7.0.0 +- New method: Timer.isDetroyed. +- Widget steps add widgetStep option = true. +- Improved ErrorManager. +- Redirect messages for bots gracefully handled. +- New alias: node.on.done. +- New properties added to every DONE msg: stepId and stageId. +- Frame appears at once, waiting for the callback to be executed. +- Stager.require: lets split stages across files. +- Stager.share: shares some variables with all requires. +- Fixed: game scrolls up when a new frame is not loaded. + ## 6.2.0 - DONE is async with respect to node.game.step to let other listeners on DONE finish first. From 803857839fa0d520abeba2c4c69837c93e0708b9 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 20 Oct 2021 11:11:55 +0200 Subject: [PATCH 46/51] 7.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 776e722a..a3807d95 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nodegame-client", "description": "nodeGame client for the browser and node.js", - "version": "6.2.0", + "version": "7.0.0", "homepage": "http://www.nodegame.org", "keywords": [ "game", From ad3f124e7771b2fcb909f8e6116ba9fe22979a34 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 6 Nov 2021 21:54:44 +0100 Subject: [PATCH 47/51] widget steps simplified options passing --- build/nodegame-full.js | 105 +++++++++++++++++++++++------------------ index.browser.js | 2 +- lib/core/Game.js | 4 +- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 84f1177d..82b52124 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -6036,7 +6036,7 @@ if (!Array.prototype.indexOf) { // ### __update.indexes // If TRUE, rebuild indexes on every insert and remove - this.__update.indexes = false; + this.__update.indexes = true; // ### __update.sort // If TRUE, sort db on every insert and remove @@ -10455,7 +10455,7 @@ if (!Array.prototype.indexOf) { node.support = JSUS.compatibility(); // Auto-Generated. - node.version = '6.2.0'; + node.version = '7.0.0'; })(window); @@ -25297,7 +25297,9 @@ if (!Array.prototype.indexOf) { } // Add options, if missing. - if (!widget.options) widget.options = {}; + // User can specify the options in a nested object, or flat them + // down in case there are no conflicts. + if (!widget.options) widget.options = widget; // Make main callback to get/append the widget. widgetCb = function() { @@ -39955,11 +39957,11 @@ if (!Array.prototype.indexOf) { * * Inits the widget after constructor and default properties are added * - * @param {object} options Configuration options + * @param {object} opts Configuration options * * @see Widgets.get */ - Widget.prototype.init = function(options) {}; + Widget.prototype.init = function(opts) {}; /** * ### Widget.listeners @@ -40003,14 +40005,14 @@ if (!Array.prototype.indexOf) { * * Returns the values currently stored by the widget * - * @param {mixed} options Settings controlling the content of return value + * @param {mixed} opts Settings controlling the content of return value * * @return {mixed} The values of the widget */ - Widget.prototype.getValues = function(options) {}; + Widget.prototype.getValues = function(opts) {}; /** - * ### Widget.getValues + * ### Widget.setValues * * Set the stored values directly * @@ -40028,7 +40030,7 @@ if (!Array.prototype.indexOf) { * Deletes current selection, any highlighting, and other data * that the widget might have collected to far. */ - Widget.prototype.reset = function(options) {}; + Widget.prototype.reset = function(opts) {}; /** * ### Widget.highlight @@ -42071,56 +42073,56 @@ if (!Array.prototype.indexOf) { * * @param {object} options Optional. Configuration options */ - BackButton.prototype.init = function(options) { + BackButton.prototype.init = function(opts) { var tmp; - options = options || {}; + opts = opts || {}; //Button - if ('undefined' === typeof options.id) { + if ('undefined' === typeof opts.id) { tmp = BackButton.className; } - else if ('string' === typeof options.id) { - tmp = options.id; + else if ('string' === typeof opts.id) { + tmp = opts.id; } - else if (false === options.id) { + else if (false === opts.id) { tmp = ''; } else { - throw new TypeError('BackButton.init: options.id must ' + + throw new TypeError('BackButton.init: opts.id must ' + 'be string, false, or undefined. Found: ' + - options.id); + opts.id); } this.button.id = tmp; - if ('undefined' === typeof options.className) { + if ('undefined' === typeof opts.className) { tmp = 'btn btn-lg btn-secondary'; } - else if (options.className === false) { + else if (opts.className === false) { tmp = ''; } - else if ('string' === typeof options.className) { - tmp = options.className; + else if ('string' === typeof opts.className) { + tmp = opts.className; } - else if (J.isArray(options.className)) { - tmp = options.className.join(' '); + else if (J.isArray(opts.className)) { + tmp = opts.className.join(' '); } else { - throw new TypeError('BackButton.init: options.className must ' + + throw new TypeError('BackButton.init: opts.className must ' + 'be string, array, or undefined. Found: ' + - options.className); + opts.className); } this.button.className = tmp; // Button text. - this.button.value = 'string' === typeof options.text ? - options.text : this.getText('back'); + this.button.value = 'string' === typeof opts.text ? + opts.text : this.getText('back'); this.stepOptions.acrossStages = - 'undefined' === typeof options.acrossStages ? - false : !!options.acrossStages; + 'undefined' === typeof opts.acrossStages ? + false : !!opts.acrossStages; this.stepOptions.acrossRounds = - 'undefined' === typeof options.acrossRounds ? - true : !!options.acrossRounds; + 'undefined' === typeof opts.acrossRounds ? + true : !!opts.acrossRounds; }; BackButton.prototype.append = function() { @@ -45650,7 +45652,12 @@ if (!Array.prototype.indexOf) { if (this.textarea) obj.freetext = this.textarea.value; // Simplify everything, if requested. - if (opts.simplify || this.simplify) obj = obj.forms; + if (opts.simplify || this.simplify) { + res = obj; + obj = obj.forms; + if (res.isCorrect === false) obj.isCorrect = false; + if (res.freetext) obj.freetext = res.freetext; + } return obj; }; @@ -49116,6 +49123,10 @@ if (!Array.prototype.indexOf) { notAgree: 'No, I do not agree', + showHideConsent: function(w, s) { + return (s === 'hide' ? 'Hide' : 'Show') + ' Consent Form'; + } + }; /** @@ -49163,9 +49174,9 @@ if (!Array.prototype.indexOf) { this.consent = opts.consent || node.game.settings.CONSENT; - if ('object' !== typeof this.consent) { - throw new TypeError('Consent: consent must be object. Found: ' + - this.consent); + if (this.consent && 'object' !== typeof this.consent) { + throw new TypeError('Consent: consent must be object or ' + + 'undefined. Found: ' + this.consent); } this.showPrint = opts.showPrint === false ? false : true; @@ -49191,6 +49202,9 @@ if (!Array.prototype.indexOf) { Consent.prototype.append = function() { var consent, html; + // Hide not agreed div. + W.hide('notAgreed'); + consent = W.gid('consent'); html = ''; @@ -49223,12 +49237,15 @@ if (!Array.prototype.indexOf) { var a, na, p, id; // Replace all texts. - for (p in consent) { - if (consent.hasOwnProperty(p)) { - // Making lower-case and replacing underscores with dashes. - id = p.toLowerCase(); - id = id.replace(new RegExp("_", 'g'), "-"); - W.setInnerHTML(id, consent[p]); + if (consent) { + for (p in consent) { + if (consent.hasOwnProperty(p)) { + // Making lower-case and replacing underscore + // s with dashes. + id = p.toLowerCase(); + id = id.replace(new RegExp("_", 'g'), "-"); + W.setInnerHTML(id, consent[p]); + } } } @@ -49240,7 +49257,7 @@ if (!Array.prototype.indexOf) { if (!na) throw new Error('Consent: notAgree button not found'); - a.onclick = function() { node.done(); }; + a.onclick = function() { node.done({ consent: true }); }; na.onclick = function() { var showIt, confirmed; @@ -59028,9 +59045,7 @@ if (!Array.prototype.indexOf) { // ## Dependencies - SVOGauge.dependencies = { - JSUS: {} - }; + SVOGauge.dependencies = {}; /** * ## SVOGauge constructor diff --git a/index.browser.js b/index.browser.js index 65cfa095..e8bbc71c 100644 --- a/index.browser.js +++ b/index.browser.js @@ -21,6 +21,6 @@ node.support = JSUS.compatibility(); // Auto-Generated. - node.version = '6.2.0'; + node.version = '7.0.0'; })(window); diff --git a/lib/core/Game.js b/lib/core/Game.js index bb0bd7d5..82f87e60 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1054,7 +1054,9 @@ } // Add options, if missing. - if (!widget.options) widget.options = {}; + // User can specify the options in a nested object, or flat them + // down in case there are no conflicts. + if (!widget.options) widget.options = widget; // Make main callback to get/append the widget. widgetCb = function() { From 13ca7b8e35b3431d0c5f1750b3650cf7ec346081 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 6 Nov 2021 21:55:54 +0100 Subject: [PATCH 48/51] 7.1.0 --- CHANGELOG | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b89bd08f..8594e326 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # nodegame-client change log +## 7.1.0 +- Widget steps supports flattened options inside widget object. + ## 7.0.0 - New method: Timer.isDetroyed. - Widget steps add widgetStep option = true. diff --git a/package.json b/package.json index a3807d95..fd8d68d3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nodegame-client", "description": "nodeGame client for the browser and node.js", - "version": "7.0.0", + "version": "7.1.0", "homepage": "http://www.nodegame.org", "keywords": [ "game", From db9669fe3d252fe95d791239d790cee7d3d0081a Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 13 Dec 2021 14:41:59 +0100 Subject: [PATCH 49/51] required and requiredChoice used to check if a widget step requires action --- build/nodegame-full.js | 1678 ++++++++++++++++++++++++++++++++++++---- index.browser.js | 2 +- lib/core/Game.js | 7 +- 3 files changed, 1524 insertions(+), 163 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 82b52124..c8ae9ed7 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -10455,7 +10455,7 @@ if (!Array.prototype.indexOf) { node.support = JSUS.compatibility(); // Auto-Generated. - node.version = '7.0.0'; + node.version = '7.1.0'; })(window); @@ -25352,9 +25352,10 @@ if (!Array.prototype.indexOf) { // Make the done callback to send results. widgetDone = function() { - var values, opts; + var values, opts, req; + req = widgetObj.required || widgetObj.requiredChoice; // TODO: harmonize: required or checkValues? - if (widgetObj.required && widget.checkValues !== false) { + if (req && widget.checkValues !== false) { opts = { highlight: true, markAttempt: true }; } else { @@ -25367,7 +25368,7 @@ if (!Array.prototype.indexOf) { // If it is not timeup, and user did not // disabled it, check answers. - if (widgetObj.required && widget.checkValues !== false && + if (req && widget.checkValues !== false && !node.game.timer.isTimeup()) { // Widget must return some values (otherwise it @@ -39916,7 +39917,7 @@ if (!Array.prototype.indexOf) { /** * # Widget - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Prototype of a widget class @@ -40817,6 +40818,28 @@ if (!Array.prototype.indexOf) { 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. /** @@ -40842,11 +40865,12 @@ if (!Array.prototype.indexOf) { */ 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) { @@ -40997,18 +41021,18 @@ if (!Array.prototype.indexOf) { * 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 @@ -41447,11 +41471,11 @@ if (!Array.prototype.indexOf) { } } // 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; } } @@ -41640,7 +41664,7 @@ if (!Array.prototype.indexOf) { } // 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; }; @@ -41693,7 +41717,7 @@ if (!Array.prototype.indexOf) { 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.'); @@ -41972,7 +41996,7 @@ if (!Array.prototype.indexOf) { // ## 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.'; @@ -41980,12 +42004,6 @@ if (!Array.prototype.indexOf) { BackButton.className = 'backbutton'; BackButton.texts.back = 'Back'; - // ## Dependencies - - BackButton.dependencies = { - JSUS: {} - }; - /** * ## BackButton constructor * @@ -42022,6 +42040,11 @@ if (!Array.prototype.indexOf) { 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) return; + } res = node.game.stepBack(that.stepOptions); if (res === false) that.enable(); }; @@ -42050,6 +42073,18 @@ if (!Array.prototype.indexOf) { // ## @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 @@ -42123,6 +42158,8 @@ if (!Array.prototype.indexOf) { this.stepOptions.acrossRounds = 'undefined' === typeof opts.acrossRounds ? true : !!opts.acrossRounds; + + setOnClick(this, opts.onclick); }; BackButton.prototype.append = function() { @@ -42145,19 +42182,30 @@ if (!Array.prototype.indexOf) { 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 (prop) { + setOnClick(that, prop.onclick, true); + if (prop.enable) that.enable(); + } + }); + + // Catch those events. + node.events.game.on('WIDGET_NEXT', function() { + that.enable(); }); }; @@ -42179,6 +42227,22 @@ if (!Array.prototype.indexOf) { this.button.disabled = false; }; + // ## 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); /** @@ -45009,7 +45073,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceManager.version = '1.4.1'; + ChoiceManager.version = '1.6.0'; ChoiceManager.description = 'Groups together and manages a set of ' + 'survey forms (e.g., ChoiceTable).'; @@ -45141,9 +45205,39 @@ if (!Array.prototype.indexOf) { /** * ### 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 methods @@ -45240,6 +45334,9 @@ if (!Array.prototype.indexOf) { // If TRUE, it returns getValues returns forms.values. this.simplify = !!options.simplify; + // If TRUE, forms are displayed one by one. + this.oneByOne = !!options.oneByOne; + // After all configuration options are evaluated, add forms. if ('undefined' !== typeof options.forms) this.setForms(options.forms); @@ -45307,7 +45404,18 @@ if (!Array.prototype.indexOf) { name = form.name || 'ChoiceTable'; // Add defaults. J.mixout(form, this.formsOptions); + + // Display forms one by one. + if (this.oneByOne && this.oneByOneCounter !== i) { + form.hidden = true; + } + + if (form.conditional) { + this.conditionals[form.id] = form.conditional; + } + form = node.widgets.get(name, form); + } if (form.id) { @@ -45603,39 +45711,62 @@ if (!Array.prototype.indexOf) { 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; + + len = this.forms.length; + + // 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 { - // 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))) { + // Copy all partial results in the obj returning the + obj.forms = this.oneByOneResults; + } - obj.missValues.push(form.id); - lastErrored = form; + } + // All forms on the page. + else { + i = -1; + 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; } - if (opts.markAttempt && - obj.forms[form.id].isCorrect === false) { + else { + // ContentBox does not return a value. + res = form.getValues(opts); + if (!res) continue; + obj.forms[form.id] = res; - // obj.isCorrect = false; - lastErrored = form; + res = checkFormResult(res, form, opts, obj); + if (res) lastErrored = res; } } } + if (lastErrored) { if (opts.highlight && 'function' === typeof lastErrored.bodyDiv.scrollIntoView) { @@ -45685,8 +45816,107 @@ if (!Array.prototype.indexOf) { if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); }; + /** + * ### 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. + */ + ChoiceManager.prototype.next = function() { + var form, conditional, failsafe; + if (!this.oneByOne) return false; + if (!this.forms || !this.forms.length) { + throw new Error('ChoiceManager.next: no forms found.'); + } + form = this.forms[this.oneByOneCounter]; + if (!form || form.next()) return false; + if (this.oneByOneCounter >= (this.forms.length-1)) 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); + } + form.show(); + W.adjustFrameHeight(); + + node.emit('WIDGET_NEXT', this); + }; + + ChoiceManager.prototype.prev = function() { + var form; + if (!this.oneByOne) return false; + if (!this.forms || !this.forms.length) { + throw new Error('ChoiceManager.prev: no forms found.'); + } + form = this.forms[this.oneByOneCounter]; + if (form.prev()) return true; + if (this.oneByOneCounter <= 1) return false; + form.hide(); + this.oneByOneCounter--; + this.forms[this.oneByOneCounter].show(); + W.adjustFrameHeight(); + node.emit('WIDGET_PREV', this); + }; + // ## Helper methods. + 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; + } + + function checkConditional(that, id) { + var f, c, form; + f = that.conditionals[id]; + if (f) { + for (c in f) { + if (f.hasOwnProperty(c)) { + form = that.formsById[c]; + // No multiple choice allowed. + if (form && form.currentChoice !== f[c]) return false; + } + } + } + return true; + } + +// In progress. +// const createOnClick = (choice, question) => { +// return function(value, removed, td) { +// var w, hide; +// w = node.widgets.lastAppended.formsById[question]; +// if (J.isArray(choice)) { +// hide = !J.inArray(this.currentChoice, choice); +// } +// else { +// hide = this.currentChoice !== choice; +// } +// if (hide) w.hide(); +// else w.show(); +// W.adjustFrameHeight(); +// }; +// }; +// onclick: createOnClick([0, 1], 'crypto_occupation') + })(node); /** @@ -45708,7 +45938,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceTable.version = '1.8.1'; + ChoiceTable.version = '1.10.0'; ChoiceTable.description = 'Creates a configurable table where ' + 'each cell is a selectable choice.'; @@ -45748,6 +45978,7 @@ if (!Array.prototype.indexOf) { if (w.requiredChoice) res += ' *'; return res; }, + error: function(w, value) { if (value !== null && ('number' === typeof w.correctChoice || @@ -45756,18 +45987,16 @@ if (!Array.prototype.indexOf) { 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 * @@ -45811,8 +46040,8 @@ if (!Array.prototype.indexOf) { * @see ChoiceTable.onclick */ this.listener = function(e) { - var name, value, td, tr; - var i, len, removed; + var name, value, td; + var i, len, removed, other; e = e || window.event; td = e.target || e.srcElement; @@ -45857,6 +46086,19 @@ if (!Array.prototype.indexOf) { // One more click. that.numberOfClicks++; + len = that.choices.length; + + if (that.customInput) { + // Is "Other" currently selected? + other = value === (len - 1); + if (that.customInput.isHidden()) { + if (other) that.customInput.show(); + } + else { + if (other) that.customInput.hide(); + } + } + // Click on an already selected choice. if (that.isChoiceCurrent(value)) { that.unsetCurrentChoice(value); @@ -45918,6 +46160,8 @@ if (!Array.prototype.indexOf) { value = parseInt(value, 10); that.onclick.call(that, value, removed, td); } + + if (that.doneOnClick) node.done(); }; /** @@ -46251,6 +46495,59 @@ if (!Array.prototype.indexOf) { * If TRUE, cells have same width regardless of content */ 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.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 + */ + this.solution = null; + + /** + * ### ChoiceTable.solutionDisplayed + * + * TRUE, if the solution is currently displayed + */ + this.solutionDisplayed = false; + + /** + * ### ChoiceTable.solutionDiv + * + * The
element containing the solution + */ + this.solutionDiv = null; } // ## ChoiceTable methods @@ -46583,6 +46880,11 @@ if (!Array.prototype.indexOf) { this.choicesSetSize = opts.choicesSetSize; } + // Add other. + if ('undefined' !== typeof opts.other) { + this.other = opts.other; + } + // Add the choices. if ('undefined' !== typeof opts.choices) { this.setChoices(opts.choices); @@ -46601,9 +46903,9 @@ if (!Array.prototype.indexOf) { // Add the correct choices. if ('undefined' !== typeof opts.disabledChoices) { 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: ' + + opts.disabledChoices); } // TODO: check if values of disabled choices are correct? @@ -46618,9 +46920,22 @@ if (!Array.prototype.indexOf) { } } - if ('undefined' === typeof opts.sameWidthCells) { + 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; + } }; /** @@ -46677,6 +46992,11 @@ if (!Array.prototype.indexOf) { this.order = J.seq(0, len-1); if (this.shuffleChoices) this.order = J.shuffle(this.order); + 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. @@ -47055,6 +47375,11 @@ if (!Array.prototype.indexOf) { 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) { @@ -47070,6 +47395,28 @@ if (!Array.prototype.indexOf) { } }; + /** + * ### 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 + } + // 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); + + }; + /** * ### ChoiceTable.setError * @@ -47122,6 +47469,7 @@ if (!Array.prototype.indexOf) { // 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'); }; @@ -47142,6 +47490,7 @@ if (!Array.prototype.indexOf) { 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'); }; @@ -47166,9 +47515,24 @@ if (!Array.prototype.indexOf) { * @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) { @@ -47176,40 +47540,40 @@ if (!Array.prototype.indexOf) { 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; + }; /** @@ -47315,18 +47679,26 @@ if (!Array.prototype.indexOf) { * * 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); }; @@ -47338,9 +47710,14 @@ if (!Array.prototype.indexOf) { * * @see ChoiceTable.highlighted */ - ChoiceTable.prototype.unhighlight = function() { + ChoiceTable.prototype.unhighlight = function(opts) { + var ci; 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'); @@ -47374,7 +47751,10 @@ if (!Array.prototype.indexOf) { * @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, @@ -47392,20 +47772,21 @@ if (!Array.prototype.indexOf) { // 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(); } @@ -47418,18 +47799,42 @@ if (!Array.prototype.indexOf) { if (this.groupOrder === 0 || this.groupOrder) { obj.groupOrder = this.groupOrder; } - if (null !== this.correctChoice || null !== this.requiredChoice) { + + ci = this.customInput; + if (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; }; @@ -47551,6 +47956,9 @@ if (!Array.prototype.indexOf) { // Make a random comment. if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); + if (this.custominput && !this.custominput.isHidden()) { + this.custominput.setValues(); + } }; /** @@ -47591,6 +47999,7 @@ if (!Array.prototype.indexOf) { if (this.isHighlighted()) this.unhighlight(); if (options.shuffleChoices) this.shuffle(); + if (this.customInput) this.customInput.reset(); }; /** @@ -47605,8 +48014,15 @@ if (!Array.prototype.indexOf) { 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); @@ -47639,6 +48055,40 @@ if (!Array.prototype.indexOf) { 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; + if (!this.solution || this.solutionDisplayed) return false; + this.solutionDisplayed = true; + sol = this.solution; + 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() { + if (!this.solutionDisplayed) return false; + this.solutionDisplayed = false; + this.solutionDiv.innerHTML = ''; + this.enable(); + W.adjustFrameHeight(); + node.emit('WIDGET_NEXT', this); + return true; + }; + // ## Helper methods. /** @@ -47713,7 +48163,10 @@ if (!Array.prototype.indexOf) { * @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; } @@ -51287,7 +51740,6 @@ if (!Array.prototype.indexOf) { * * @return {mixed} The value in the input * - * @see CustomInput.verifyChoice * @see CustomInput.reset */ CustomInput.prototype.getValues = function(opts) { @@ -53608,12 +54060,6 @@ if (!Array.prototype.indexOf) { DoneButton.className = 'donebutton'; DoneButton.texts.done = 'Done'; - // ## Dependencies - - DoneButton.dependencies = { - JSUS: {} - }; - /** * ## DoneButton constructor * @@ -53649,6 +54095,10 @@ if (!Array.prototype.indexOf) { 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(); }; @@ -53757,14 +54207,7 @@ if (!Array.prototype.indexOf) { '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() { @@ -53817,6 +54260,8 @@ if (!Array.prototype.indexOf) { } if ('string' === typeof prop) that.button.value = prop; else if (prop && prop.text) that.button.value = prop.text; + + if (prop) setOnClick(this, prop.onclick, true); }); if (this.disableOnDisconnect) { @@ -53881,6 +54326,910 @@ if (!Array.prototype.indexOf) { 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; + } + } + +})(node); + +(function(node) { + + node.widgets.register('Dropdown', Dropdown); + + // Meta-data. + + Dropdown.version = '0.3.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 && + w.choices.indexOf(value) < 0) { + return 'No custom values allowed.' + } + if (value !== null && w.correctChoice !== null) { + return 'Not correct, try again.'; + } + if (value !== null && w.verifyChoice().err) { + return w.verifyChoice().err; + } + + return 'Answer required.'; + } + }; + + // Title is displayed in the header. + Dropdown.title = false; + // Classname is added to the widgets. + Dropdown.className = 'dropdown'; + + // Constructor taking a configuration parameter. + // The options object is always existing. + function Dropdown() { + var that; + that = this; + + // You can define widget properties here, + // but they should get assigned a value in init. + + this.id = null; + + /** + * ### Dropdown.mainText + * + * Main text above the dropdown + */ + this.mainText = null; + + /** + * ### Dropdown.labelText + * + * A label text for the input + */ + this.labelText = null; + + /** + * ### Dropdown.placeholder + * + * A placeholder text for the input + */ + this.placeholder = null; + + /** + * ### Dropdown.choices + * + * The array available choices + */ + this.choices = null; + + /** + * ### Dropdown.tag + * + * The HTML tag: "datalist" or "select" + */ + this.tag = null; + + /** + * ### Dropdown.menu + * + * 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 + * + * The main listener + * + * @see Dropdown.onchange + */ + this.listener = function (e) { + var menu, timeout; + + e = e || window.event; + menu = e.target || e.srcElement; + + that.currentChoice = menu.value; + if (that.currentChoice.length === 0) that.currentChoice = null; + + // Relative time. + if ('string' === typeof that.timeFrom) { + that.timeCurrentChoice = node.timer.getTimeSince(that.timeFrom); + } + // Absolute time. + else { + that.timeCurrentChoice = Date.now ? + Date.now() : new Date().getTime(); + } + + // One more change. + that.numberOfChanges++; + + // Remove any warning/errors on change. + if (that.isHighlighted()) that.unhighlight(); + + if (timeout) clearTimeout(timeout); + + timeout = setTimeout(function () { + that.verifyChoice(); + if (that.verifyChoice().err) { + that.setError(that.verifyChoice().err) + } + + }, that.validationSpeed); + + // Call onchange, if any. + if (that.onchange) { + that.onchange(that.currentChoice, that); + } + + }; + + /* + * ### Dropdown.onchange + * + * User defined onchange function + */ + this.onchange = null; + + /** + * ### Dropdown.timeCurrentChoice + * + * Time when the last choice was made + */ + this.timeCurrentChoice = null; + + /** + * ### Dropdown.timeFrom + * + * Time is measured from timestamp as saved by node.timer + * + * Default event is a new step is loaded (user can interact with + * the screen). Set it to FALSE, to have absolute time. + * + * @see node.timer.getTimeSince + */ + this.timeFrom = 'step'; + + /** + * ### Dropdown.numberOfChanges + * + * Total number of changes between different choices + */ + this.numberOfChanges = 0; + + /** + * ### Dropdown.currentChoice + * + * Choice associated with currently selected cell/s + * + * The field is a number. + */ + this.currentChoice = null; + + /** + * ### Dropdown.shuffleChoices + * + * If TRUE, choices are shuffled. + */ + this.shuffleChoices = null; + + /** + * ### Dropdown.order + * + * The current order of display of choices + * + */ + this.order = null; + + /** + * ### Dropdown.errorBox + * + * An HTML element displayed when a validation error occurs + */ + this.errorBox = null; + + /** + * ### Dropdown.correctChoice + * + * The correct choice/s + * + * The field is an array or number|string. + * + */ + this.correctChoice = null; + + /** + * ### Dropdown.requiredChoice + * + * If True, a choice is required. + */ + this.requiredChoice = null; + + /** + * ### Dropdown.fixedChoice + * + * If True, custom values in menu do not validated. + */ + this.fixedChoice = null; + + /** + * ### Dropdown.inputWidth + * + * The width of the input form as string (css attribute) + * + * Some types preset it automatically + */ + this.inputWidth = null; + + /** + * ### CustomInput.userValidation + * + * An additional validation executed after the main validation function + * + * The function returns an object like: + * + * ```javascript + * { + * value: 'validvalue', + * err: 'This error occurred' // If invalid. + * } + * ``` + */ + this.validation = null; + + /** + * ### Dropdown.validationSpeed + * + * How often (in milliseconds) the validation function is called + * + * Default: 500 + */ + this.validationSpeed = 500; + + } + + + Dropdown.prototype.init = function (options) { + // 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'); + } + + if ('string' === typeof options.mainText) { + this.mainText = options.mainText; + } + else if ('undefined' !== typeof options.mainText) { + throw new TypeError('Dropdown.init: options.mainText must ' + + 'be string or undefined. Found: ' + + options.mainText); + } + + // Set the labelText, if any. + if ('string' === typeof options.labelText) { + this.labelText = options.labelText; + } + else if ('undefined' !== typeof options.labelText) { + throw new TypeError('Dropdown.init: options.labelText must ' + + 'be string or undefined. Found: ' + + options.labelText); + } + + // Set the placeholder text, if any. + if ('string' === typeof options.placeholder) { + this.placeholder = options.placeholder; + } + else if ('undefined' !== typeof options.placeholder) { + throw new TypeError('Dropdown.init: options.placeholder must ' + + 'be string or undefined. Found: ' + + options.placeholder); + } + + // Add the choices. + if ('undefined' !== typeof options.choices) { + this.choices = options.choices; + } + + // Option requiredChoice, if any. + if ('boolean' === typeof options.requiredChoice) { + this.requiredChoice = options.requiredChoice; + } + else if ('undefined' !== typeof options.requiredChoice) { + throw new TypeError('Dropdown.init: options.requiredChoice ' + + 'be boolean or undefined. Found: ' + + options.requiredChoice); + } + + // Add the correct choices. + if ('undefined' !== typeof options.correctChoice) { + if (this.requiredChoice) { + throw new Error('Dropdown.init: cannot specify both ' + + 'options 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'); + } + else { + this.correctChoice = options.correctChoice; + } + + } + + // Option fixedChoice, if any. + if ('boolean' === typeof options.fixedChoice) { + this.fixedChoice = options.fixedChoice; + } + else if ('undefined' !== typeof options.fixedChoice) { + throw new TypeError('Dropdown.init: options.fixedChoice ' + + 'be boolean or undefined. Found: ' + + options.fixedChoice); + } + + if ("undefined" === typeof options.tag) { + this.tag = "datalist"; + } + else if ("datalist" === options.tag || "select" === options.tag) { + this.tag = options.tag; + } + else { + throw new TypeError('Dropdown.init: options.tag must ' + + 'be "datalist", "select" or undefined. Found: ' + options.tag); + } + + // Set the main onchange listener, if any. + if ('function' === typeof options.listener) { + this.listener = function (e) { + options.listener.call(this, e); + }; + } + else if ('undefined' !== typeof options.listener) { + throw new TypeError('Dropdown.init: opts.listener must ' + + 'be function or undefined. Found: ' + + options.listener); + } + + // Set an additional onchange, if any. + if ('function' === typeof options.onchange) { + this.onchange = options.onchange; + } + else if ('undefined' !== typeof options.onchange) { + throw new TypeError('Dropdownn.init: opts.onchange must ' + + 'be function or undefined. Found: ' + + options.onchange); + } + + // Set an additional validation, if any. + if ('function' === typeof options.validation) { + this.validation = options.validation; + } + else if ('undefined' !== typeof options.validation) { + throw new TypeError('Dropdownn.init: opts.validation must ' + + 'be function or undefined. Found: ' + + options.validation); + } + + + // Option shuffleChoices, default false. + if ('undefined' === typeof options.shuffleChoices) tmp = false; + else tmp = !!options.shuffleChoices; + this.shuffleChoices = tmp; + + if (options.width) { + if ('string' !== typeof options.width) { + throw new TypeError('Dropdownn.init:width must be string or ' + + 'undefined. Found: ' + options.width); + } + this.inputWidth = options.width; + } + + // Validation Speed + if ('undefined' !== typeof options.validationSpeed) { + + tmp = J.isInt(options.valiadtionSpeed, 0, undefined, true); + if (tmp === false) { + throw new TypeError('Dropdownn.init: validationSpeed must ' + + ' a non-negative number or undefined. Found: ' + + options.validationSpeed); + } + this.validationSpeed = tmp; + } + + } + + // 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; + + text = W.get('p'); + text.innerHTML = this.mainText; + text.id = 'p'; + this.bodyDiv.appendChild(text); + + label = W.get('label'); + label.innerHTML = this.labelText + this.bodyDiv.appendChild(label); + + this.setChoices(this.choices, true); + + this.errorBox = W.append('div', this.bodyDiv, { + className: 'errbox', id: 'errbox' + }); + }; + + + Dropdown.prototype.setChoices = function (choices, append) { + var isDatalist, order; + var select; + var i, len, value, name; + + // TODO validate choices. + this.choices = choices; + + if (!append) return; + + isDatalist = this.tag === 'datalist'; + + // Create the structure from scratch or just clear all options. + if (this.menu) { + select = isDatalist ? this.datalist : this.menu; + select.innerHTML = ''; + } + else { + if (isDatalist) { + + this.menu = W.add('input', this.bodyDiv, { + id: this.id, + autocomplete: 'off' + }); + + this.datalist = select = W.add('datalist', this.bodyDiv, { + id: this.id + "_datalist" + }); + + this.menu.setAttribute('list', this.datalist.id); + } + else { + + select = W.get('select'); + select.id = this.id; + + 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++) { + + // 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 + * + * Compares the current choice/s with the correct one/s + * + * Depending on current settings, there are three modes of verifying + * choices: + * + * - requiredChoice: either true or false. + * - 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 + * + */ + 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; + } + + if (this.requiredChoice) { + res.value = current !== null && current !== this.placeholder; + } + + // If no correct choice is set return null. + if ('undefined' === typeof correct) res.value = null; + if ('string' === typeof correct) { + res.value = current === correct; + } + if ('number' === typeof correct) { + res.value = current === this.choices[correct]; + } + if (J.isArray(correct)) { + correctOptions = correct.map(function (x) { + return that.choices[x]; + }); + res.value = correctOptions.indexOf(current) >= 0; + } + + if (this.fixedChoice) { + if (this.choices.indexOf(current) < 0) res.value = false; + } + + if (this.validation) this.validation(this.currentChoice, res); + + return res; + }; + + /** + * ### Dropdown.setError + * + * Set the error msg inside the errorBox + * + * @param {string} The error msg (can contain HTML) + * + * @see Dropdown.errorBox + */ + Dropdown.prototype.setError = function (err) { + // TODO: the errorBox is added only if .append() is called. + // However, DropdownGroup use the table without calling .append(). + if (this.errorBox) this.errorBox.innerHTML = err || ''; + if (err) this.highlight(); + else this.unhighlight(); + }; + + /** + * ### Dropdown.highlight + * + * Highlights the input + * + * @param {string} The style for the table's border. + * Default '3px solid red' + * + * @see Dropdown.highlighted + */ + Dropdown.prototype.highlight = function (border) { + if (border && 'string' !== typeof border) { + throw new TypeError('Dropdown.highlight: border must be ' + + 'string or undefined. Found: ' + border); + } + if (this.highlighted) return; + this.menu.style.border = border || '3px solid red'; + this.highlighted = true; + this.emit('highlighted', border); + }; + + /** + * ### Dropdown.unhighlight + * + * Removes highlight + * + * @see Dropdown.highlighted + */ + Dropdown.prototype.unhighlight = function () { + if (this.highlighted !== true) return; + this.menu.style.border = ''; + this.highlighted = false; + this.setError(); + 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 ('number' !== typeof choice) { + 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.'); + } + 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) { + opts.values = J.randomInt(this.choices.length) -1; + } + + this.selectChoice(opts.values); + + }; + + /** + * ### Dropdown.getValues + * + * Returns the values for current selection and other paradata + * + * Paradata that is not set or recorded will be omitted + * + * @return {object} Object containing the choice and paradata + * + * @see Dropdown.verifyChoice + */ + Dropdown.prototype.getValues = function (opts) { + var obj; + opts = opts || {}; + var verif = this.verifyChoice().value; + + obj = { + id: this.id, + choice: this.fixedChoice ? + this.choices.indexOf(this.currentChoice) : this.currentChoice, + time: this.timeCurrentChoice, + nChanges: this.numberOfChanges + }; + if ('undefined' === typeof opts.highlight) opts.highlight = true; + if (this.shuffleChoices) obj.order = this.order; + + // Option getValue backward compatible. + if (opts.addValue !== false && opts.getValue !== false) { + obj.value = this.currentChoice; + } + + if (null !== this.correctChoice || null !== this.requiredChoice || + null !== this.fixedChoice) { + obj.isCorrect = verif; + if (!obj.isCorrect && opts.highlight) this.highlight(); + } + if (obj.isCorrect === false) { + this.setError(this.getText('error', obj.value)); + } + return obj; + }; + + /** + * ### Dropdown.listeners + * + * Implements Widget.listeners + * + * Adds two listeners two disable/enable the widget on events: + * INPUT_DISABLE, INPUT_ENABLE + * + * @see Widget.listeners + */ + Dropdown.prototype.listeners = function () { + var that = this; + node.on('INPUT_DISABLE', function () { + that.disable(); + }); + node.on('INPUT_ENABLE', function () { + that.enable(); + }); + }; + + /** + * ### Dropdown.disable + * + * Enables the dropdown menu + */ + Dropdown.prototype.disable = function () { + if (this.disabled === true) return; + this.disabled = true; + if (this.menu) this.menu.removeEventListener('change', this.listener); + this.emit('disabled'); + }; + + /** + * ### Dropdown.enable + * + * Enables the dropdown menu + */ + Dropdown.prototype.enable = function () { + if (this.disabled === false) return; + if (!this.menu) { + 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 = this.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); /** @@ -54321,7 +55670,7 @@ if (!Array.prototype.indexOf) { // ## 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.'; @@ -54577,6 +55926,7 @@ if (!Array.prototype.indexOf) { var totalWinElement, totalWinParaElement, totalWinInputElement; var exitCodeElement, exitCodeParaElement, exitCodeInputElement; var exitCodeBtn, exitCodeGroup; + var basePay; var that = this; endScreenElement = document.createElement('div'); @@ -54643,6 +55993,11 @@ if (!Array.prototype.indexOf) { this.exitCodeInputElement = exitCodeInputElement; } + basePay = node.game.settings.BASE_PAY; + if ('undefined' !== typeof basePay) { + this.updateDisplay({ basePay: basePay, total: basePay }); + } + if (this.showEmailForm) { node.widgets.append(this.emailForm, endScreenElement, { title: false, @@ -54678,7 +56033,8 @@ if (!Array.prototype.indexOf) { document.execCommand('copy', false); inp.remove(); alert(this.getText('exitCopyMsg')); - } catch (err) { + } + catch (err) { alert(this.getText('exitCopyError')); } }; @@ -54721,7 +56077,6 @@ if (!Array.prototype.indexOf) { if ('undefined' !== typeof data.basePay) { preWin = data.basePay; - } if ('undefined' !== typeof data.bonus && @@ -54799,7 +56154,7 @@ if (!Array.prototype.indexOf) { /** * # Feedback - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Sends a feedback message to the server @@ -54881,12 +56236,6 @@ if (!Array.prototype.indexOf) { colOver = '#a32020'; // #f2dede'; colRemain = '#78b360'; // '#dff0d8'; - // ## Dependencies - - Feedback.dependencies = { - JSUS: {} - }; - /** * ## Feedback constructor * @@ -55063,6 +56412,13 @@ if (!Array.prototype.indexOf) { } } + // TODO: check this. + // if (this.minWords || this.minChars || this.maxWords || + // this.maxChars) { + // + // this.required = true; + // } + /** * ### Feedback.rows * @@ -57620,10 +58976,6 @@ if (!Array.prototype.indexOf) { // Backward compatibility. RiskGauge.texts.mainText = RiskGauge.texts.holt_laury_mainText; - // ## Dependencies - RiskGauge.dependencies = { - JSUS: {} - }; /** * ## RiskGauge constructor @@ -57976,6 +59328,15 @@ if (!Array.prototype.indexOf) { 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. @@ -58036,7 +59397,8 @@ if (!Array.prototype.indexOf) { // Main text. W.add('div', that.bodyDiv, { innerHTML: that.mainText || - that.getText('bomb_mainText', probBomb) + that.getText('bomb_mainText', probBomb), + className: 'bomb-maintext' }); // Slider. @@ -58154,6 +59516,8 @@ if (!Array.prototype.indexOf) { cl = 'bomb_' + (isWinner ? 'won' : 'lost'); bombResult.innerHTML = that.getText(cl); bombResult.className += (' ' + cl); + + if (that.onopen) that.onopen(isWinner, that); }; } }; @@ -58513,10 +59877,6 @@ if (!Array.prototype.indexOf) { // ## Dependencies - Slider.dependencies = { - JSUS: {} - }; - Slider.texts = { currentValue: function(widget, value) { return 'Value: ' + value; @@ -59373,8 +60733,7 @@ if (!Array.prototype.indexOf) { // ## Dependencies VisualRound.dependencies = { - GamePlot: {}, - JSUS: {} + GamePlot: {} }; /** @@ -59823,12 +61182,16 @@ if (!Array.prototype.indexOf) { // Compute current values. this.curStage = stage.stage; + // Stage can be indexed by id or number in the sequence. if ('string' === typeof this.curStage) { this.curStage = this.gamePlot.normalizeGameStage(stage).stage; } this.curStage -= this.stageOffset; + // 0.0.0 + if (this.curStage < 1) return; + this.curStep = stage.step; this.curRound = stage.round; @@ -60441,7 +61804,6 @@ if (!Array.prototype.indexOf) { // ## Dependencies VisualStage.dependencies = { - JSUS: {}, Table: {} }; @@ -60832,8 +62194,7 @@ if (!Array.prototype.indexOf) { // ## Dependencies VisualTimer.dependencies = { - GameTimer: {}, - JSUS: {} + GameTimer: {} }; /** @@ -60938,7 +62299,7 @@ if (!Array.prototype.indexOf) { * @see GameTimer */ VisualTimer.prototype.init = function(options) { - var t, gameTimerOptions; + var gameTimerOptions; // We keep the check for object, because this widget is often // called by users and the restart methods does not guarantee @@ -61025,7 +62386,7 @@ if (!Array.prototype.indexOf) { // 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', { @@ -61585,7 +62946,6 @@ if (!Array.prototype.indexOf) { // ## Dependencies WaitingRoom.dependencies = { - JSUS: {}, VisualTimer: {} }; @@ -62413,8 +63773,8 @@ if (!Array.prototype.indexOf) { 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(); } }; diff --git a/index.browser.js b/index.browser.js index e8bbc71c..fe7f82e8 100644 --- a/index.browser.js +++ b/index.browser.js @@ -21,6 +21,6 @@ node.support = JSUS.compatibility(); // Auto-Generated. - node.version = '7.0.0'; + node.version = '7.1.0'; })(window); diff --git a/lib/core/Game.js b/lib/core/Game.js index 82f87e60..214f262b 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1109,9 +1109,10 @@ // Make the done callback to send results. widgetDone = function() { - var values, opts; + var values, opts, req; + req = widgetObj.required || widgetObj.requiredChoice; // TODO: harmonize: required or checkValues? - if (widgetObj.required && widget.checkValues !== false) { + if (req && widget.checkValues !== false) { opts = { highlight: true, markAttempt: true }; } else { @@ -1124,7 +1125,7 @@ // If it is not timeup, and user did not // disabled it, check answers. - if (widgetObj.required && widget.checkValues !== false && + if (req && widget.checkValues !== false && !node.game.timer.isTimeup()) { // Widget must return some values (otherwise it From a98c0232c21878b3b370968432e17f7518f57df3 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 19 Jan 2022 13:41:04 +0100 Subject: [PATCH 50/51] minor --- build/nodegame-full.js | 844 +++++++++++++++++++++++++++++++---------- lib/core/Game.js | 6 +- 2 files changed, 643 insertions(+), 207 deletions(-) diff --git a/build/nodegame-full.js b/build/nodegame-full.js index c8ae9ed7..06e3d8b4 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -25426,9 +25426,11 @@ if (!Array.prototype.indexOf) { // Make the exit callback (destroy widget by default). if (widget.destroyOnExit !== false) { widgetExit = function() { - this[widget.ref].destroy(); + // It can happen with a gotoStep remote command. + if (!node.game[widget.ref]) return; + node.game[widget.ref].destroy(); // Remove node.game reference. - this[widget.ref] = null; + node.game[widget.ref] = null; }; // We are skipping the stage.exit property. exitCb = this.plot.getProperty(step, 'exit', @@ -40256,10 +40258,26 @@ if (!Array.prototype.indexOf) { * @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'); } }; @@ -42001,6 +42019,7 @@ if (!Array.prototype.indexOf) { 'pressed goes to the previous step.'; BackButton.title = false; + BackButton.panel = false; BackButton.className = 'backbutton'; BackButton.texts.back = 'Back'; @@ -42043,7 +42062,10 @@ if (!Array.prototype.indexOf) { 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) return; + if (node.widgets.last.prev() !== false) { + that.enable(); + return; + } } res = node.game.stepBack(that.stepOptions); if (res === false) that.enable(); @@ -42215,7 +42237,10 @@ if (!Array.prototype.indexOf) { * 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'); }; /** @@ -42224,7 +42249,10 @@ if (!Array.prototype.indexOf) { * Enables the back button */ BackButton.prototype.enable = function() { + if (!this.disabled) return; + this.disabled = false; this.button.disabled = false; + this.emit('enabled'); }; // ## Helper functions. @@ -45058,7 +45086,7 @@ if (!Array.prototype.indexOf) { /** * # ChoiceManager - * Copyright(c) 2021 Stefano Balietti + * Copyright(c) 2022 Stefano Balietti * MIT Licensed * * Creates and manages a set of selectable choices forms (e.g., ChoiceTable). @@ -45073,7 +45101,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceManager.version = '1.6.0'; + ChoiceManager.version = '1.7.0'; ChoiceManager.description = 'Groups together and manages a set of ' + 'survey forms (e.g., ChoiceTable).'; @@ -45082,7 +45110,9 @@ if (!Array.prototype.indexOf) { // ## Dependencies - ChoiceManager.dependencies = {}; + ChoiceManager.dependencies = { + BackButton: {}, DoneButton: {} + }; /** * ## ChoiceManager constructor @@ -45238,6 +45268,21 @@ if (!Array.prototype.indexOf) { * 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 methods @@ -45337,6 +45382,14 @@ if (!Array.prototype.indexOf) { // 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; + // After all configuration options are evaluated, add forms. if ('undefined' !== typeof options.forms) this.setForms(options.forms); @@ -45371,7 +45424,7 @@ if (!Array.prototype.indexOf) { * @see ChoiceManager.buildTableAndForms */ ChoiceManager.prototype.setForms = function(forms) { - var form, formsById, i, len, parsedForms, name; + var i, len, parsedForms; if ('function' === typeof forms) { parsedForms = forms.call(node.game); if (!J.isArray(parsedForms)) { @@ -45394,59 +45447,17 @@ if (!Array.prototype.indexOf) { } // Manual clone forms. - formsById = {}; - forms = new Array(len); + this.formsById = {}; + this.order = new Array(len); + this.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); - - // Display forms one by one. - if (this.oneByOne && this.oneByOneCounter !== i) { - form.hidden = true; - } - - if (form.conditional) { - this.conditionals[form.id] = form.conditional; - } - - 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; - } + this.addForm(parsedForms[i], false, i); + // Save the order in which the choices will be added. + this.order[i] = i; } - // Assigned verified forms. - this.forms = forms; - this.formsById = formsById; - // Save the order in which the choices will be added. - this.order = J.seq(0, len-1); + // Shuffle, if needed. if (this.shuffleForms) this.order = J.shuffle(this.order); }; @@ -45461,20 +45472,19 @@ if (!Array.prototype.indexOf) { * @see ChoiceManager.order */ ChoiceManager.prototype.buildDl = function() { - var i, len, dt; + var i, len; var form; i = -1, len = this.forms.length; 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); + appendDT(this.dl, form); } }; ChoiceManager.prototype.append = function() { + var div, opts; + // Id must be unique. if (W.getElementById(this.id)) { throw new Error('ChoiceManager.append: id is not ' + @@ -45507,6 +45517,25 @@ if (!Array.prototype.indexOf) { // 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); + } + } }; /** @@ -45545,6 +45574,78 @@ if (!Array.prototype.indexOf) { 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)) { + // TODO: smart checking form name. Maybe in Stager already? + name = form.name || 'ChoiceTable'; + // Add defaults. + J.mixout(form, this.formsOptions); + + 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; + } + + // Display forms one by one. + if (this.oneByOne && this.oneByOneCounter !== idx) { + form.hidden = true; + } + + if (form.conditional) { + this.conditionals[form.id] = form.conditional; + } + + form = node.widgets.get(name, form); + + } + + if (form.id) { + if (this.formsById[form.id]) { + throw new Error('ChoiceManager.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 * @@ -45817,24 +45918,27 @@ if (!Array.prototype.indexOf) { }; /** - * ### ChoiceManager.setValues + * ### ChoiceManager.next * * 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. + * @return {boolean} FALSE, if there is not another visualization. */ ChoiceManager.prototype.next = function() { - var form, conditional, failsafe; + var form, conditional, failsafe, that; if (!this.oneByOne) return false; if (!this.forms || !this.forms.length) { throw new Error('ChoiceManager.next: no forms found.'); } form = this.forms[this.oneByOneCounter]; - if (!form || form.next()) return false; + 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) { @@ -45842,26 +45946,65 @@ if (!Array.prototype.indexOf) { if (!form) return false; conditional = checkConditional(this, form.id); } - form.show(); + + if ('undefined' !== typeof $) { + $(form.panelDiv).fadeIn(); + form.hidden = false; // for nodeGame. + } + else { + form.show(); + } + that = this; + setTimeout(function() { + if (node.game.isPaused()) return; + if (that.backBtn) that.backBtn.enable(); + if (that.doneBtn) that.doneBtn.enable(); + }, 250); + + W.adjustFrameHeight(); node.emit('WIDGET_NEXT', this); + + return true; }; ChoiceManager.prototype.prev = function() { - var form; + var form, conditional, failsafe; if (!this.oneByOne) return false; if (!this.forms || !this.forms.length) { throw new Error('ChoiceManager.prev: no forms found.'); } form = this.forms[this.oneByOneCounter]; + if (!form) return false; if (form.prev()) return true; - if (this.oneByOneCounter <= 1) return false; + if (this.oneByOneCounter <= 0) return false; form.hide(); - this.oneByOneCounter--; - this.forms[this.oneByOneCounter].show(); + + 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(); + } 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. @@ -45891,14 +46034,28 @@ if (!Array.prototype.indexOf) { for (c in f) { if (f.hasOwnProperty(c)) { form = that.formsById[c]; + if (!form) continue; // No multiple choice allowed. - if (form && form.currentChoice !== f[c]) return false; + if (J.isArray(f[c])) { + if (!J.inArray(form.currentChoice, f[c])) return false; + } + else if (form.currentChoice !== f[c]) { + return false; + } } } } return true; } + function appendDT(dl, form) { + var dt; + dt = document.createElement('dt'); + dt.className = 'question'; + node.widgets.append(form, dt); + dl.appendChild(dt); + } + // In progress. // const createOnClick = (choice, question) => { // return function(value, removed, td) { @@ -46532,6 +46689,12 @@ if (!Array.prototype.indexOf) { * ### 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; @@ -46542,6 +46705,13 @@ if (!Array.prototype.indexOf) { */ this.solutionDisplayed = false; + /** + * ### ChoiceTable.solutionNoChoice + * + * TRUE, he solution is displayed upon trigger even with no choice + */ + this.solutionNoChoice = false; + /** * ### ChoiceTable.solutionDiv * @@ -46977,7 +47147,7 @@ if (!Array.prototype.indexOf) { * @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'); @@ -46985,6 +47155,9 @@ if (!Array.prototype.indexOf) { if (!choices.length) { throw new Error('ChoiceTable.setChoices: choices array is empty'); } + // Check and drop previous "other" choices. + idxOther = choices.indexOf(this.getText('other')); + if (this.other && idxOther >= 0) choices.splice(idxOther, 1); this.choices = choices; len = choices.length; @@ -46993,8 +47166,8 @@ if (!Array.prototype.indexOf) { if (this.shuffleChoices) this.order = J.shuffle(this.order); if (this.other) { - this.choices[len] = this.getText('other'); - this.order[len] = len + this.choices[len] = this.getText('other'); + this.order[len] = len } // Build the table and choices at once (faster). @@ -47551,7 +47724,7 @@ if (!Array.prototype.indexOf) { // Multiple selections allowed. // Make it an array (can be a string). - if (J.isArray(correctChoice)) correctChoice = [correctChoice]; + if (!J.isArray(correctChoice)) correctChoice = [correctChoice]; len = correctChoice.length; lenJ = this.currentChoice.length; @@ -47712,6 +47885,7 @@ if (!Array.prototype.indexOf) { */ ChoiceTable.prototype.unhighlight = function(opts) { var ci; + opts = opts || {}; if (!this.table || this.highlighted !== true) return; this.table.style.border = ''; ci = this.customInput; @@ -48066,9 +48240,12 @@ if (!Array.prototype.indexOf) { */ ChoiceTable.prototype.next = function() { var sol; - if (!this.solution || this.solutionDisplayed) return false; - this.solutionDisplayed = true; sol = this.solution; + // No solution or solution already displayed. + if (!sol || this.solutionDisplayed) return false; + // Solution, but no answer provided. + if (sol && !this.isChoiceDone() && !this.solutionNoChoice) return false; + this.solutionDisplayed = true; if ('function' === typeof sol) { sol = this.solution(this.verifyChoice(false), this); } @@ -48085,10 +48262,26 @@ if (!Array.prototype.indexOf) { this.solutionDiv.innerHTML = ''; this.enable(); W.adjustFrameHeight(); - node.emit('WIDGET_NEXT', this); + node.emit('WIDGET_PREV', this); return true; }; + ChoiceTable.prototype.isChoiceDone = function(complete) { + var cho, mul, len; + cho = this.currentChoice; + mul = this.selectMultiple; + // 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. /** @@ -48221,7 +48414,7 @@ if (!Array.prototype.indexOf) { // ## Dependencies ChoiceTableGroup.dependencies = { - JSUS: {} + ChoiceTable: {} }; /** @@ -49552,7 +49745,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - Consent.version = '0.3.0'; + Consent.version = '0.4.0'; Consent.description = 'Displays a configurable consent form.'; Consent.title = false; @@ -49659,6 +49852,10 @@ if (!Array.prototype.indexOf) { W.hide('notAgreed'); consent = W.gid('consent'); + if (!consent) { + throw new Error('Consent.append: the page does not contain an ' + + 'element with id "consent"'); + } html = ''; // Print. @@ -49673,9 +49870,9 @@ if (!Array.prototype.indexOf) { html += '' + this.getText('consentTerms') + '
'; // Buttons. - html += '
' + + html += '
' + '
'; @@ -51388,9 +51585,9 @@ if (!Array.prototype.indexOf) { if (that.required) res.err = that.getText('emptyErr'); } else if (tmp) { - res = tmp(value); + res = tmp.call(this, value); } - if (that.userValidation) that.userValidation(res); + if (that.userValidation) that.userValidation.call(this, res); return res; }; @@ -54057,6 +54254,7 @@ if (!Array.prototype.indexOf) { 'pressed emits node.done().'; DoneButton.title = false; + DoneButton.panel = false; DoneButton.className = 'donebutton'; DoneButton.texts.done = 'Done'; @@ -54481,7 +54679,7 @@ if (!Array.prototype.indexOf) { // Call onchange, if any. if (that.onchange) { - that.onchange(that.currentChoice, that); + that.onchange(that.currentChoice, menu, that); } }; @@ -55022,7 +55220,14 @@ if (!Array.prototype.indexOf) { return; } } - else if ('number' !== typeof choice) { + 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); } @@ -55070,7 +55275,7 @@ if (!Array.prototype.indexOf) { if (!this.choices || !this.choices.length) { throw new Error('Dropdown.setValues: no choices found.'); } - opts = opts || {}; + if ('undefined' === typeof opts) opts = {}; // TODO: this code is duplicated from ChoiceTable. if (opts.correct && this.correctChoice !== null) { @@ -55102,9 +55307,12 @@ if (!Array.prototype.indexOf) { opts = { values: opts }; } else if (opts && 'undefined' === typeof opts.values) { - opts.values = J.randomInt(this.choices.length) -1; + // 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); }; @@ -55174,7 +55382,7 @@ if (!Array.prototype.indexOf) { /** * ### Dropdown.disable * - * Enables the dropdown menu + * Disables the dropdown menu */ Dropdown.prototype.disable = function () { if (this.disabled === true) return; @@ -55995,7 +56203,9 @@ if (!Array.prototype.indexOf) { basePay = node.game.settings.BASE_PAY; if ('undefined' !== typeof basePay) { - this.updateDisplay({ basePay: basePay, total: basePay }); + this.updateDisplay({ + basePay: basePay, total: basePay, exitCode: '' + }); } if (this.showEmailForm) { @@ -59852,7 +60062,7 @@ if (!Array.prototype.indexOf) { /** * # Slider - * Copyright(c) 2020 Stefano Balietti + * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a configurable slider. @@ -59869,7 +60079,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - Slider.version = '0.4.0'; + Slider.version = '0.5.0'; Slider.description = 'Creates a configurable slider'; Slider.title = false; @@ -60024,6 +60234,18 @@ if (!Array.prototype.indexOf) { */ 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 @@ -60239,6 +60461,23 @@ if (!Array.prototype.indexOf) { } 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; + } }; /** @@ -60248,7 +60487,7 @@ if (!Array.prototype.indexOf) { * @param {object} opts Configuration options */ Slider.prototype.append = function() { - var container; + var container, tmp; // The original color of the rangeFill container (default black) // that is replaced upon highlighting. @@ -60276,6 +60515,14 @@ if (!Array.prototype.indexOf) { 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' @@ -60300,6 +60547,15 @@ if (!Array.prototype.indexOf) { if (this.sliderWidth) this.slider.style.width = this.sliderWidth; + + 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.displayValue) { this.valueSpan = W.add('span', this.bodyDiv, { className: 'slider-display-value' @@ -60359,11 +60615,36 @@ if (!Array.prototype.indexOf) { }; Slider.prototype.setValues = function(opts) { - opts = opts || {}; + if ('undefined' === typeof opts) opts = {}; + else if ('number' === typeof opts) opts = { value: opts }; this.slider.value = opts.value; this.slider.oninput(); }; + /** + * ### Slider.disable + * + * Disables the slider + */ + Slider.prototype.disable = function () { + if (this.disabled === true) return; + this.disabled = true; + this.slider.disabled = true; + this.emit('disabled'); + }; + + /** + * ### Slider.enable + * + * Enables the dropdown menu + */ + Slider.prototype.enable = function () { + if (this.disabled === false) return; + this.disabled = false; + this.slider.disabled = false; + this.emit('enabled'); + }; + })(node); /** @@ -60403,10 +60684,6 @@ if (!Array.prototype.indexOf) { left: 'Your Bonus:
Other\'s Bonus:' }; - // ## Dependencies - - SVOGauge.dependencies = {}; - /** * ## SVOGauge constructor * @@ -62923,7 +63200,7 @@ if (!Array.prototype.indexOf) { /** * # WaitingRoom - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2022 Stefano Balietti * MIT Licensed * * Displays the number of connected/required players to start a game @@ -62937,7 +63214,7 @@ if (!Array.prototype.indexOf) { 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'; @@ -63077,7 +63354,6 @@ if (!Array.prototype.indexOf) { // #### defaultTreatments defaultTreatments: 'Defaults:' - }; /** @@ -63262,6 +63538,20 @@ if (!Array.prototype.indexOf) { */ this.selectedTreatment = null; + + /** + * ### WaitingRoom.addDefaultTreatments + * + * If TRUE, after the user defined treatments, it adds default ones + * + * It has effect only if WaitingRoom.selectTreatmentOption is TRUE. + * + * Default: TRUE + * + * @see WaitingRoom.selectTreatmentOption + */ + this.addDefaultTreatments = null; + } // ## WaitingRoom methods @@ -63286,7 +63576,8 @@ if (!Array.prototype.indexOf) { * @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. ' + @@ -63375,137 +63666,280 @@ if (!Array.prototype.indexOf) { else this.playWithBotOption = false; if (conf.selectTreatmentOption) this.selectTreatmentOption = true; else this.selectTreatmentOption = false; + if ('undefined' === typeof conf.addDefaultTreatments) { + this.addDefaultTreatments = !!conf.addDefaultTreatments; + } + else { + this.addDefaultTreatments = true; + } + + // Button for bots and treatments. + if (conf.queryStringDispatch) { + this.queryStringTreatmentVariable = 'lang'; + t = J.getQueryString(this.queryStringTreatmentVariable); + + if (t) { + if (!conf.availableTreatments[t]) { + alert('Unknown t', t); + } + else { + node.say('PLAYWITHBOT', 'SERVER', t); + return; + } + } + } + if (conf.treatmentDisplayCb) { + this.treatmentDisplayCb = conf.treatmentDisplayCb; + } // 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); + // } + // } + // + // 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); + // } + // + // } + // + // 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; + // } + // // Append button group. + // w.bodyDiv.appendChild(document.createElement('br')); + // w.bodyDiv.appendChild(btnGroup); + // + // })(this); + // } 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; + // 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 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'; - 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; + var li, a, t, liT1, liT2, liT3, display, counter; + counter = 0; 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; + li = document.createElement('div'); + li.style.flex = '200px'; + // li.style.display = 'flex'; a = document.createElement('a'); + a.className = + 'btn-default btn-large round btn-icon'; a.href = '#'; - a.innerHTML = '' + t + ': ' + - conf.availableTreatments[t]; + if (w.treatmentDisplayCb) { + display = w.treatmentDisplayCb(t, + conf.availableTreatments[t], ++counter, w); + } + else { + display = '' + t + ': ' + + conf.availableTreatments[t]; + } + a.innerHTML = display; + a.id = t; li.appendChild(a); + + a.onclick = function() { + var t; + t = this.id; + // Clicked on description? + // btnTreatment.innerHTML = t + ' '; + w.selectedTreatment = t; + node.say('PLAYWITHBOT', 'SERVER', + w.selectedTreatment); + }; + 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); + else flexBox.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); - } - btnGroupTreatments.appendChild(btnTreatment); - btnGroupTreatments.appendChild(ul); + if (w.addDefaultTreatments !== false) { + flexBox.appendChild(liT1); + flexBox.appendChild(liT2); + flexBox.appendChild(liT3); + } + } - btnGroup.appendChild(btnGroupTreatments); + // 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'); + // + // btnGroupTreatments.appendChild(btnTreatment); - // 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 = ''; - } - }; + // btnGroup.appendChild(btnGroupTreatments); - 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; + // w.treatmentBtn = btnTreatment; } // Append button group. - w.bodyDiv.appendChild(document.createElement('br')); - w.bodyDiv.appendChild(btnGroup); + // w.bodyDiv.appendChild(document.createElement('br')); + // w.bodyDiv.appendChild(btnGroup); })(this); } diff --git a/lib/core/Game.js b/lib/core/Game.js index 214f262b..b4746409 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -1183,9 +1183,11 @@ // Make the exit callback (destroy widget by default). if (widget.destroyOnExit !== false) { widgetExit = function() { - this[widget.ref].destroy(); + // It can happen with a gotoStep remote command. + if (!node.game[widget.ref]) return; + node.game[widget.ref].destroy(); // Remove node.game reference. - this[widget.ref] = null; + node.game[widget.ref] = null; }; // We are skipping the stage.exit property. exitCb = this.plot.getProperty(step, 'exit', From 0249d472a92e8e7aa6720866d51059fc57011689 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 9 Mar 2022 12:50:52 +0100 Subject: [PATCH 51/51] tested automatic setting custom game properties in reconnect; commented --- bin/build.js | 2 +- build/nodegame-full.js | 816 ++++++++++++++++++++++++++++++++++------- lib/core/Game.js | 41 ++- lib/core/Session.js | 2 +- lib/core/SessionOld.js | 361 ++++++++++++++++++ 5 files changed, 1072 insertions(+), 150 deletions(-) create mode 100644 lib/core/SessionOld.js diff --git a/bin/build.js b/bin/build.js index d617304b..30036553 100755 --- a/bin/build.js +++ b/bin/build.js @@ -88,7 +88,7 @@ var ng_client = [ rootDir + "lib/core/Game.js", // Not used for now. - // rootDir + "lib/core/Session.js", + rootDir + "lib/core/Session.js", rootDir + "lib/core/Timer.js", diff --git a/build/nodegame-full.js b/build/nodegame-full.js index 06e3d8b4..8d42b205 100644 --- a/build/nodegame-full.js +++ b/build/nodegame-full.js @@ -26552,22 +26552,22 @@ if (!Array.prototype.indexOf) { * object itself as parameter * * @param {Game} game The game instance - * @param {object} options The options to process + * @param {object} opts The options to process * * @see Game.gotoStep * @see GamePlot.tmpCache * @see Game.willBeDone * @see Game.beDone */ - function processGotoStepOptions(game, options) { + function processGotoStepOptions(game, opts) { var prop; // Be done.. now! Skips Game.execStep. - if (options.beDone) { + if (opts.beDone) { game.willBeDone = true; game.beDone = true; } - else if (options.willBeDone) { + else if (opts.willBeDone) { // TODO: why not setting willBeDone? It was not working, check! // Call node.done() immediately after PLAYING is emitted. game.node.once('PLAYING', function() { @@ -26577,30 +26577,38 @@ if (!Array.prototype.indexOf) { // Temporarily modify plot properties. // Must be done after setting the role. - if (options.plot) { - for (prop in options.plot) { - if (options.plot.hasOwnProperty(prop)) { - game.plot.tmpCache(prop, options.plot[prop]); + if (opts.plot) { + for (prop in opts.plot) { + if (opts.plot.hasOwnProperty(prop)) { + game.plot.tmpCache(prop, opts.plot[prop]); } } } - if (options.msgs) { - options.msgs.foreach(function(msg) { + if (opts.msgs) { + opts.msgs.foreach(function(msg) { game.node.socket.onMessage(new GameMsg(msg).toInEvent(), msg); }); } + if (opts.game) { + for (prop in opts.game) { + if (opts.game.hasOwnProperty(prop)) { + game[prop] = opts.game[prop]; + } + } + } + // TODO: rename cb. - // Call the cb with options as param, if found. - if (options.cb) { - if ('function' === typeof options.cb) { - options.cb.call(game, options); + // Call the cb with opts as param, if found. + if (opts.cb) { + if ('function' === typeof opts.cb) { + opts.cb.call(game, opts); } else { - throw new TypeError('Game.gotoStep: options.cb must be ' + + throw new TypeError('Game.gotoStep: opts.cb must be ' + 'function or undefined. Found: ' + - options.cb); + opts.cb); } } } @@ -26611,6 +26619,360 @@ if (!Array.prototype.indexOf) { 'undefined' != typeof node ? node : module.parent.exports ); +/** + * # GameSession + * Copyright(c) 2022 Stefano Balietti + * MIT Licensed + * + * `nodeGame` session manager + */ +(function(exports, node) { + + "use strict"; + + // ## Global scope + + var J = node.JSUS; + + // Exposing constructor. + exports.GameSession = GameSession; + exports.GameSession.SessionManager = SessionManager; + + GameSession.prototype = new SessionManager(); + GameSession.prototype.constructor = GameSession; + + /** + * ## GameSession constructor + * + * Creates a new instance of GameSession + * + * @param {NodeGameClient} node A reference to the node object. + */ + function GameSession(node) { + SessionManager.call(this); + + /** + * ### GameSession.node + * + * The reference to the node object. + */ + this.node = node; + + // Register default variables in the session. + this.register('player', { + set: function(p) { + node.createPlayer(p); + }, + get: function() { + return node.player; + } + }); + + this.register('game.memory', { + set: function(value) { + node.game.memory.clear(true); + node.game.memory.importDB(value); + }, + get: function() { + return (node.game.memory) ? node.game.memory.fetch() : null; + } + }); + + this.register('events.history', { + set: function(value) { + node.events.history.history.clear(true); + node.events.history.history.importDB(value); + }, + get: function() { + return node.events.history ? + node.events.history.history.fetch() : null; + } + }); + + this.register('stage', { + set: function() { + // GameSession.restoreStage + }, + get: function() { + return node.player.stage; + } + }); + + this.register('node.env'); + } + + +// GameSession.prototype.restoreStage = function(stage) { +// +// try { +// // GOTO STATE +// node.game.execStage(node.plot.getStep(stage)); +// +// var discard = ['LOG', +// 'STATECHANGE', +// 'WINDOW_LOADED', +// 'BEFORE_LOADING', +// 'LOADED', +// 'in.say.STATE', +// 'UPDATED_PLIST', +// 'NODEGAME_READY', +// 'out.say.STATE', +// 'out.set.STATE', +// 'in.say.PLIST', +// 'STAGEDONE', // maybe not here +// 'out.say.HI' +// ]; +// +// // RE-EMIT EVENTS +// node.events.history.remit(node.game.getStateLevel(), discard); +// node.info('game stage restored'); +// return true; +// } +// catch(e) { +// node.err('could not restore game stage. ' + +// 'An error has occurred: ' + e); +// return false; +// } +// +// }; + + /** + * ## SessionManager constructor + * + * Creates a new session manager. + */ + function SessionManager() { + + /** + * ### SessionManager.session + * + * Container of all variables registered in the session. + */ + this.session = {}; + } + + // ## SessionManager methods + + /** + * ### SessionManager.getVariable (static) + * + * Default session getter. + * + * @param {string} p The path to a variable included in _node_ + * @return {mixed} The requested variable + */ + SessionManager.getVariable = function(p) { + return J.getNestedValue(p, node); + }; + + /** + * ### SessionManager.setVariable (static) + * + * Default session setter. + * + * @param {string} p The path to the variable to set in _node_ + * @param {mixed} value The value to set + */ + SessionManager.setVariable = function(p, value) { + J.setNestedValue(p, value, node); + }; + + /** + * ### SessionManager.register + * + * Register a new variable to the session + * + * Overwrites previously registered variables with the same name. + * + * Usage example: + * + * ```javascript + * node.session.register('player', { + * set: function(p) { + * node.createPlayer(p); + * }, + * get: function() { + * return node.player; + * } + * }); + * ``` + * + * @param {string} path A string containing a path to a variable + * @param {object} conf Optional. Configuration object containing setters + * and getters + */ + SessionManager.prototype.register = function(path, conf) { + if ('string' !== typeof path) { + throw new TypeError('SessionManager.register: path must be ' + + 'string.'); + } + if (conf && 'object' !== typeof conf) { + throw new TypeError('SessionManager.register: conf must be ' + + 'object or undefined.'); + } + + this.session[path] = { + + get: (conf && conf.get) ? + conf.get : function() { + return J.getNestedValue(path, node); + }, + + set: (conf && conf.set) ? + conf.set : function(value) { + J.setNestedValue(path, value, node); + } + }; + + return this.session[path]; + }; + + /** + * ### SessionManager.unregister + * + * Unegister a variable from session + * + * @param {string} path A string containing a path to a variable previously + * registered. + * + * @see SessionManager.register + */ + SessionManager.prototype.unregister = function(path) { + if ('string' !== typeof path) { + throw new TypeError('SessionManager.unregister: path must be ' + + 'string.'); + } + if (!this.session[path]) { + node.warn('SessionManager.unregister: path is not registered ' + + 'in the session: ' + path + '.'); + return false; + } + + delete this.session[path]; + return true; + }; + + /** + * ### SessionManager.clear + * + * Unegister all registered session variables + * + * @see SessionManager.unregister + */ + SessionManager.prototype.clear = function() { + this.session = {}; + }; + + /** + * ### SessionManager.get + * + * Returns the value/s of one/all registered session variable/s + * + * @param {string|undefined} path A previously registred variable or + * undefined to return all values + * + * @see SessionManager.register + */ + SessionManager.prototype.get = function(path) { + var session = {}; + // Returns one variable. + if ('string' === typeof path) { + return this.session[path] ? this.session[path].get() : undefined; + } + // Returns all registered variables. + else if ('undefined' === typeof path) { + for (path in this.session) { + if (this.session.hasOwnProperty(path)) { + session[path] = this.session[path].get(); + } + } + return session; + } + else { + throw new TypeError('SessionManager.get: path must be string or ' + + 'undefined.'); + } + }; + + /** + * ### SessionManager.isRegistered + * + * Returns TRUE, if a variable is registred + * + * @param {string} path A previously registred variable + * + * @return {boolean} TRUE, if the variable is registered + * + * @see SessionManager.register + * @see SessionManager.unregister + */ + SessionManager.prototype.isRegistered = function(path) { + if ('string' !== typeof path) { + throw new TypeError('SessionManager.isRegistered: path must be ' + + 'string.'); + } + return this.session.hasOwnProperty(path); + }; + + /** + * ### SessionManager.serialize + * + * Returns an object containing that can be to restore the session + * + * The serialized session is an object containing _getter_, _setter_, and + * current value of each of the registered session variables. + * + * @return {object} session The serialized session + * + * @see SessionManager.restore + */ + SessionManager.prototype.serialize = function() { + var session = {}; + for (var path in this.session) { + if (this.session.hasOwnProperty(path)) { + session[path] = { + value: this.session[path].get(), + get: this.session[path].get, + set: this.session[path].set + }; + } + } + return session; + }; + + /** + * ### SessionManager.restore + * + * Restore a previously serialized session object + * + * @param {object} session A serialized session object + * @param {boolean} register Optional. If TRUE, every path is also + * registered before being restored. + */ + SessionManager.prototype.restore = function(session, register) { + var i; + if ('object' !== typeof session) { + throw new TypeError('SessionManager.restore: session must be ' + + 'object.'); + } + register = 'undefined' !== typeof register ? register : true; + for (i in session) { + if (session.hasOwnProperty(i)) { + if (register) this.register(i, session[i]); + session[i].set(session[i].value); + } + } + }; + +// SessionManager.prototype.store = function() { +// //node.store(node.socket.id, this.get()); +// }; + +})( + 'undefined' != typeof node ? node : module.exports, + 'undefined' != typeof node ? node : module.parent.exports +); + /** * # Timer * Copyright(c) 2021 Stefano Balietti @@ -37721,7 +38083,7 @@ if (!Array.prototype.indexOf) { /** * # extra - * Copyright(c) 2019 Stefano Balietti + * Copyright(c) 2022 Stefano Balietti * MIT Licensed * * GameWindow extras @@ -37942,24 +38304,25 @@ if (!Array.prototype.indexOf) { * and a method stop, that clears the interval */ GameWindow.prototype.getLoadingDots = function(len, id) { - var spanDots, i, limit, intervalId; + var spanDots, counter, intervalId; if (len & len < 0) { throw new Error('GameWindow.getLoadingDots: len cannot be < 0. ' + 'Found: ' + len); } - len = len || 5; spanDots = document.createElement('span'); spanDots.id = id || 'span_dots'; - limit = ''; - for (i = 0; i < len; i++) { - limit = limit + '.'; - } // Refreshing the dots... + counter = 0; + len = len || 5; + // So the height does not change. + spanDots.innerHTML = ' '; intervalId = setInterval(function() { - if (spanDots.innerHTML !== limit) { + if (counter < len) { + counter++; spanDots.innerHTML = spanDots.innerHTML + '.'; } else { + counter = 0; spanDots.innerHTML = '.'; } }, 1000); @@ -38144,8 +38507,13 @@ if (!Array.prototype.indexOf) { }; + GameWindow.prototype.setInnerHTML = function(search, replace, mod) { + // console.log('***deprecated: use W.html instead of W.setInnerHTML'); + this.html(search, replace, mod); + }; + /** - * ### GameWindow.setInnerHTML + * ### GameWindow.html * * Replaces the innerHTML of the element with matching id or class name * @@ -38158,19 +38526,21 @@ if (!Array.prototype.indexOf) { * - 'className': replaces all elements with same class name * - 'g': replaces globally, both by id and className */ - GameWindow.prototype.setInnerHTML = function(search, replace, mod) { + GameWindow.prototype.html = function(search, replace, mod) { var el, i, len; // Only process strings or numbers. if ('string' !== typeof search && 'number' !== typeof search) { throw new TypeError('GameWindow.setInnerHTML: search must be ' + - 'string or number. Found: ' + search); + 'string or number. Found: ' + search + + " (replace = " + replace + ")"); } // Only process strings or numbers. if ('string' !== typeof replace && 'number' !== typeof replace) { throw new TypeError('GameWindow.setInnerHTML: replace must be ' + - 'string or number. Found: ' + replace); + 'string or number. Found: ' + replace + + " (search = " + search + ")"); } if ('undefined' === typeof mod) { @@ -38179,12 +38549,14 @@ if (!Array.prototype.indexOf) { else if ('string' === typeof mod) { if (mod !== 'g' && mod !== 'id' && mod !== 'className') { throw new Error('GameWindow.setInnerHTML: invalid ' + - 'mod value: ' + mod); + 'mod value: ' + mod + + " (search = " + search + ")"); } } else { throw new TypeError('GameWindow.setInnerHTML: mod must be ' + - 'string or undefined. Found: ' + mod); + 'string or undefined. Found: ' + mod + + " (search = " + search + ")"); } if (mod === 'id' || mod === 'g') { @@ -40470,7 +40842,7 @@ if (!Array.prototype.indexOf) { // 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); @@ -40487,6 +40859,9 @@ if (!Array.prototype.indexOf) { 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, ' + @@ -41397,6 +41772,7 @@ if (!Array.prototype.indexOf) { // Properties that will modify the UI of the widget once appended. + if (options.bootstrap5) widget._bootstrap5 = true; if (options.disabled) widget._disabled = true; if (options.highlighted) widget._highlighted = true; if (options.collapsed) widget._collapsed = true; @@ -41515,7 +41891,7 @@ if (!Array.prototype.indexOf) { }; /** - * ### Widgets.append + * ### Widgets.append|add * * Appends a widget to the specified root element * @@ -41539,6 +41915,7 @@ if (!Array.prototype.indexOf) { * * @see Widgets.get */ + Widgets.prototype.add = Widgets.prototype.append = function(w, root, options) { var tmp; @@ -41586,7 +41963,7 @@ if (!Array.prototype.indexOf) { // 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 ] : @@ -41614,7 +41991,7 @@ if (!Array.prototype.indexOf) { // 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'; @@ -41629,7 +42006,7 @@ if (!Array.prototype.indexOf) { } // Add body (with or without panel). - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5. tmp = options.panel !== false ? 'card-body' : 'no-panel-body'; } @@ -41642,7 +42019,7 @@ if (!Array.prototype.indexOf) { // Optionally add footer. if (w.footer) { - if (options.bootstrap5) { + if (w._bootstrap5) { // Bootstrap 5. tmp = options.panel === false ? 'no-panel-heading' : 'card-footer'; @@ -41687,12 +42064,6 @@ if (!Array.prototype.indexOf) { 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 * @@ -45101,7 +45472,7 @@ if (!Array.prototype.indexOf) { // ## Meta-data - ChoiceManager.version = '1.7.0'; + ChoiceManager.version = '1.8.0'; ChoiceManager.description = 'Groups together and manages a set of ' + 'survey forms (e.g., ChoiceTable).'; @@ -45283,6 +45654,13 @@ if (!Array.prototype.indexOf) { */ this.backBtn = null; + /** + * ### ChoiceManager.honeypot + * + * Array of unused input forms to detect bots. + */ + this.honeypot = null; + } // ## ChoiceManager methods @@ -45390,9 +45768,13 @@ if (!Array.prototype.indexOf) { // 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; + // After all configuration options are evaluated, add forms. if ('undefined' !== typeof options.forms) this.setForms(options.forms); + }; /** @@ -45536,6 +45918,9 @@ if (!Array.prototype.indexOf) { this.doneBtn = node.widgets.append('DoneButton', div, opts); } } + + + if (this.honeypot) this.addHoneypot(this.honeypot); }; /** @@ -45610,6 +45995,10 @@ if (!Array.prototype.indexOf) { this.conditionals[form.id] = form.conditional; } + if (this._bootstrap5 && 'undefined' === typeof form.bootstrap5) { + form.bootstrap5 = true; + } + form = node.widgets.get(name, form); } @@ -45815,36 +46204,39 @@ if (!Array.prototype.indexOf) { 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; - } - - } + // 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 { + // else { i = -1; for ( ; ++i < len ; ) { form = this.forms[i]; @@ -45866,7 +46258,7 @@ if (!Array.prototype.indexOf) { if (res) lastErrored = res; } } - } + // } if (lastErrored) { if (opts.highlight && @@ -45890,6 +46282,14 @@ if (!Array.prototype.indexOf) { 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; }; @@ -45917,6 +46317,69 @@ if (!Array.prototype.indexOf) { 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('ChoiceManager.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' } + ]; + } + + // 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 * @@ -45954,6 +46417,8 @@ if (!Array.prototype.indexOf) { else { form.show(); } + window.scrollTo(0,0); + that = this; setTimeout(function() { if (node.game.isPaused()) return; @@ -45995,6 +46460,8 @@ if (!Array.prototype.indexOf) { else { form.show(); } + window.scrollTo(0,0); + W.adjustFrameHeight(); node.emit('WIDGET_PREV', this); @@ -46031,6 +46498,9 @@ if (!Array.prototype.indexOf) { var f, c, form; f = that.conditionals[id]; if (f) { + if ('function' === typeof f) { + return f.call(that, that.formsById); + } for (c in f) { if (f.hasOwnProperty(c)) { form = that.formsById[c]; @@ -46197,21 +46667,26 @@ if (!Array.prototype.indexOf) { * @see ChoiceTable.onclick */ this.listener = function(e) { - var name, value, td; + var name, value, td, ci; var i, len, removed, other; 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; + } } } } @@ -46243,21 +46718,21 @@ if (!Array.prototype.indexOf) { // One more click. that.numberOfClicks++; + removed = that.isChoiceCurrent(value); len = that.choices.length; if (that.customInput) { // Is "Other" currently selected? - other = value === (len - 1); - if (that.customInput.isHidden()) { - if (other) that.customInput.show(); + if (value === (len - 1) && !removed) { + that.customInput.show(); } else { - if (other) that.customInput.hide(); + that.customInput.hide(); } } // Click on an already selected choice. - if (that.isChoiceCurrent(value)) { + if (removed) { that.unsetCurrentChoice(value); J.removeClass(td, 'selected'); @@ -46274,7 +46749,6 @@ if (!Array.prototype.indexOf) { else { that.selected = null; } - removed = true; } // Click on a new choice. else { @@ -46892,24 +47366,42 @@ if (!Array.prototype.indexOf) { } // 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.hint += ' *'; + } + 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. @@ -46949,10 +47441,18 @@ if (!Array.prototype.indexOf) { '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)) { @@ -46960,19 +47460,24 @@ if (!Array.prototype.indexOf) { 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) { @@ -47034,11 +47539,14 @@ if (!Array.prototype.indexOf) { // 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) { @@ -47047,7 +47555,7 @@ if (!Array.prototype.indexOf) { 'right options are set.'); } - this.choicesSetSize = opts.choicesSetSize; + this.choicesSetSize = tmp; } // Add other. @@ -47056,35 +47564,51 @@ if (!Array.prototype.indexOf) { } // 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'); } + 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 TypeError('ChoiceTable.init: disabledChoices ' + 'must be undefined or array. Found: ' + - opts.disabledChoices); + 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 (var i = 0; i < tmp.length; i++) { + that.disableChoice(tmp[i]); } })(); } @@ -48244,12 +48768,14 @@ if (!Array.prototype.indexOf) { // No solution or solution already displayed. if (!sol || this.solutionDisplayed) return false; // Solution, but no answer provided. - if (sol && !this.isChoiceDone() && !this.solutionNoChoice) return false; - this.solutionDisplayed = true; - if ('function' === typeof sol) { - sol = this.solution(this.verifyChoice(false), this); + 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.solutionDiv.innerHTML = sol; this.disable(); W.adjustFrameHeight(); node.emit('WIDGET_NEXT', this); @@ -48257,6 +48783,7 @@ if (!Array.prototype.indexOf) { }; ChoiceTable.prototype.prev = function() { + return false; if (!this.solutionDisplayed) return false; this.solutionDisplayed = false; this.solutionDiv.innerHTML = ''; @@ -48267,9 +48794,12 @@ if (!Array.prototype.indexOf) { }; ChoiceTable.prototype.isChoiceDone = function(complete) { - var cho, mul, len; + 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. @@ -49847,7 +50377,7 @@ if (!Array.prototype.indexOf) { }; Consent.prototype.append = function() { - var consent, html; + var consent, html, btn1, btn2, st1, st2; // Hide not agreed div. W.hide('notAgreed'); @@ -49870,11 +50400,27 @@ if (!Array.prototype.indexOf) { html += '' + this.getText('consentTerms') + '
'; // Buttons. - html += '
' + - '
'; + html += '
'; + + if (document.querySelector('html').dir === 'rtl') { + btn1 = 'agree'; + btn2 = 'notAgree'; + st1 = 'info'; + st2 = 'danger'; + } + else { + btn1 = 'notAgree'; + btn2 = 'agree'; + st1 = 'danger'; + st2 = 'info'; + } + + html += ''; + + html += '
'; consent.innerHTML += html; setTimeout(function() { W.adjustFrameHeight(); }); @@ -63874,6 +64420,7 @@ if (!Array.prototype.indexOf) { if (conf.availableTreatments.hasOwnProperty(t)) { li = document.createElement('div'); li.style.flex = '200px'; + li.style['margin-top'] = '10px'; // li.style.display = 'flex'; a = document.createElement('a'); a.className = @@ -63908,6 +64455,11 @@ if (!Array.prototype.indexOf) { } } + li = document.createElement('div'); + li.style.flex = '200px'; + li.style['margin-top'] = '10px'; + // Hack to fit nicely the treatments. + flexBox.appendChild(li); if (w.addDefaultTreatments !== false) { flexBox.appendChild(liT1); diff --git a/lib/core/Game.js b/lib/core/Game.js index b4746409..5dcf598d 100644 --- a/lib/core/Game.js +++ b/lib/core/Game.js @@ -2309,22 +2309,22 @@ * object itself as parameter * * @param {Game} game The game instance - * @param {object} options The options to process + * @param {object} opts The options to process * * @see Game.gotoStep * @see GamePlot.tmpCache * @see Game.willBeDone * @see Game.beDone */ - function processGotoStepOptions(game, options) { + function processGotoStepOptions(game, opts) { var prop; // Be done.. now! Skips Game.execStep. - if (options.beDone) { + if (opts.beDone) { game.willBeDone = true; game.beDone = true; } - else if (options.willBeDone) { + else if (opts.willBeDone) { // TODO: why not setting willBeDone? It was not working, check! // Call node.done() immediately after PLAYING is emitted. game.node.once('PLAYING', function() { @@ -2334,30 +2334,39 @@ // Temporarily modify plot properties. // Must be done after setting the role. - if (options.plot) { - for (prop in options.plot) { - if (options.plot.hasOwnProperty(prop)) { - game.plot.tmpCache(prop, options.plot[prop]); + if (opts.plot) { + for (prop in opts.plot) { + if (opts.plot.hasOwnProperty(prop)) { + game.plot.tmpCache(prop, opts.plot[prop]); } } } - if (options.msgs) { - options.msgs.foreach(function(msg) { + if (opts.msgs) { + opts.msgs.foreach(function(msg) { game.node.socket.onMessage(new GameMsg(msg).toInEvent(), msg); }); } + // Experimental. To be replaced by a session manager. + // if (opts.game) { + // for (prop in opts.game) { + // if (opts.game.hasOwnProperty(prop)) { + // game[prop] = opts.game[prop]; + // } + // } + // } + // TODO: rename cb. - // Call the cb with options as param, if found. - if (options.cb) { - if ('function' === typeof options.cb) { - options.cb.call(game, options); + // Call the cb with opts as param, if found. + if (opts.cb) { + if ('function' === typeof opts.cb) { + opts.cb.call(game, opts); } else { - throw new TypeError('Game.gotoStep: options.cb must be ' + + throw new TypeError('Game.gotoStep: opts.cb must be ' + 'function or undefined. Found: ' + - options.cb); + opts.cb); } } } diff --git a/lib/core/Session.js b/lib/core/Session.js index 7c34b9ce..2c9e5f92 100644 --- a/lib/core/Session.js +++ b/lib/core/Session.js @@ -1,6 +1,6 @@ /** * # GameSession - * Copyright(c) 2015 Stefano Balietti + * Copyright(c) 2022 Stefano Balietti * MIT Licensed * * `nodeGame` session manager diff --git a/lib/core/SessionOld.js b/lib/core/SessionOld.js new file mode 100644 index 00000000..30029483 --- /dev/null +++ b/lib/core/SessionOld.js @@ -0,0 +1,361 @@ +/** + * # GameSession + * Copyright(c) 2015 Stefano Balietti + * MIT Licensed + * + * `nodeGame` session manager + */ +(function(exports, node) { + + "use strict"; + + // ## Global scope + + var J = node.JSUS; + + // Exposing constructor. + exports.GameSession = GameSession; + exports.GameSession.SessionManager = SessionManager; + + GameSession.prototype = new SessionManager(); + GameSession.prototype.constructor = GameSession; + + /** + * ## GameSession constructor + * + * Creates a new instance of GameSession + * + * @param {NodeGameClient} node A reference to the node object. + */ + function GameSession(node) { + SessionManager.call(this); + + /** + * ### GameSession.node + * + * The reference to the node object. + */ + this.node = node; + + // Register default variables in the session. + this.register('player', { + set: function(p) { + node.createPlayer(p); + }, + get: function() { + return node.player; + } + }); + + this.register('game.memory', { + set: function(value) { + node.game.memory.clear(true); + node.game.memory.importDB(value); + }, + get: function() { + return (node.game.memory) ? node.game.memory.fetch() : null; + } + }); + + this.register('events.history', { + set: function(value) { + node.events.history.history.clear(true); + node.events.history.history.importDB(value); + }, + get: function() { + return node.events.history ? + node.events.history.history.fetch() : null; + } + }); + + this.register('stage', { + set: function() { + // GameSession.restoreStage + }, + get: function() { + return node.player.stage; + } + }); + + this.register('node.env'); + } + + +// GameSession.prototype.restoreStage = function(stage) { +// +// try { +// // GOTO STATE +// node.game.execStage(node.plot.getStep(stage)); +// +// var discard = ['LOG', +// 'STATECHANGE', +// 'WINDOW_LOADED', +// 'BEFORE_LOADING', +// 'LOADED', +// 'in.say.STATE', +// 'UPDATED_PLIST', +// 'NODEGAME_READY', +// 'out.say.STATE', +// 'out.set.STATE', +// 'in.say.PLIST', +// 'STAGEDONE', // maybe not here +// 'out.say.HI' +// ]; +// +// // RE-EMIT EVENTS +// node.events.history.remit(node.game.getStateLevel(), discard); +// node.info('game stage restored'); +// return true; +// } +// catch(e) { +// node.err('could not restore game stage. ' + +// 'An error has occurred: ' + e); +// return false; +// } +// +// }; + + /** + * ## SessionManager constructor + * + * Creates a new session manager. + */ + function SessionManager() { + + /** + * ### SessionManager.session + * + * Container of all variables registered in the session. + */ + this.session = { + vars: {}, + add: function(name, value) { + + }, + get: function(name, value) { + + } + }; + } + + // ## SessionManager methods + + /** + * ### SessionManager.getVariable (static) + * + * Default session getter. + * + * @param {string} p The path to a variable included in _node_ + * @return {mixed} The requested variable + */ + SessionManager.getVariable = function(p) { + return J.getNestedValue(p, node); + }; + + /** + * ### SessionManager.setVariable (static) + * + * Default session setter. + * + * @param {string} p The path to the variable to set in _node_ + * @param {mixed} value The value to set + */ + SessionManager.setVariable = function(p, value) { + J.setNestedValue(p, value, node); + }; + + /** + * ### SessionManager.register + * + * Register a new variable to the session + * + * Overwrites previously registered variables with the same name. + * + * Usage example: + * + * ```javascript + * node.session.register('player', { + * set: function(p) { + * node.createPlayer(p); + * }, + * get: function() { + * return node.player; + * } + * }); + * ``` + * + * @param {string} path A string containing a path to a variable + * @param {object} conf Optional. Configuration object containing setters + * and getters + */ + SessionManager.prototype.register = function(path, conf) { + if ('string' !== typeof path) { + throw new TypeError('SessionManager.register: path must be ' + + 'string.'); + } + if (conf && 'object' !== typeof conf) { + throw new TypeError('SessionManager.register: conf must be ' + + 'object or undefined.'); + } + + this.session[path] = { + + get: (conf && conf.get) ? + conf.get : function() { + return J.getNestedValue(path, node); + }, + + set: (conf && conf.set) ? + conf.set : function(value) { + J.setNestedValue(path, value, node); + } + }; + + return this.session[path]; + }; + + /** + * ### SessionManager.unregister + * + * Unegister a variable from session + * + * @param {string} path A string containing a path to a variable previously + * registered. + * + * @see SessionManager.register + */ + SessionManager.prototype.unregister = function(path) { + if ('string' !== typeof path) { + throw new TypeError('SessionManager.unregister: path must be ' + + 'string.'); + } + if (!this.session[path]) { + node.warn('SessionManager.unregister: path is not registered ' + + 'in the session: ' + path + '.'); + return false; + } + + delete this.session[path]; + return true; + }; + + /** + * ### SessionManager.clear + * + * Unegister all registered session variables + * + * @see SessionManager.unregister + */ + SessionManager.prototype.clear = function() { + this.session = {}; + }; + + /** + * ### SessionManager.get + * + * Returns the value/s of one/all registered session variable/s + * + * @param {string|undefined} path A previously registred variable or + * undefined to return all values + * + * @see SessionManager.register + */ + SessionManager.prototype.get = function(path) { + var session = {}; + // Returns one variable. + if ('string' === typeof path) { + return this.session[path] ? this.session[path].get() : undefined; + } + // Returns all registered variables. + else if ('undefined' === typeof path) { + for (path in this.session) { + if (this.session.hasOwnProperty(path)) { + session[path] = this.session[path].get(); + } + } + return session; + } + else { + throw new TypeError('SessionManager.get: path must be string or ' + + 'undefined.'); + } + }; + + /** + * ### SessionManager.isRegistered + * + * Returns TRUE, if a variable is registred + * + * @param {string} path A previously registred variable + * + * @return {boolean} TRUE, if the variable is registered + * + * @see SessionManager.register + * @see SessionManager.unregister + */ + SessionManager.prototype.isRegistered = function(path) { + if ('string' !== typeof path) { + throw new TypeError('SessionManager.isRegistered: path must be ' + + 'string.'); + } + return this.session.hasOwnProperty(path); + }; + + /** + * ### SessionManager.serialize + * + * Returns an object containing that can be to restore the session + * + * The serialized session is an object containing _getter_, _setter_, and + * current value of each of the registered session variables. + * + * @return {object} session The serialized session + * + * @see SessionManager.restore + */ + SessionManager.prototype.serialize = function() { + var session = {}; + for (var path in this.session) { + if (this.session.hasOwnProperty(path)) { + session[path] = { + value: this.session[path].get(), + get: this.session[path].get, + set: this.session[path].set + }; + } + } + return session; + }; + + /** + * ### SessionManager.restore + * + * Restore a previously serialized session object + * + * @param {object} session A serialized session object + * @param {boolean} register Optional. If TRUE, every path is also + * registered before being restored. + */ + SessionManager.prototype.restore = function(session, register) { + var i; + if ('object' !== typeof session) { + throw new TypeError('SessionManager.restore: session must be ' + + 'object.'); + } + register = 'undefined' !== typeof register ? register : true; + for (i in session) { + if (session.hasOwnProperty(i)) { + if (register) this.register(i, session[i]); + session[i].set(session[i].value); + } + } + }; + +// SessionManager.prototype.store = function() { +// //node.store(node.socket.id, this.get()); +// }; + +})( + 'undefined' != typeof node ? node : module.exports, + 'undefined' != typeof node ? node : module.parent.exports +);