UNPKG

nodegame-widgets

Version:

Collections of useful and reusable javascript / HTML snippets for nodeGame

1,478 lines (1,321 loc) 69.6 kB
/** * # ChoiceTable * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a configurable table where each cell is a selectable choice * * // TODO: register time for each current choice if selectMultiple is on? * * www.nodegame.org */ (function(node) { "use strict"; node.widgets.register('ChoiceTable', ChoiceTable); // ## Meta-data ChoiceTable.version = '1.8.1'; ChoiceTable.description = 'Creates a configurable table where ' + 'each cell is a selectable choice.'; 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; } else { res += 'multiple selection allowed'; } } else { 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 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.'; } // correct: 'Correct.' }; ChoiceTable.separator = '::'; // ## Dependencies ChoiceTable.dependencies = { JSUS: {} }; /** * ## ChoiceTable constructor * * Creates a new instance of ChoiceTable */ function ChoiceTable() { var that; that = this; /** * ### ChoiceTable.table * * The HTML element triggering the listener function when clicked */ this.table = null; /** * ### ChoiceTable.choicesSetSize * * How many choices can be on the same row/column */ this.choicesSetSize = null; /** * ### ChoiceTable.tr * * Reference to TR elements of the table * * Note: if the orientation is vertical there will be multiple TR * otherwise just one. * * @see createTR */ this.trs = []; /** * ### ChoiceTable.listener * * The main function listening on clicks * * @see ChoiceTable.onclick */ this.listener = function(e) { var name, value, td, tr; var i, len, removed; e = e || window.event; td = e.target || e.srcElement; // See if it is a clickable choice. if ('undefined' === typeof that.choicesIds[td.id]) { // It might be a nested element, try the parent. td = td.parentNode; if (!td) return; if ('undefined' === typeof that.choicesIds[td.id]) { td = td.parentNode; if (!td || 'undefined' === typeof that.choicesIds[td.id]) { return; } } } // 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(); } // Id of elements are in the form of name_value or name_item_value. value = td.id.split(that.separator); // Separator not found, not a clickable cell. if (value.length === 1) return; name = value[0]; value = parseInt(value[1], 10); // value = value[1]; // Choice disabled. // console.log('VALUE: ', value); if (that.disabledChoices[value]) return; // One more click. that.numberOfClicks++; // Click on an already selected choice. if (that.isChoiceCurrent(value)) { that.unsetCurrentChoice(value); J.removeClass(td, 'selected'); if (that.selectMultiple) { // Remove selected TD (need to keep this clean for reset). i = -1, len = that.selected.length; for ( ; ++i < len ; ) { if (that.selected[i].id === td.id) { that.selected.splice(i, 1); break; } } } else { that.selected = null; } removed = true; } // Click on a new choice. else { // Have we exhausted available choices? if ('number' === typeof that.selectMultiple && that.selected.length === that.selectMultiple) return; J.addClass(td, 'selected'); if (that.oneTimeClick) { setTimeout(function() { J.removeClass(td, 'selected'); }, 60); } else { that.setCurrentChoice(value); if (that.selectMultiple) { that.selected.push(td); } else { // If only 1 selection allowed, remove old selection. if (that.selected) { J.removeClass(that.selected, 'selected'); } that.selected = td; } } } // Remove any warning/errors on click. if (that.isHighlighted()) that.unhighlight(); // Call onclick, if any. if (that.onclick) { // TODO: Should we parseInt it anyway when we store // the current choice? value = parseInt(value, 10); that.onclick.call(that, value, removed, td); } }; /** * ## ChoiceTable.onclick * * The user-defined onclick listener * * Receives 3 input parameters: the value of the clicked choice, * whether it was a remove action, and the reference to the TD object. * * @see ChoiceTableGroup.listener */ this.onclick = null; /** * ### ChoiceTable.mainText * * The main text introducing the choices * * @see ChoiceTable.spanMainText */ this.mainText = null; /** * ### ChoiceTable.hint * * An additional text with information about how to select items * * If not specified, it may be auto-filled, e.g. '(pick 2)'. * * @see Feedback.texts.autoHint */ this.hint = null; /** * ### ChoiceTable.spanMainText * * The span containing the main text */ this.spanMainText = null; /** * ### ChoiceTable.choices * * The array available choices */ this.choices = null; /** * ### ChoiceTable.choicesValues * * Map of choices' values to indexes in the choices array */ this.choicesValues = {}; /** * ### ChoiceTable.choicesIds * * Map of choices' cells ids to choices * * Used to determine what are the clickable choices. */ this.choicesIds = {}; /** * ### ChoiceTable.choicesCells * * The cells of the table associated with each choice */ this.choicesCells = null; /** * ### ChoiceTable.left * * A non-clickable first cell of the row/column * * It will be placed to the left of the choices if orientation * is horizontal, or above the choices if orientation is vertical * * @see ChoiceTable.orientation */ this.left = null; /** * ### ChoiceTable.leftCell * * The rendered left cell * * @see ChoiceTable.renderSpecial */ this.leftCell = null; /** * ### ChoiceTable.right * * A non-clickable last cell of the row/column * * It will be placed to the right of the choices if orientation * is horizontal, or below the choices if orientation is vertical * * @see ChoiceTable.orientation */ this.right = null; /** * ### ChoiceTable.rightCell * * The rendered right cell * * @see ChoiceTable.renderSpecial */ this.rightCell = null; /** * ### ChoiceTable.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 * * Time when the last choice was made */ this.timeCurrentChoice = null; /** * ### ChoiceTable.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'; /** * ### ChoiceTable.order * * The current order of display of choices * * @see ChoiceTable.originalOrder */ this.order = null; /** * ### ChoiceTable.correctChoice * * The correct choice/s * * The field is an array or number|string depending * on the value of ChoiceTable.selectMultiple * * @see ChoiceTable.selectMultiple */ this.correctChoice = null; /** * ### ChoiceTable.requiredChoice * * The number of required choices. Default 0 */ this.requiredChoice = null; /** * ### ChoiceTable.attempts * * List of currentChoices at the moment of verifying correct answers */ this.attempts = []; /** * ### ChoiceTable.numberOfClicks * * Total number of clicks on different choices */ this.numberOfClicks = 0; /** * ### ChoiceTable.selected * * Currently selected TD elements * * @see ChoiceTable.currentChoice */ this.selected = null; /** * ### ChoiceTable.currentChoice * * Choice/s associated with currently selected cell/s * * The field is an array or number|string depending * on the value of ChoiceTable.selectMultiple * * @see ChoiceTable.selectMultiple * * @see ChoiceTable.selected */ this.currentChoice = null; /** * ### ChoiceTable.selectMultiple * * The number of maximum simulataneous selections (>1), or false * * Note: this option is incompatible with `oneTimeClick`. */ this.selectMultiple = null; /** * ### ChoiceTable.oneTimeClick * * If TRUE, the selection is immediately removed after one click * * This is useful to create a buttons group which trigger some actions. * * Note: this option is incompatible with `selectMultiple`. */ this.oneTimeClick = null; /** * ### ChoiceTable.shuffleChoices * * If TRUE, choices are randomly assigned to cells * * @see ChoiceTable.order */ this.shuffleChoices = null; /** * ### ChoiceTable.renderer * * A callback that renders the content of each cell * * The callback must accept three parameters: * * - a td HTML element, * - a choice * - the index of the choice element within the choices array * * and optionally return the _value_ for the choice (otherwise * the order in the choices array is used as value). */ this.renderer = null; /** * ### ChoiceTable.orientation * * Orientation of display of choices: vertical ('V') or horizontal ('H') * * Default orientation is horizontal. */ this.orientation = 'H'; /** * ### ChoiceTable.group * * The name of the group where the table belongs, if any */ this.group = null; /** * ### ChoiceTable.groupOrder * * The order of the choice table within the group */ this.groupOrder = null; /** * ### ChoiceTable.freeText * * If truthy, a textarea for free-text comment will be added * * If 'string', the text will be added inside the textarea */ this.freeText = null; /** * ### ChoiceTable.textarea * * Textarea for free-text comment */ this.textarea = null; /** * ### ChoiceTable.separator * * Symbol used to separate tokens in the id attribute of every cell * * Default ChoiceTable.separator * * @see ChoiceTable.renderChoice */ this.separator = ChoiceTable.separator; /** * ### ChoiceTable.tabbable * * If TRUE, the elements of the table can be accessed with TAB * * Clicking is simulated upon pressing space or enter. * * Default TRUE * * @see ChoiceTable.renderChoice */ this.tabbable = null; /** * ### ChoiceTable.disabledChoices * * 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 /** * ### ChoiceTable.init * * Initializes the instance * * Available options are: * * - left: the content of the left (or top) cell * - right: the content of the right (or bottom) cell * - className: the className of the table (string, array), or false * to have none. * - orientation: orientation of the table: vertical (v) or horizontal (h) * - group: the name of the group (number or string), if any * - groupOrder: the order of the table in the group, if any * - listener: a function executed at every click. Context is * `this` instance * - onclick: a function executed after the listener function. Context is * `this` instance * - mainText: a text to be displayed above the table * - hint: a text with extra info to be displayed after mainText * - choices: the array of available choices. See * `ChoiceTable.renderChoice` for info about the format * - correctChoice: the array|number|string of correct choices. See * `ChoiceTable.setCorrectChoice` for info about the format * - selectMultiple: if TRUE multiple cells can be selected * - shuffleChoices: if TRUE, choices are shuffled before being added * to the table * - renderer: a function that will render the choices. See * ChoiceTable.renderer for info about the format * - freeText: if TRUE, a textarea will be added under the table, * if 'string', the text will be added inside the textarea * - timeFrom: The timestamp as recorded by `node.timer.setTimestamp` * or FALSE, to measure absolute time for current choice * - tabbable: if TRUE, each cell can be reached with TAB and clicked * with SPACE or ENTER. Default: TRUE. * - disabledChoices: array of disabled choices (values). * * @param {object} opts Configuration options */ ChoiceTable.prototype.init = function(opts) { var tmp, that; that = this; if (!this.id) { throw new TypeError('ChoiceTable.init: opts.id is missing'); } // Option orientation, default 'H'. if ('undefined' === typeof opts.orientation) { tmp = 'H'; } else if ('string' !== typeof opts.orientation) { throw new TypeError('ChoiceTable.init: opts.orientation must ' + 'be string, or undefined. Found: ' + opts.orientation); } else { tmp = opts.orientation.toLowerCase().trim(); if (tmp === 'horizontal' || tmp === 'h') { tmp = 'H'; } else if (tmp === 'vertical' || tmp === 'v') { tmp = 'V'; } else { throw new Error('ChoiceTable.init: opts.orientation is ' + 'invalid: ' + tmp); } } this.orientation = tmp; // Option shuffleChoices, default false. if ('undefined' === typeof opts.shuffleChoices) tmp = false; else tmp = !!opts.shuffleChoices; this.shuffleChoices = tmp; // Option selectMultiple, default false. tmp = opts.selectMultiple; if ('undefined' === typeof tmp) { tmp = false; } else if ('boolean' !== typeof tmp) { tmp = J.isInt(tmp, 1); if (!tmp) { throw new Error('ChoiceTable.init: selectMultiple must be ' + 'undefined or an integer > 1. Found: ' + tmp); } } this.selectMultiple = tmp; // Make an array for currentChoice and selected. if (tmp) { this.selected = []; this.currentChoice = []; } // Option requiredChoice, if any. if ('number' === typeof opts.requiredChoice) { if (!J.isInt(opts.requiredChoice, 0)) { throw new Error('ChoiceTable.init: if number, requiredChoice ' + 'must a positive integer. Found: ' + opts.requiredChoice); } if ('number' === typeof this.selectMultiple && opts.requiredChoice > this.selectMultiple) { throw new Error('ChoiceTable.init: requiredChoice cannot be ' + 'larger than selectMultiple. Found: ' + opts.requiredChoice + ' > ' + this.selectMultiple); } this.requiredChoice = opts.requiredChoice; } else if ('boolean' === typeof opts.requiredChoice) { this.requiredChoice = opts.requiredChoice ? 1 : null; } else if ('undefined' !== typeof opts.requiredChoice) { throw new TypeError('ChoiceTable.init: opts.requiredChoice ' + 'be number, boolean or undefined. Found: ' + opts.requiredChoice); } if ('undefined' !== typeof opts.oneTimeClick) { this.oneTimeClick = !!opts.oneTimeClick; } // Set the group, if any. if ('string' === typeof opts.group || 'number' === typeof opts.group) { this.group = opts.group; } else if ('undefined' !== typeof opts.group) { throw new TypeError('ChoiceTable.init: opts.group must ' + 'be string, number or undefined. Found: ' + opts.group); } // Set the groupOrder, if any. if ('number' === typeof opts.groupOrder) { this.groupOrder = opts.groupOrder; } else if ('undefined' !== typeof opts.groupOrder) { throw new TypeError('ChoiceTable.init: opts.groupOrder must ' + 'be number or undefined. Found: ' + opts.groupOrder); } // Set the main onclick listener, if any. if ('function' === typeof opts.listener) { this.listener = function(e) { opts.listener.call(this, e); }; } else if ('undefined' !== typeof opts.listener) { throw new TypeError('ChoiceTable.init: opts.listener must ' + 'be function or undefined. Found: ' + opts.listener); } // Set an additional onclick, if any. if ('function' === typeof opts.onclick) { this.onclick = opts.onclick; } else if ('undefined' !== typeof opts.onclick) { throw new TypeError('ChoiceTable.init: opts.onclick must ' + 'be function or undefined. Found: ' + opts.onclick); } // Set the mainText, if any. if ('string' === typeof opts.mainText) { this.mainText = opts.mainText; } else if ('undefined' !== typeof opts.mainText) { throw new TypeError('ChoiceTable.init: opts.mainText must ' + 'be string or undefined. Found: ' + opts.mainText); } // Set the hint, if any. if ('string' === typeof opts.hint || false === opts.hint) { this.hint = opts.hint; if (this.requiredChoice) this.hint += ' *'; } else if ('undefined' !== typeof opts.hint) { throw new TypeError('ChoiceTable.init: opts.hint must ' + 'be a string, false, or undefined. Found: ' + opts.hint); } else { // Returns undefined if there are no constraints. this.hint = this.getText('autoHint'); } // Set the timeFrom, if any. if (opts.timeFrom === false || 'string' === typeof opts.timeFrom) { this.timeFrom = opts.timeFrom; } else if ('undefined' !== typeof opts.timeFrom) { throw new TypeError('ChoiceTable.init: opts.timeFrom must ' + 'be string, false, or undefined. Found: ' + opts.timeFrom); } // Set the separator, if any. if ('string' === typeof opts.separator) { this.separator = opts.separator; } else if ('undefined' !== typeof opts.separator) { throw new TypeError('ChoiceTable.init: opts.separator must ' + 'be string, or undefined. Found: ' + opts.separator); } // Conflict might be generated by id or seperator. tmp = this.id + this.separator.substring(0, (this.separator.length -1)); if (this.id.indexOf(this.separator) !== -1 || tmp.indexOf(this.separator) !== -1) { throw new Error('ChoiceTable.init: separator cannot be ' + 'included in the id or in the concatenation ' + '(id + separator). Please specify the right ' + 'separator option. Found: ' + this.separator); } if ('string' === typeof opts.left || 'number' === typeof opts.left) { this.left = '' + opts.left; } else if (J.isNode(opts.left) || J.isElement(opts.left)) { 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); } if ('string' === typeof opts.right || 'number' === typeof opts.right) { this.right = '' + opts.right; } else if (J.isNode(opts.right) || J.isElement(opts.right)) { this.right = opts.right; } else if ('undefined' !== typeof opts.right) { throw new TypeError('ChoiceTable.init: opts.right must ' + 'be string, number, an HTML Element or ' + 'undefined. Found: ' + opts.right); } // Set the className, if not use default. if ('undefined' === typeof opts.className) { this.className = ChoiceTable.className; } else if (opts.className === false) { this.className = false; } else if ('string' === typeof opts.className) { this.className = ChoiceTable.className + ' ' + opts.className; } else if ( J.isArray(opts.className)) { this.className = [ChoiceTable.className].concat(opts.className); } else { throw new TypeError('ChoiceTable.init: opts.' + 'className must be string, array, ' + 'or undefined. Found: ' + opts.className); } if (opts.tabbable !== false) this.tabbable = true; // Set the renderer, if any. if ('function' === typeof opts.renderer) { this.renderer = opts.renderer; } else if ('undefined' !== typeof opts.renderer) { throw new TypeError('ChoiceTable.init: opts.renderer must ' + 'be function or undefined. Found: ' + opts.renderer); } // After all configuration opts are evaluated, add choices. // Set table. if ('object' === typeof opts.table) { this.table = opts.table; } else if ('undefined' !== typeof opts.table && false !== opts.table) { throw new TypeError('ChoiceTable.init: opts.table ' + 'must be object, false or undefined. ' + 'Found: ' + opts.table); } this.table = opts.table; this.freeText = 'string' === typeof opts.freeText ? opts.freeText : !!opts.freeText; // Add the correct choices. if ('undefined' !== typeof opts.choicesSetSize) { if (!J.isInt(opts.choicesSetSize, 0)) { throw new Error('ChoiceTable.init: choicesSetSize must be ' + 'undefined or an integer > 0. Found: ' + opts.choicesSetSize); } if (this.left || this.right) { throw new Error('ChoiceTable.init: choicesSetSize option ' + 'cannot be specified when either left or ' + 'right options are set.'); } this.choicesSetSize = 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)) { throw new Error('ChoiceTable.init: disabledChoices must be ' + 'undefined or array. Found: ' + opts.disabledChoices); } // 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]); } })(); } } if ('undefined' === typeof opts.sameWidthCells) { this.sameWidthCells = !!opts.sameWidthCells; } }; /** * ### ChoiceTable.disableChoice * * Marks a choice as disabled (will not be clickable) * * @param {string|number} value The value of the choice to disable` */ ChoiceTable.prototype.disableChoice = function(value) { this.disabledChoices[value] = true; }; /** * ### ChoiceTable.enableChoice * * Enables a choice (will be clickable again if previously disabled) * * @param {string|number} value The value of the choice to disable` */ ChoiceTable.prototype.enableChoice = function(value) { this.disabledChoices[value] = null; }; /** * ### ChoiceTable.setChoices * * Sets the available choices and optionally builds the table * * If a table is defined, it will automatically append the choices * as TD cells. Otherwise, the choices will be built but not appended. * * @param {array} choices The array of choices * * @see ChoiceTable.table * @see ChoiceTable.shuffleChoices * @see ChoiceTable.order * @see ChoiceTable.buildChoices * @see ChoiceTable.buildTableAndChoices */ ChoiceTable.prototype.setChoices = function(choices) { var len; if (!J.isArray(choices)) { throw new TypeError('ChoiceTable.setChoices: choices ' + 'must be array'); } if (!choices.length) { throw new Error('ChoiceTable.setChoices: choices array is empty'); } this.choices = choices; len = choices.length; // Save the order in which the choices will be added. this.order = J.seq(0, len-1); if (this.shuffleChoices) this.order = J.shuffle(this.order); // Build the table and choices at once (faster). if (this.table) this.buildTableAndChoices(); // Or just build choices. else this.buildChoices(); }; /** * ### ChoiceTable.buildChoices * * Render every choice and stores cell in `choicesCells` array * * Left and right cells are also rendered, if specified. * * Follows a shuffled order, if set * * @see ChoiceTable.order * @see ChoiceTable.renderChoice * @see ChoiceTable.renderSpecial */ ChoiceTable.prototype.buildChoices = function() { var i, len; i = -1, len = this.choices.length; // Pre-allocate the choicesCells array. this.choicesCells = new Array(len); for ( ; ++i < len ; ) { this.renderChoice(this.choices[this.order[i]], i); } if (this.left) this.renderSpecial('left', this.left); if (this.right) this.renderSpecial('right', this.right); }; /** * ### ChoiceTable.buildTable * * Builds the table of clickable choices and enables it * * Must be called after choices have been set already. * * @see ChoiceTable.setChoices * @see ChoiceTable.order * @see ChoiceTable.renderChoice * @see ChoiceTable.orientation * @see ChoiceTable.choicesSetSize */ ChoiceTable.prototype.buildTable = (function() { function makeSet(i, len, H, doSets) { var tr, counter; counter = 0; // Start adding tr/s and tds based on the orientation. if (H) { tr = createTR(this, 'main'); // Add horizontal choices title. if (this.leftCell) tr.appendChild(this.leftCell); } // Main loop. for ( ; ++i < len ; ) { if (!H) { tr = createTR(this, 'left'); // Add vertical choices title. if (i === 0 && this.leftCell) { tr.appendChild(this.leftCell); tr = createTR(this, i); } } // Clickable cell. tr.appendChild(this.choicesCells[i]); // Stop if we reached set size (still need to add the right). if (doSets && ++counter >= this.choicesSetSize) break; } if (this.rightCell) { if (!H) tr = createTR(this, 'right'); tr.appendChild(this.rightCell); } // Start a new set, if necessary. if (i !== len) makeSet.call(this, i, len, H, doSets); } return function() { var len, H, doSets; if (!this.choicesCells) { throw new Error('ChoiceTable.buildTable: choices not set, ' + 'cannot build table. Id: ' + this.id); } H = this.orientation === 'H'; len = this.choicesCells.length; doSets = 'number' === typeof this.choicesSetSize; // Recursively makes sets makeSet.call(this, -1, len, H, doSets); // Enable onclick listener. this.enable(); }; })(); /** * ### ChoiceTable.buildTableAndChoices * * Builds the table of clickable choices * * @see ChoiceTable.choices * @see ChoiceTable.order * @see ChoiceTable.renderChoice * @see ChoiceTable.orientation */ ChoiceTable.prototype.buildTableAndChoices = function() { var i, len, tr, td, H; len = this.choices.length; // Pre-allocate the choicesCells array. this.choicesCells = new Array(len); // Start adding tr/s and tds based on the orientation. i = -1, H = this.orientation === 'H'; if (H) { tr = createTR(this, 'main'); // Add horizontal choices left. if (this.left) { td = this.renderSpecial('left', this.left); tr.appendChild(td); } } // Main loop. for ( ; ++i < len ; ) { if (!H) { tr = createTR(this, 'left'); // Add vertical choices left. if (i === 0 && this.left) { td = this.renderSpecial('left', this.left); tr.appendChild(td); tr = createTR(this, i); } } // Clickable cell. td = this.renderChoice(this.choices[this.order[i]], i); tr.appendChild(td); } if (this.right) { if (!H) tr = createTR(this, 'right'); td = this.renderSpecial('right', this.right); tr.appendChild(td); } // Enable onclick listener. this.enable(); }; /** * ### ChoiceTable.renderSpecial * * Renders a non-choice element into a cell of the table (e.g. left/right) * * @param {string} type The type of special cell ('left' or 'right'). * @param {mixed} special The special element. It must be string or number, * or array where the first element is the 'value' (incorporated in the * `id` field) and the second the text to display as choice. * * @return {HTMLElement} td The newly created cell of the table * * @see ChoiceTable.left * @see ChoiceTable.right */ ChoiceTable.prototype.renderSpecial = function(type, special) { var td, className; td = document.createElement('td'); if ('string' === typeof special) td.innerHTML = special; // HTML element (checked before). else td.appendChild(special); if (type === 'left') { className = this.className ? this.className + '-left' : 'left'; this.leftCell = td; } else if (type === 'right') { className = this.className ? this.className + '-right' : 'right'; this.rightCell = td; } else { throw new Error('ChoiceTable.renderSpecial: unknown type: ' + type); } td.className = className; td.id = this.id + this.separator + 'special-cell-' + type; return td; }; /* UPDATED TEXT * @param {mixed} choice The choice element. It must be string or * number, HTML element, or an array. If array, the first * element is the short value (string or number), and the second * one the the full value (string, number or HTML element) to * display. If a renderer function is defined there are no * restriction on the format of choice * @param {number} idx The position of the choice within the choice array */ /** * ### ChoiceTable.renderChoice * * Transforms a choice element into a cell of the table * * A reference to the cell is saved in `choicesCells`. * * @param {mixed} choice The choice element. It may be string, number, * array where the first element is the 'value' and the second the * text to display as choice, or an object with properties value and * display. If a renderer function is defined there are no restriction * on the format of choice. * @param {number} idx The position of the choice within the choice array * * @return {HTMLElement} td The newly created cell of the table * * @see ChoiceTable.renderer * @see ChoiceTable.separator * @see ChoiceTable.choicesCells */ ChoiceTable.prototype.renderChoice = function(choice, idx) { var td, shortValue, value, width; td = document.createElement('td'); if (this.tabbable) J.makeTabbable(td); // Forces equal width. if (this.sameWidthCells && this.orientation === 'H') { width = this.left ? 70 : 100; if (this.right) width = width - 30; width = width / (this.choicesSetSize || this.choices.length); td.style.width = width.toFixed(2) + '%'; } // Use custom renderer. if (this.renderer) { value = this.renderer(td, choice, idx); if ('undefined' === typeof value) value = idx; } // Or use standard format. else { if (J.isArray(choice)) { shortValue = choice[0]; choice = choice[1]; } else if ('object' === typeof choice) { shortValue = choice.value; choice = choice.display; } value = this.shuffleChoices ? this.order[idx] : idx; if ('string' === typeof choice || 'number' === typeof choice) { td.innerHTML = choice; } 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); } } // Map a value to the index. if ('undefined' !== typeof this.choicesValues[value]) { throw new Error('ChoiceTable.renderChoice: value already ' + 'in use: ' + value); } // Add the id if not added already by the renderer function. if (!td.id || td.id === '') { td.id = this.id + this.separator + value; } // All fine, updates global variables. this.choicesValues[value] = idx; this.choicesCells[idx] = td; this.choicesIds[td.id] = td; return td; }; /** * ### ChoiceTable.setCorrectChoice * * Set the correct choice/s * * Correct choice/s are always stored as 'strings', or not number * because then they are compared against the valued saved in * the `id` field of the cell * * @param {number|string|array} If `selectMultiple` is set, param must * be an array, otherwise a string or a number. Each correct choice * must have been already defined as choice (value) * * @see ChoiceTable.setChoices * @see checkCorrectChoiceParam */ ChoiceTable.prototype.setCorrectChoice = function(choice) { var i, len; if (!this.selectMultiple) { choice = checkCorrectChoiceParam(this, choice); } else { if (J.isArray(choice) && choice.length) { i = -1, len = choice.length; for ( ; ++i < len ; ) { choice[i] = checkCorrectChoiceParam(this, choice[i]); } } else { throw new TypeError('ChoiceTable.setCorrectChoice: choice ' + 'must be non-empty array. Found: ' + choice); } } this.correctChoice = choice; }; /** * ### ChoiceTable.append * * Implements Widget.append * * Checks that id is unique. * * Appends (all optional): * * - mainText: a question or statement introducing the choices * - table: the table containing the choices * - freeText: a textarea for comments * * @see Widget.append */ ChoiceTable.prototype.append = function() { var tmp; // Id must be unique. if (W.getElementById(this.id)) { throw new Error('ChoiceTable.append: id is not ' + 'unique: ' + this.id); } // MainText. if (this.mainText) { this.spanMainText = W.append('span', this.bodyDiv, { className: 'choicetable-maintext', innerHTML: this.mainText }); } // Hint. if (this.hint) { W.append('span', this.spanMainText || this.bodyDiv, { className: 'choicetable-hint', innerHTML: this.hint }); } // Create/set table. if (this.table !== false) { // Create table, if it was not passed as object before. if ('undefined' === typeof this.table) { this.table = document.createElement('table'); this.buildTable(); } // Set table id. this.table.id = this.id; // 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); } 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'); if (this.id) this.textarea.id = this.id + '_text'; if ('string' === typeof this.freeText) { this.textarea.placeholder = this.freeText; } tmp = this.className ? this.className + '-freetext' : 'freetext'; this.textarea.className = tmp; // Append textarea. this.bodyDiv.appendChild(this.textarea); } }; /** * ### 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) { // 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(); }; /** * ### ChoiceTable.listeners * * Implements Widget.listeners * * Adds two listeners two disable/enable the widget on events: * INPUT_DISABLE, INPUT_ENABLE * * @see Widget.listeners */ ChoiceTable.prototype.listeners = function() { var that = this; node.on('INPUT_DISABLE', function() { that.disable(); }); node.on('INPUT_ENABLE', function() { that.enable(); }); }; /** * ### ChoiceTable.disable * * Disables clicking on the table and removes CSS 'clicklable' class */ ChoiceTable.prototype.disable = function() { if (this.disabled === true) return; this.disabled = true; if (this.table) { J.removeClass(this.table, 'clickable'); this.table.removeEventListener('click', this.listener); // Remove listener to make cells clickable with the keyboard. if (this.tabbable) J.makeClickable(this.table, false); } this.emit('disabled'); }; /** * ### ChoiceTable.enable * * Enables clicking on the table and adds CSS 'clicklable' class * * @return {function} cb The event listener function */ ChoiceTable.prototype.enable = function() { if (this.disabled === false) return; if (!this.table) { throw new Error('ChoiceTable.enable: table not defined'); } this.disabled = false; J.addClass(this.table, 'clickable'); this.table.addEventListener('click', this.listener); // Add listener to make cells clickable with the keyboard. if (this.tabbable) J.makeClickable(this.table); this.emit('enabled'); }; /** * ### ChoiceTable.verifyChoice * * Compares the current choice/s with the correct one/s * * Depending on current settings, there are two modes of verifying * choices: * * - requiredChoice: there must be at least N choices selected * - correctChoice: the choices are compared against correct ones. * * @param {boolean} markAttempt Optional. If TRUE, the value of * current choice is added to the attempts array. Default: TRUE * * @return {boolean|null} TRUE if current choice is correct, * FALSE if it is not correct, or NULL if no correct choice * was set * * @see ChoiceTable.attempts * @see ChoiceTable.setCorrectChoice */ ChoiceTable.prototype.verifyChoice =