UNPKG

nodegame-widgets

Version:

Collections of useful and reusable javascript / HTML snippets for nodeGame

1,173 lines (1,052 loc) 38.1 kB
/** * # CustomInputGroup * Copyright(c) 2021 Stefano Balietti * MIT Licensed * * Creates a table that groups together several custom input widgets * * @see CustomInput * * www.nodegame.org */ (function(node) { "use strict"; node.widgets.register('CustomInputGroup', CustomInputGroup); // ## Meta-data CustomInputGroup.version = '0.3.0'; CustomInputGroup.description = 'Groups together and manages sets of ' + 'CustomInput widgets.'; CustomInputGroup.title = false; CustomInputGroup.className = 'custominput custominputgroup'; CustomInputGroup.separator = '::'; CustomInputGroup.texts.autoHint = function(w) { if (w.requiredChoice) return '*'; else return false; }; CustomInputGroup.texts.inputErr = 'One or more errors detected.'; // ## Dependencies CustomInputGroup.dependencies = { JSUS: {} }; /** * ## CustomInputGroup constructor * * Creates a new instance of CustomInputGroup * * @param {object} options Optional. Configuration options. * If a `table` option is specified, it sets it as main * table. All other options are passed to the init method. */ function CustomInputGroup() { /** * ### CustomInputGroup.dl * * The table containing all the custom inputs */ this.table = null; /** * ### CustomInputGroup.trs * * Collection of all trs created * * Useful when shuffling items/choices * * @see CustomInputGroup.shuffle */ this.trs = []; /** * ### CustomInputGroup.mainText * * The main text introducing the choices * * @see CustomInputGroup.spanMainText */ this.mainText = null; /** * ### CustomInputGroup.spanMainText * * The span containing the main text */ this.spanMainText = null; /** * ### CustomInputGroup.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; /** * ### CustomInputGroup.items * * The array of available items */ this.items = null; /** * ### CustomInputGroup.itemsById * * Map of items ids to items */ this.itemsById = {}; /** * ### CustomInputGroup.itemsMap * * Maps items ids to the position in the items array */ this.itemsMap = {}; /** * ### CustomInputGroup.choices * * Array of default choices (if passed as global parameter) */ this.choices = null; /** * ### CustomInputGroup.choicesById * * Map of items choices ids to corresponding cell * * Useful to detect clickable cells. */ this.choicesById = {}; /** * ### CustomInputGroup.itemsSettings * * The array of settings for each item */ this.itemsSettings = null; /** * ### CustomInputGroup.order * * The current order of display of choices * * May differ from `originalOrder` if shuffled. * * @see CustomInputGroup.originalOrder */ this.order = null; /** * ### CustomInputGroup.originalOrder * * The initial order of display of choices * * @see CustomInput.order */ this.originalOrder = null; /** * ### CustomInputGroup.shuffleItems * * If TRUE, items are inserted in random order * * @see CustomInputGroup.order */ this.shuffleItems = null; /** * ### CustomInputGroup.requiredChoice * * The number of required choices. */ this.requiredChoice = null; /** * ### ChoiceManager.required * * TRUE if widget should be checked upon node.done. * * TODO: Harmonize required and requiredChoice */ this.required = null; /** * ### CustomInputGroup.orientation * * Orientation of display of items: vertical ('V') or horizontal ('H') * * Default orientation is vertical. */ this.orientation = 'V'; /** * ### CustomInputGroup.group * * The name of the group where the table belongs, if any */ this.group = null; /** * ### CustomInputGroup.groupOrder * * The order of the choice table within the group */ this.groupOrder = null; /** * ### CustomInputGroup.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; /** * ### CustomInputGroup.textarea * * Textarea for free-text comment */ this.textarea = null; // Options passed to each individual item. /** * ### CustomInputGroup.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'; /** * ### CustomInputGroup.separator * * Symbol used to separate tokens in the id attribute of every cell * * Default CustomInputGroup.separator * * This option is passed to each individual item. * * @see mixinSettings */ this.separator = CustomInputGroup.separator; /** * ### CustomInputGroup.shuffleChoices * * If TRUE, choices in items are shuffled * * This option is passed to each individual item. * * @see mixinSettings */ this.shuffleChoices = null; /** * ### CustomInputGroup.sharedOptions * * An object containing options to be added to every custom input * * Options are added only if forms are specified as object literals, * and can be overriden by each individual form. */ this.sharedOptions = {}; /** * ### CustomInputGroup.summaryInput * * A summary custom input added last which can be updated in real time * * @see CustomInputGroup.summaryInputCb */ this.summaryInput = null; /** * ### CustomInputGroup.errorBox * * An HTML element displayed when a validation error occurs */ this.errorBox = null; /** * ### CustomInputGroup.validation * * The callback validating all the inputs at once * * The callback is executed by getValues. * * Input paramers: * * - res: the validation result of all inputs * - values: object literal containing the current value of each input * - widget: a reference to this widget * * Return value: * * - res: the result object as it is on success, or with with an err * property containing the error message on failure. Any change * to the result object is carried over. * * @see CustomInputGroup.oninput */ this.validation = null; /** * ### CustomInputGroup._validation * * Reference to the user defined validation function * * @api private */ this._validation = null; /** * ### CustomInputGroup.oninput * * Callback called when any input has changed * * Input paramers: * * - res: the validation result of the single input * - input: the custom input that fired oninput * - widget: a reference to this widget */ this.oninput = null; /** * ### CustomInputGroup._oninput * * Reference to the user defined oninput function * * @api private */ this._oninput = null; } // ## CustomInputGroup methods /** * ### CustomInputGroup.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 * - onclick: a custom onclick 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 * - sharedOptions: Options shared across all inputs * - summary: An object containing the options to instantiate a custom * input summary field. * - validation: A validation callback for all inputs. * - oninput: A callback called when any input is changed * * @param {object} opts Configuration options */ CustomInputGroup.prototype.init = function(opts) { var tmp, that; that = this; // TODO: many options checking are replicated. Skip them all? // Have a method in CustomInput? if (!this.id) { throw new TypeError('CustomInputGroup.init: id ' + 'is missing.'); } // Option orientation, default 'H'. if ('undefined' === typeof opts.orientation) { tmp = 'V'; } else if ('string' !== typeof opts.orientation) { throw new TypeError('CustomInputGroup.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('CustomInputGroup.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('CustomInputGroup.init: ' + 'requiredChoice ' + 'be number or boolean or undefined. Found: ' + opts.requiredChoice); } if ('undefined' !== typeof opts.required) { this.required = !!opts.required; } // 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('CustomInputGroup.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('CustomInputGroup.init: groupOrder ' + 'must be number or undefined. Found: ' + opts.groupOrder); } // Set the validation function. if ('function' === typeof opts.validation) { this._validation = opts.validation; this.validation = function(res, values) { if (!values) values = that.getValues({ valuesOnly: true }); return that._validation(res || {}, values, that) }; } else if ('undefined' !== typeof opts.validation) { throw new TypeError('CustomInputGroup.init: validation must ' + 'be function or undefined. Found: ' + opts.validation); } // Set the validation function. if ('function' === typeof opts.oninput) { this._oninput = opts.oninput; this.oninput = function(res, input) { that._oninput(res, input, that); }; } else if ('undefined' !== typeof opts.oninput) { throw new TypeError('CustomInputGroup.init: oninput must ' + 'be function or undefined. Found: ' + opts.oninput); } // Set the mainText, if any. if ('string' === typeof opts.mainText) { this.mainText = opts.mainText; } else if ('undefined' !== typeof opts.mainText) { throw new TypeError('CustomInputGroup.init: mainText ' + 'must be string or undefined. Found: ' + opts.mainText); } // Set the hint, if any. if ('string' === typeof opts.hint) { this.hint = opts.hint; if (this.requiredChoice) this.hint += ' *'; } else if ('undefined' !== typeof opts.hint) { throw new TypeError('CustomInputGroup.init: hint must ' + 'be a string, 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('CustomInputGroup.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 className, if not use default. if ('undefined' === typeof opts.className) { this.className = CustomInputGroup.className; } else if (opts.className === false || 'string' === typeof opts.className || J.isArray(opts.className)) { this.className = opts.className; } else { throw new TypeError('CustomInputGroup.init: ' + 'className must be string, array, ' + 'or undefined. Found: ' + opts.className); } // sharedOptions. if ('undefined' !== typeof opts.sharedOptions) { if ('object' !== typeof opts.sharedOptions) { throw new TypeError('CustomInputGroup.init: sharedOptions' + ' must be object or undefined. Found: ' + opts.sharedOptions); } if (opts.sharedOptions.hasOwnProperty('name')) { throw new Error('CustomInputGroup.init: sharedOptions ' + 'cannot contain property name. Found: ' + opts.sharedOptions); } this.sharedOptions = J.mixin(this.sharedOptions, opts.sharedOptions); } if ('undefined' !== typeof opts.summary) { if ('string' === typeof opts.summary) { opts.summary = { mainText: opts.summary } } else if ('object' !== typeof opts.summary) { throw new TypeError('CustomInputGroup.init: summary' + ' must be object or undefined. Found: ' + opts.summary); } this.summaryInput = opts.summary; } // After all configuration options 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('CustomInputGroup.init: table ' + 'must be object, false or undefined. ' + 'Found: ' + opts.table); } // TODO: check this. this.table = opts.table; this.freeText = 'string' === typeof opts.freeText ? opts.freeText : !!opts.freeText; // Add the items. if ('undefined' !== typeof opts.items) this.setItems(opts.items); }; /** * ### CustomInputGroup.setItems * * Sets the available items and optionally builds the table * * @param {array} items The array of items * * @see CustomInputGroup.table * @see CustomInputGroup.order * @see CustomInputGroup.shuffleItems * @see CustomInputGroup.buildTable */ CustomInputGroup.prototype.setItems = function(items) { var len; if (!J.isArray(items)) { throw new TypeError('CustomInputGroup.setItems: ' + 'items must be array. Found: ' + items); } if (!items.length) { throw new Error('CustomInputGroup.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(); }; /** * ### CustomInputGroup.buildTable * * Builds the table of clickable items and enables it * * Must be called after items have been set already. * * @see CustomInputGroup.setCustomInputs */ CustomInputGroup.prototype.buildTable = function() { var i, len, tr, H; H = this.orientation === 'H'; i = -1, len = this.itemsSettings.length; if (H) tr = createTR(this, 'row1'); for ( ; ++i < len ; ) { // Add new TR. if (!H) tr = createTR(this, 'row' + (i+1)); addCustomInput(this, tr, i); } if (this.summaryInput) { if (!H) tr = createTR(this, 'row' + (i+1)); addSummaryInput(this, tr, i); } var that = this; this.table.onclick = function() { // Remove any warning/error from form on click. if (that.isHighlighted()) that.unhighlight(); }; this.enable(true); }; /** * ### CustomInputGroup.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 */ CustomInputGroup.prototype.append = function() { // Id must be unique. if (W.getElementById(this.id)) { throw new Error('CustomInputGroup.append: id ' + 'is not unique: ' + this.id); } // MainText. if (this.mainText) { this.spanMainText = W.append('span', this.bodyDiv, { className: 'custominputgroup-maintext', innerHTML: this.mainText }); } // Hint. if (this.hint) { W.append('span', this.spanMainText || this.bodyDiv, { className: 'custominputgroup-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'); this.textarea.id = this.id + '_text'; this.textarea.className = CustomInputGroup.className + '-freetext'; if ('string' === typeof this.freeText) { this.textarea.placeholder = this.freeText; } // Append textarea. this.bodyDiv.appendChild(this.textarea); } }; /** * ### CustomInputGroup.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 */ CustomInputGroup.prototype.listeners = function() { var that = this; node.on('INPUT_DISABLE', function() { that.disable(); }); node.on('INPUT_ENABLE', function() { that.enable(); }); }; /** * ### CustomInputGroup.disable * * Disables clicking on the table and removes CSS 'clicklable' class */ CustomInputGroup.prototype.disable = function() { if (this.disabled === true || !this.table) return; this.disabled = true; this.emit('disabled'); }; /** * ### CustomInputGroup.enable * * Enables clicking on the table and adds CSS 'clicklable' class * * @return {function} cb The event listener function */ CustomInputGroup.prototype.enable = function(force) { if (!this.table || (!force && !this.disabled)) return; this.disabled = false; this.emit('enabled'); }; /** * ### CustomInputGroup.highlight * * Highlights the choice table * * @param {string} The style for the table's border. * Default '1px solid red' * * @see CustomInputGroup.highlighted */ CustomInputGroup.prototype.highlight = function(border) { if (border && 'string' !== typeof border) { throw new TypeError('CustomInputGroup.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); }; /** * ### CustomInputGroup.unhighlight * * Removes highlight from the choice table * * @see CustomInputGroup.highlighted */ CustomInputGroup.prototype.unhighlight = function() { if (!this.table || this.highlighted !== true) return; this.table.style.border = ''; this.highlighted = false; this.errorBox.innerHTML = ''; this.emit('unhighlighted'); }; /** * ### CustomInputGroup.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: * * - valuesOnly: just returns the current values, no other checkings. * - 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 CustomInputGroup.verifyChoice * @see CustomInputGroup.reset */ CustomInputGroup.prototype.getValues = function(opts) { var res, i, len, input, toReset, values; opts = opts || {}; i = -1, len = this.items.length; if (opts.valuesOnly) { res = {}; for ( ; ++i < len ; ) { res[this.items[i].id] = this.items[i].getValues({ valuesOnly: true }); } return res; } res = { id: this.id, order: this.order, items: {}, isCorrect: true }; if ('undefined' === typeof opts.highlight) opts.highlight = true; // TODO: do we need markAttempt? // if ('undefined' === typeof opts.markAttempt) opts.markAttempt = true; // Make sure reset is done only at the end. toReset = opts.reset; opts.reset = false; if (this.validation) values = {}; for ( ; ++i < len ; ) { input = this.items[i]; res.items[input.id] = input.getValues(opts); // TODO is null or empty? if (res.items[input.id].value === "") { res.missValues = true; // TODO: check do we need to check for correctChoice? if (input.requiredChoice || input.required || input.correctChoice) { res.err = true; res.isCorrect = false; } } if (res.items[input.id].isCorrect === false && opts.highlight) { res.err = true; } if (values) values[input.id] = res.items[input.id].value; } if (!res.err && values) { // 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; }; /** * ### CustomInputGroup.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 CustomInput.setValues * * @experimental */ CustomInputGroup.prototype.setValues = function(opts) { var i, len; if (!this.items || !this.items.length) { throw new Error('CustomInputGroup.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'); }; /** * ### CustomInputGroup.reset * * Resets all the CustomInput items and textarea * * @param {object} options Optional. Options specifying how to set * to reset each item * * @see CustomInput.reset * @see CustomInputGroup.shuffle */ CustomInputGroup.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(); }; /** * ### CustomInputGroup.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 */ CustomInputGroup.prototype.shuffle = function(opts) { var order, i, len, 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; }; /** * ### CustomInputGroup.setError * * Set the error msg inside the errorBox and call highlight * * @param {string} The error msg (can contain HTML) * * @see CustomInput.highlight * @see CustomInput.errorBox */ CustomInputGroup.prototype.setError = function(err) { this.errorBox.innerHTML = err; this.highlight(); }; // ## Helper methods. /** * ### mixinSettings * * Mix-ins global settings with local settings for specific choice tables * * @param {CustomInputGroup} 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 ('object' !== typeof s) { throw new TypeError('CustomInputGroup.buildTable: item must be ' + 'string or object. Found: ' + s); } s.group = that.id; s.groupOrder = i+1; s.orientation = that.orientation; s.title = false; if (that.oninput) s.oninput = that.oninput; if ('undefined' === typeof s.requiredChoice && that.requiredChoice) { s.requiredChoice = that.requiredChoice; } if ('undefined' === typeof s.timeFrom) s.timeFrom = that.timeFrom; s = J.mixout(s, that.sharedOptions); // No reference is stored in node.widgets. s.storeRef = false; return s; } /** * ### addCustomInput * * Creates a instance i-th of choice table with relative settings * * Stores a reference of each input in `itemsById` * * @param {CustomInputGroup} that This instance * @param {HTMLElement} tr A TR element where the custom input is appended * @param {number} i The ordinal position of the table in the group * * @return {object} ct The requested choice table * * @see CustomInputGroup.itemsSettings * @see CustomInputGroup.itemsById * @see mixinSettings */ function addCustomInput(that, tr, i) { var ci, s, td, idx; idx = that.order[i]; s = mixinSettings(that, that.itemsSettings[idx], i); td = document.createElement('td'); tr.appendChild(td); ci = node.widgets.append('CustomInput', td, s); if (that.itemsById[ci.id]) { throw new Error('CustomInputGroup.buildTable: an input ' + 'with the same id already exists: ' + ci.id); } that.itemsById[ci.id] = ci; that.items[idx] = ci; that.itemsMap[ci.id] = idx; if (s.required || s.requiredChoice || s.correctChoice) { // False is set manually, otherwise undefined. if (that.required === false) { throw new Error('CustomInputGroup.buildTable: required is ' + 'false, but item "' + s.id + '" has required truthy'); } that.required = true; } return ci; } /** * ### addSummaryInput * * Creates the last summary input * * Stores a reference in `summaryInput` * * @param {CustomInputGroup} that This instance * @param {HTMLElement} tr A TR element where the custom input is appended * @param {number} i The ordinal position of the table in the group * * @return {object} ct The requested choice table * * @see CustomInputGroup.itemsSettings * @see CustomInputGroup.itemsById * @see mixinSettings */ function addSummaryInput(that, tr, i) { var ci, s, td; s = J.mixout({ id: that.id + '_summary', storeRef: false, title: false, panel: false, className: 'custominputgroup-summary', disabled: true }, that.sharedOptions); s = J.mixin(s, that.summaryInput); td = document.createElement('td'); tr.appendChild(td); ci = node.widgets.append('CustomInput', td, s); that.summaryInput = ci; return ci; } /** * ### 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 {CustomInput} 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);