UNPKG

nodegame-widgets

Version:

Collections of useful and reusable javascript / HTML snippets for nodeGame

1,355 lines (1,202 loc) 44 kB
/** * # ChoiceTableGroup * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a table that groups together several choice tables widgets * * @see ChoiceTable * * www.nodegame.org */ (function(node) { "use strict"; node.widgets.register('ChoiceTableGroup', ChoiceTableGroup); // ## Meta-data ChoiceTableGroup.version = '1.8.0'; ChoiceTableGroup.description = 'Groups together and manages sets of ' + 'ChoiceTable widgets.'; ChoiceTableGroup.title = 'Make your choice'; ChoiceTableGroup.className = 'choicetable choicetablegroup'; ChoiceTableGroup.separator = '::'; ChoiceTableGroup.texts = { autoHint: function(w) { if (w.requiredChoice) return '*'; else return false; }, error: 'Selection required.' }; // ## Dependencies ChoiceTableGroup.dependencies = { JSUS: {} }; /** * ## ChoiceTableGroup constructor * * Creates a new instance of ChoiceTableGroup * * @param {object} options Optional. Configuration options. * If a `table` option is specified, it sets it as the clickable * table. All other options are passed to the init method. */ function ChoiceTableGroup() { var that; that = this; /** * ### ChoiceTableGroup.dl * * The clickable table containing all the cells */ this.table = null; /** * ### ChoiceTableGroup.trs * * Collection of all trs created * * Useful when shuffling items/choices * * @see ChoiceTableGroup.shuffle */ this.trs = []; /** * ## ChoiceTableGroup.listener * * The main listener function * * @see ChoiceTableGroup.enable * @see ChoiceTableGroup.disable * @see ChoiceTableGroup.onclick */ this.listener = function(e) { var name, value, item, td, oldSelected; var time, removed; // Relative time. if ('string' === typeof that.timeFrom) { time = node.timer.getTimeSince(that.timeFrom); } // Absolute time. else { time = Date.now ? Date.now() : new Date().getTime(); } e = e || window.event; td = e.target || e.srcElement; // Not a clickable choice. if ('undefined' === typeof that.choicesById[td.id]) { // It might be a nested element, try the parent. td = td.parentNode; if (!td || 'undefined' === typeof that.choicesById[td.id]) { return; } } // if (!that.choicesById[td.id]) return; // 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 = value[1]; item = that.itemsById[name]; // Not a clickable cell. if (!item) return; item.timeCurrentChoice = time; // One more click. item.numberOfClicks++; // If only 1 selection allowed, remove selection from oldSelected. if (!item.selectMultiple) { oldSelected = item.selected; if (oldSelected) J.removeClass(oldSelected, 'selected'); if (item.isChoiceCurrent(value)) { item.unsetCurrentChoice(value); removed = true; } else { item.currentChoice = value; J.addClass(td, 'selected'); item.selected = td; } } // Remove any warning/error from form 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, name, value, removed, td); } }; /** * ## ChoiceTableGroup.onclick * * The user-defined onclick function * * Receives 4 input parameters: the name of the choice table clicked, * 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; /** * ### ChoiceTableGroup.mainText * * The main text introducing the choices * * @see ChoiceTableGroup.spanMainText */ this.mainText = null; /** * ### ChoiceTableGroup.spanMainText * * The span containing the main text */ this.spanMainText = null; /** * ### ChoiceTableGroup.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; /** * ### ChoiceTableGroup.errorBox * * An HTML element displayed when a validation error occurs */ this.errorBox = null; /** * ### ChoiceTableGroup.items * * The array of available items */ this.items = null; /** * ### ChoiceTableGroup.itemsById * * Map of items ids to items */ this.itemsById = {}; /** * ### ChoiceTableGroup.itemsMap * * Maps items ids to the position in the items array */ this.itemsMap = {}; /** * ### ChoiceTableGroup.choices * * Array of default choices (if passed as global parameter) */ this.choices = null; /** * ### ChoiceTableGroup.choicesById * * Map of items choices ids to corresponding cell * * Useful to detect clickable cells. */ this.choicesById = {}; /** * ### ChoiceTableGroup.itemsSettings * * The array of settings for each item */ this.itemsSettings = null; /** * ### ChoiceTableGroup.order * * The current order of display of choices * * May differ from `originalOrder` if shuffled. * * @see ChoiceTableGroup.originalOrder */ this.order = null; /** * ### ChoiceTableGroup.originalOrder * * The initial order of display of choices * * @see ChoiceTable.order */ this.originalOrder = null; /** * ### ChoiceTableGroup.shuffleItems * * If TRUE, items are inserted in random order * * @see ChoiceTableGroup.order */ this.shuffleItems = null; /** * ### ChoiceTableGroup.requiredChoice * * The number of required choices. */ this.requiredChoice = null; /** * ### ChoiceTableGroup.orientation * * Orientation of display of items: vertical ('V') or horizontal ('H') * * Default orientation is horizontal. */ this.orientation = 'H'; /** * ### ChoiceTableGroup.group * * The name of the group where the table belongs, if any */ this.group = null; /** * ### ChoiceTableGroup.groupOrder * * The order of the choice table within the group */ this.groupOrder = null; /** * ### ChoiceTableGroup.freeText * * If truthy, a textarea for free-text comment will be added * * If 'string', the text will be added inside the the textarea */ this.freeText = null; /** * ### ChoiceTableGroup.textarea * * Textarea for free-text comment */ this.textarea = null; /** * ### ChoiceTableGroup.header * * Header to be displayed above the table * * @experimental */ this.header = null; // Options passed to each individual item. /** * ### ChoiceTableGroup.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. * * This option is passed to each individual item. * * @see mixinSettings * * @see node.timer.getTimeSince */ this.timeFrom = 'step'; /** * ### ChoiceTableGroup.selectMultiple * * If TRUE, it allows to select multiple cells * * This option is passed to each individual item. * * @see mixinSettings */ this.selectMultiple = null; /** * ### ChoiceTableGroup.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 option is passed to each individual item. * * @see mixinSettings */ this.renderer = null; /** * ### ChoiceTableGroup.separator * * Symbol used to separate tokens in the id attribute of every cell * * Default ChoiceTableGroup.separator * * This option is passed to each individual item. * * @see mixinSettings */ this.separator = ChoiceTableGroup.separator; /** * ### ChoiceTableGroup.shuffleChoices * * If TRUE, choices in items are shuffled * * This option is passed to each individual item. * * @see mixinSettings */ this.shuffleChoices = null; /** * ### ChoiceTableGroup.tabbable * * If TRUE, the elements of each choicetable can be accessed with TAB * * Clicking is simulated upon pressing space or enter. * * Default TRUE * * @see ChoiceTable.tabbable */ this.tabbable = null; } // ## ChoiceTableGroup methods /** * ### ChoiceTableGroup.init * * Initializes the instance * * Available options are: * * - 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 custom 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 * - shuffleItems: if TRUE, items are shuffled before being added * to the table * - freeText: if TRUE, a textarea will be added under the table, * if 'string', the text will be added inside the 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. * * @param {object} opts Configuration options */ ChoiceTableGroup.prototype.init = function(opts) { var tmp; // TODO: many options checking are replicated. Skip them all? // Have a method in ChoiceTable? if (!this.id) { throw new TypeError('ChoiceTableGroup.init: id ' + 'is missing.'); } // Option orientation, default 'H'. if ('undefined' === typeof opts.orientation) { tmp = 'H'; } else if ('string' !== typeof opts.orientation) { throw new TypeError('ChoiceTableGroup.init: 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('ChoiceTableGroup.init: orientation ' + 'is invalid: ' + tmp); } } this.orientation = tmp; // Option shuffleItems, default false. if ('undefined' === typeof opts.shuffleItems) tmp = false; else tmp = !!opts.shuffleItems; this.shuffleItems = tmp; // Option requiredChoice, if any. if ('number' === typeof opts.requiredChoice) { this.requiredChoice = opts.requiredChoice; } else if ('boolean' === typeof opts.requiredChoice) { this.requiredChoice = opts.requiredChoice ? 1 : 0; } else if ('undefined' !== typeof opts.requiredChoice) { throw new TypeError('ChoiceTableGroup.init: ' + 'opts.requiredChoice ' + 'be number or boolean or undefined. Found: ' + opts.requiredChoice); } // 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('ChoiceTableGroup.init: 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.group) { throw new TypeError('ChoiceTableGroup.init: 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('ChoiceTableGroup.init: 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('ChoiceTableGroup.init: 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('ChoiceTableGroup.init: 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; } else if ('undefined' !== typeof opts.hint) { throw new TypeError('ChoiceTableGroup.init: 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('ChoiceTableGroup.init: timeFrom ' + 'must be string, false, or undefined. Found: ' + opts.timeFrom); } // Option shuffleChoices, default false. if ('undefined' !== typeof opts.shuffleChoices) { this.shuffleChoices = !!opts.shuffleChoices; } // Set the renderer, if any. if ('function' === typeof opts.renderer) { this.renderer = opts.renderer; } else if ('undefined' !== typeof opts.renderer) { throw new TypeError('ChoiceTableGroup.init: renderer ' + 'must be function or undefined. Found: ' + opts.renderer); } // Set default choices, if any. if ('undefined' !== typeof opts.choices) { this.choices = opts.choices; } // Set the className, if not use default. if ('undefined' === typeof opts.className) { this.className = ChoiceTableGroup.className; } else if (opts.className === false || 'string' === typeof opts.className || J.isArray(opts.className)) { this.className = opts.className; } else { throw new TypeError('ChoiceTableGroup.init: ' + 'className must be string, array, ' + 'or undefined. Found: ' + opts.className); } if (opts.tabbable !== false) this.tabbable = true; // Separator checked by ChoiceTable. if (opts.separator) this.separator = opts.separator; // After all configuration opts are evaluated, add items. if ('object' === typeof opts.table) { this.table = opts.table; } else if ('undefined' !== typeof opts.table && false !== opts.table) { throw new TypeError('ChoiceTableGroup.init: table ' + 'must be object, false or undefined. ' + 'Found: ' + opts.table); } this.table = opts.table; this.freeText = 'string' === typeof opts.freeText ? opts.freeText : !!opts.freeText; if (opts.header) { if (!J.isArray(opts.header) || opts.header.length !== opts.choices.length) { throw new Error('ChoiceTableGroup.init: header ' + 'must be an array of length ' + opts.choices.length + ' or undefined. Found: ' + opts.header); } this.header = opts.header; } // Add the items. if ('undefined' !== typeof opts.items) this.setItems(opts.items); }; /** * ### ChoiceTableGroup.setItems * * Sets the available items and optionally builds the table * * @param {array} items The array of items * * @see ChoiceTableGroup.table * @see ChoiceTableGroup.order * @see ChoiceTableGroup.shuffleItems * @see ChoiceTableGroup.buildTable */ ChoiceTableGroup.prototype.setItems = function(items) { var len; if (!J.isArray(items)) { throw new TypeError('ChoiceTableGroup.setItems: ' + 'items must be array. Found: ' + items); } if (!items.length) { throw new Error('ChoiceTableGroup.setItems: ' + 'items is an empty array.'); } len = items.length; this.itemsSettings = items; this.items = new Array(len); // Save the order in which the items will be added. this.order = J.seq(0, len-1); if (this.shuffleItems) this.order = J.shuffle(this.order); this.originalOrder = this.order; // Build the table and items at once (faster). if (this.table) this.buildTable(); }; /** * ### ChoiceTableGroup.buildTable * * Builds the table of clickable items and enables it * * Must be called after items have been set already. * * @see ChoiceTableGroup.setChoiceTables * @see ChoiceTableGroup.order */ ChoiceTableGroup.prototype.buildTable = function() { var i, len, tr, H, ct; var j, lenJ, lenJOld, hasRight, cell; H = this.orientation === 'H'; i = -1, len = this.itemsSettings.length; if (H) { if (this.header) { tr = W.add('tr', this.table); W.add('td', tr, { className: 'header' }); for ( ; ++i < this.header.length ; ) { W.add('td', tr, { innerHTML: this.header[i], className: 'header' }); } i = -1; } for ( ; ++i < len ; ) { // Get item. ct = getChoiceTable(this, i); // Add new TR. tr = createTR(this, ct.id); // Append choices for item. tr.appendChild(ct.leftCell); j = -1, lenJ = ct.choicesCells.length; // Make sure all items have same number of choices. if (i === 0) { lenJOld = lenJ; } else if (lenJ !== lenJOld) { throw new Error('ChoiceTableGroup.buildTable: item ' + 'do not have same number of choices: ' + ct.id); } // TODO: might optimize. There are two loops (+1 inside ct). for ( ; ++j < lenJ ; ) { cell = ct.choicesCells[j]; tr.appendChild(cell); this.choicesById[cell.id] = cell; } if (ct.rightCell) tr.appendChild(ct.rightCell); } } else { // Add new TR. // TODO: rename, this is not the header as from options. tr = createTR(this, 'header'); // Build all items first. for ( ; ++i < len ; ) { // Get item, append choices for item. ct = getChoiceTable(this, i); // Make sure all items have same number of choices. lenJ = ct.choicesCells.length; if (i === 0) { lenJOld = lenJ; } else if (lenJ !== lenJOld) { throw new Error('ChoiceTableGroup.buildTable: item ' + 'do not have same number of choices: ' + ct.id); } if ('undefined' === typeof hasRight) { hasRight = !!ct.rightCell; } else if ((!ct.rightCell && hasRight) || (ct.rightCell && !hasRight)) { throw new Error('ChoiceTableGroup.buildTable: either all ' + 'items or no item must have the right ' + 'cell: ' + ct.id); } // Add left. tr.appendChild(ct.leftCell); } if (hasRight) lenJ++; j = -1; for ( ; ++j < lenJ ; ) { // Add new TR. tr = createTR(this, 'row' + (j+1)); i = -1; // TODO: might optimize. There are two loops (+1 inside ct). for ( ; ++i < len ; ) { if (hasRight && j === (lenJ-1)) { tr.appendChild(this.items[i].rightCell); } else { cell = this.items[i].choicesCells[j]; tr.appendChild(cell); this.choicesById[cell.id] = cell; } } } } // Enable onclick listener. this.enable(true); }; /** * ### ChoiceTableGroup.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 */ ChoiceTableGroup.prototype.append = function() { // Id must be unique. if (W.getElementById(this.id)) { throw new Error('ChoiceTableGroup.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 requested. if (this.table !== false) { if ('undefined' === typeof this.table) { this.table = document.createElement('table'); if (this.items) this.buildTable(); } // Set table id. this.table.id = this.id; if (this.className) J.addClass(this.table, this.className); 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'; this.textarea.className = ChoiceTableGroup.className + '-freetext'; if ('string' === typeof this.freeText) { this.textarea.placeholder = this.freeText; } // Append textarea. this.bodyDiv.appendChild(this.textarea); } }; /** * ### ChoiceTableGroup.listeners * * Implements Widget.listeners * * Adds two listeners two disable/enable the widget on events: * INPUT_DISABLE, INPUT_ENABLE * * Notice! Nested choice tables listeners are not executed. * * @see Widget.listeners * @see mixinSettings */ ChoiceTableGroup.prototype.listeners = function() { var that = this; node.on('INPUT_DISABLE', function() { that.disable(); }); node.on('INPUT_ENABLE', function() { that.enable(); }); }; /** * ### ChoiceTableGroup.disable * * Disables clicking on the table and removes CSS 'clicklable' class */ ChoiceTableGroup.prototype.disable = function() { if (this.disabled === true || !this.table) return; this.disabled = true; 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'); }; /** * ### ChoiceTableGroup.enable * * Enables clicking on the table and adds CSS 'clicklable' class * * @return {function} cb The event listener function */ ChoiceTableGroup.prototype.enable = function(force) { if (!this.table || (!force && !this.disabled)) return; 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'); }; /** * ### ChoiceTableGroup.verifyChoice * * Compares the current choice/s with the correct one/s * * @param {boolean} markAttempt Optional. If TRUE, the value of * current choice is added to the attempts array. Default * * @return {boolean|null} TRUE if current choice is correct, * FALSE if it is not correct, or NULL if no correct choice * was set * * @see ChoiceTableGroup.attempts * @see ChoiceTableGroup.setCorrectChoice */ ChoiceTableGroup.prototype.verifyChoice = function(markAttempt) { var i, len, out; out = {}; // Mark attempt by default. markAttempt = 'undefined' === typeof markAttempt ? true : markAttempt; i = -1, len = this.items.length; for ( ; ++i < len ; ) { out[this.items[i].id] = this.items[i].verifyChoice(markAttempt); } return out; }; /** * ### ChoiceTable.setCurrentChoice * * Marks a choice as current in each item * * If the item allows it, multiple choices can be set as current. * * @param {number|string} The choice to mark as current * * @see ChoiceTable.currentChoice * @see ChoiceTable.selectMultiple */ ChoiceTableGroup.prototype.setCurrentChoice = function(choice) { var i, len; i = -1, len = this.items[i].length; for ( ; ++i < len ; ) { this.items[i].setCurrentChoice(choice); } }; /** * ### ChoiceTableGroup.unsetCurrentChoice * * Deletes the value for currentChoice from every item * * If `ChoiceTableGroup.selectMultiple` is set the * * @param {number|string} Optional. The choice to delete from currentChoice * when multiple selections are allowed * * @see ChoiceTableGroup.currentChoice * @see ChoiceTableGroup.selectMultiple */ ChoiceTableGroup.prototype.unsetCurrentChoice = function(choice) { var i, len; i = -1, len = this.items.length; for ( ; ++i < len ; ) { this.items[i].unsetCurrentChoice(choice); } }; /** * ### ChoiceTableGroup.highlight * * Highlights the choice table * * @param {string} The style for the table's border. * Default '1px solid red' * * @see ChoiceTableGroup.highlighted */ ChoiceTableGroup.prototype.highlight = function(border) { if (border && 'string' !== typeof border) { throw new TypeError('ChoiceTableGroup.highlight: border must be ' + 'string or undefined. Found: ' + border); } if (!this.table || this.highlighted === true) return; this.table.style.border = border || '3px solid red'; this.highlighted = true; this.emit('highlighted', border); }; /** * ### ChoiceTableGroup.unhighlight * * Removes highlight from the choice table * * @see ChoiceTableGroup.highlighted */ ChoiceTableGroup.prototype.unhighlight = function() { if (!this.table || this.highlighted !== true) return; this.table.style.border = ''; this.highlighted = false; this.setError(); this.emit('unhighlighted'); }; /** * ### ChoiceTableGroup.getValues * * Returns the values for current selection and other paradata * * Paradata that is not set or recorded will be omitted * * @param {object} opts Optional. Configures the return value. * Available optionts: * * - markAttempt: If TRUE, getting the value counts as an attempt * to find the correct answer. Default: TRUE. * - highlight: If TRUE, if current value is not the correct * value, widget will be highlighted. Default: TRUE. * - reset: If TRUTHY and no item raises an error, * then it resets the state of all items before * returning it. Default: FALSE. * * @return {object} Object containing the choice and paradata * * @see ChoiceTableGroup.verifyChoice * @see ChoiceTableGroup.reset */ ChoiceTableGroup.prototype.getValues = function(opts) { var obj, i, len, tbl, toHighlight, toReset; obj = { id: this.id, order: this.order, items: {}, isCorrect: true }; opts = opts || {}; if ('undefined' === typeof opts.highlight) opts.highlight = true; // Make sure reset is done only at the end. toReset = opts.reset; opts.reset = false; i = -1, len = this.items.length; for ( ; ++i < len ; ) { tbl = this.items[i]; obj.items[tbl.id] = tbl.getValues(opts); if (obj.items[tbl.id].choice === null) { obj.missValues = true; if (tbl.requiredChoice) { toHighlight = true; obj.isCorrect = false; } } if (obj.items[tbl.id].isCorrect === false && opts.highlight) { toHighlight = true; } } 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 * * Sets values in the choice table group 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. * * @see ChoiceTable.setValues * * @experimental */ ChoiceTableGroup.prototype.setValues = function(opts) { var i, len; if (!this.items || !this.items.length) { throw new Error('ChoiceTableGroup.setValues: no items found.'); } opts = opts || {}; i = -1, len = this.items.length; for ( ; ++i < len ; ) { this.items[i].setValues(opts); } // Make a random comment. if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); }; /** * ### ChoiceTableGroup.reset * * Resets all the ChoiceTable items and textarea * * @param {object} options Optional. Options specifying how to set * to reset each item * * @see ChoiceTable.reset * @see ChoiceTableGroup.shuffle */ ChoiceTableGroup.prototype.reset = function(opts) { var i, len; opts = opts || {}; i = -1, len = this.items.length; for ( ; ++i < len ; ) { this.items[i].reset(opts); } // Delete textarea, if found. if (this.textarea) this.textarea.value = ''; if (opts.shuffleItems) this.shuffle(); if (this.isHighlighted()) this.unhighlight(); }; /** * ### ChoiceTableGroup.shuffle * * Shuffles the order of the displayed items * * Assigns the new order of items to `this.order`. * * @param {object} options Optional. Not used for now. * * TODO: shuffle choices in each item. (Note: can't use * item.shuffle, because the cells are taken out, so * there is no table and no tr in there) * * JSUS.shuffleElements */ ChoiceTableGroup.prototype.shuffle = function(opts) { var order, i, len, j, lenJ, that, cb, newOrder; if (!this.items) return; len = this.items.length; if (!len) return; that = this; newOrder = new Array(len); // Updates the groupOrder property of each item, // and saves the order of items correctly. cb = function(el, newPos, oldPos) { var i; i = el.id.split(that.separator); i = that.orientation === 'H' ? i[2] : i[0]; i = that.itemsMap[i]; that.items[i].groupOrder = (newPos+1); newOrder[newPos] = i; }; order = J.shuffle(this.order); if (this.orientation === 'H') { J.shuffleElements(this.table, order, cb); } else { // Here we maintain the columns manually. Each TR contains TD // belonging to different items, we make sure the order is the // same for all TR. len = this.trs.length; for ( i = -1 ; ++i < len ; ) { J.shuffleElements(this.trs[i], order, cb); // Call cb only on first iteration. cb = undefined; } } this.order = newOrder; }; // ## Helper methods. /** * ### mixinSettings * * Mix-ins global settings with local settings for specific choice tables * * @param {ChoiceTableGroup} that This instance * @param {object|string} s The current settings for the item * (choice table), or just its id, to mixin all settings. * @param {number} i The ordinal position of the table in the group * * @return {object} s The mixed-in settings */ function mixinSettings(that, s, i) { 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); } s.group = that.id; s.groupOrder = i+1; s.orientation = that.orientation; s.title = false; s.listeners = false; s.separator = that.separator; if ('undefined' === typeof s.choices && that.choices) { s.choices = that.choices; } if (!s.renderer && that.renderer) s.renderer = that.renderer; if ('undefined' === typeof s.requiredChoice && that.requiredChoice) { s.requiredChoice = that.requiredChoice; } if ('undefined' === typeof s.selectMultiple && null !== that.selectMultiple) { s.selectMultiple = that.selectMultiple; } if ('undefined' === typeof s.shuffleChoices && that.shuffleChoices) { s.shuffleChoices = that.shuffleChoices; } if ('undefined' === typeof s.timeFrom) s.timeFrom = that.timeFrom; if ('undefined' === typeof s.left) s.left = s.id; // No reference is stored in node.widgets. s.storeRef = false; return s; } /** * ### getChoiceTable * * Creates a instance i-th of choice table with relative settings * * Stores a reference of each table in `itemsById` * * @param {ChoiceTableGroup} that This instance * @param {number} i The ordinal position of the table in the group * * @return {object} ct The requested choice table * * @see ChoiceTableGroup.itemsSettings * @see ChoiceTableGroup.itemsById * @see mixinSettings */ function getChoiceTable(that, i) { var ct, s, idx; idx = that.order[i]; s = mixinSettings(that, that.itemsSettings[idx], i); ct = node.widgets.get('ChoiceTable', s); if (that.itemsById[ct.id]) { throw new Error('ChoiceTableGroup.buildTable: an item ' + 'with the same id already exists: ' + ct.id); } if (!ct.leftCell) { throw new Error('ChoiceTableGroup.buildTable: item ' + 'is missing a left cell: ' + s.id); } that.itemsById[ct.id] = ct; that.items[idx] = ct; that.itemsMap[ct.id] = idx; return ct; } /** * ### createTR * * Creates and append a new TR element * * If required by current configuration, the `id` attribute is * added to the TR in the form of: 'tr' + separator + widget_id * * @param {ChoiceTable} that This instance * * @return {HTMLElement} Thew newly created TR element */ function createTR(that, trid) { var tr, sep; tr = document.createElement('tr'); that.table.appendChild(tr); // Set id. sep = that.separator; tr.id = that.id + sep + 'tr' + sep + trid; // Store reference. that.trs.push(tr); return tr; } })(node);