UNPKG

@oat-sa/tao-item-runner-qti

Version:
504 lines (446 loc) 22.6 kB
define(['jquery', 'lodash', 'i18n', 'handlebars', 'lib/handlebars/helpers', 'taoQtiItem/qtiCommonRenderer/helpers/container', 'taoQtiItem/qtiCommonRenderer/helpers/instructions/instructionManager', 'taoQtiItem/qtiCommonRenderer/helpers/PciResponse', 'taoQtiItem/qtiCommonRenderer/helpers/patternMask', 'util/locale', 'ui/tooltip', 'core/logger', 'util/converter', 'taoQtiItem/qtiCommonRenderer/helpers/verticalWriting'], function ($$1, _, __, Handlebars, Helpers0, containerHelper, instructionMgr, pciResponse, patternMaskHelper, locale, tooltip, loggerFactory, converter, verticalWriting) { 'use strict'; $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1; _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _; __ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __; Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars; Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0; containerHelper = containerHelper && Object.prototype.hasOwnProperty.call(containerHelper, 'default') ? containerHelper['default'] : containerHelper; instructionMgr = instructionMgr && Object.prototype.hasOwnProperty.call(instructionMgr, 'default') ? instructionMgr['default'] : instructionMgr; pciResponse = pciResponse && Object.prototype.hasOwnProperty.call(pciResponse, 'default') ? pciResponse['default'] : pciResponse; patternMaskHelper = patternMaskHelper && Object.prototype.hasOwnProperty.call(patternMaskHelper, 'default') ? patternMaskHelper['default'] : patternMaskHelper; locale = locale && Object.prototype.hasOwnProperty.call(locale, 'default') ? locale['default'] : locale; tooltip = tooltip && Object.prototype.hasOwnProperty.call(tooltip, 'default') ? tooltip['default'] : tooltip; loggerFactory = loggerFactory && Object.prototype.hasOwnProperty.call(loggerFactory, 'default') ? loggerFactory['default'] : loggerFactory; converter = converter && Object.prototype.hasOwnProperty.call(converter, 'default') ? converter['default'] : converter; if (!Helpers0.__initialized) { Helpers0(Handlebars); Helpers0.__initialized = true; } var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) { this.compilerInfo = [4,'>= 1.0.0']; helpers = this.merge(helpers, Handlebars.helpers); data = data || {}; var buffer = "", stack1, helper, functionType="function", escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { var buffer = "", stack1; buffer += "id=\"" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.id)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + "\""; return buffer; } function program3(depth0,data) { var buffer = "", stack1; buffer += " " + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class'])),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)); return buffer; } buffer += "<input\n "; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.id), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n class=\"qti-interaction qti-inlineInteraction qti-textEntryInteraction"; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class']), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\"\n data-serial=\""; if (helper = helpers.serial) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.serial); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\"\n data-qti-class=\"textEntryInteraction\"\n type=\"text\"\n>"; return buffer; }); function tpl(data, options, asString) { var html = Template(data, options); return (asString || true) ? html : $(html); } /* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; under version 2 * of the License (non-upgradable). * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Copyright (c) 2014-2022 Open Assessment Technologies SA (under the project TAO-PRODUCT); * */ /** * Create a logger */ const logger = loggerFactory('taoQtiItem/qtiCommonRenderer/renderers/interactions/TextEntryInteraction.js'); /** * Hide the tooltip for the text input * @param {jQuery} $input */ function hideTooltip($input) { if ($input.data('$tooltip')) { $input.data('$tooltip').hide(); $input.data('textentry-tooltip-is-shown', false); } } /** * Create/Show tooltip for the text input * @param {jQuery} $input * @param {String} theme * @param {String} message */ function showTooltip($input, theme, message) { const isVertical = verticalWriting.getIsItemWritingModeVerticalRl(); message = verticalWriting.wrapDigitsInCombineUpright(message, isVertical); if ($input.data('$tooltip')) { $input.data('$tooltip').updateTitleContent(message); } else { let tooltipOptions = { theme: theme, trigger: 'manual', placement: isVertical ? 'right' : 'top' }; const textEntryTooltip = tooltip.create($input, message, tooltipOptions); $input.data('$tooltip', textEntryTooltip); } $input.data('$tooltip').show(); $input.data('textentry-tooltip-is-shown', true); } /** * Refresh tooltip position * @param {jQuery} $input */ function refreshTooltip($input) { if ($input.data('$tooltip') && $input.data('textentry-tooltip-is-shown')) { $input.data('$tooltip').hide(); $input.data('$tooltip').show(); } } /** * Validate the input for decimal values. * * This function ensures that the input value is either empty or follows * the rules for decimal numbers. It allows numbers with optional * thousands separators (commas) and a mandatory decimal point (dot). * * @param {jQuery} $input * @param {Object} options - `{ allowMinusOnly: boolean, withTooltip: boolean }` */ function validateDecimalInput($input, { allowMinusOnly = false, withTooltip = true } = {}) { const separatorName = { '.': __('(dot)'), ',': __('(comma)'), ' ': __('(space)') }; const value = converter.convert($input.val()); const thousandsSeparator = locale.getThousandsSeparator(); const decimalSeparator = locale.getDecimalSeparator(); const thousandsSeparatorName = separatorName[thousandsSeparator] ? separatorName[thousandsSeparator] : ''; const decimalSeparatorName = separatorName[decimalSeparator] ? separatorName[decimalSeparator] : ''; const escapedThousandsSeparator = thousandsSeparator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedDecimalSeparator = decimalSeparator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); let regexPattern = `^$|^-?\\d+(${escapedDecimalSeparator}\\d+)?$|^-?\\d*${escapedDecimalSeparator}$|^-?${escapedDecimalSeparator}\\d+$`; if (thousandsSeparator) { regexPattern += `|^-?\\d{1,3}(${escapedThousandsSeparator}\\d{3})*(${escapedDecimalSeparator}\\d+)?$`; } if (allowMinusOnly) { regexPattern += '|^-$'; } const regex = new RegExp(regexPattern); if (!regex.test(value)) { $input.addClass('error'); if (withTooltip) { const decimalError = thousandsSeparator ? __( 'Invalid value, use %s %s for decimal point and %s %s for thousands separator.', decimalSeparator, decimalSeparatorName, thousandsSeparator, thousandsSeparatorName ) : __('Invalid value, use %s %s for decimal point.', decimalSeparator, decimalSeparatorName); showTooltip($input, 'error', decimalError); } else { hideTooltip($input); } } else { $input.removeClass('error'); hideTooltip($input); } } /** * Validate the input for integer values. * @param {jQuery} $input * @param {Object} options - `{ allowMinusOnly: boolean, withTooltip: boolean }` */ function validateIntegerInput($input, { allowMinusOnly = false, withTooltip = true } = {}) { const value = converter.convert($input.val()); const regex = new RegExp(`^${allowMinusOnly ? '-?' : ''}$|^-?\\d+$`); if (!regex.test(value)) { $input.addClass('error'); if (withTooltip) { showTooltip($input, 'error', __('Invalid value, should be an integer number.')); } else { hideTooltip($input); } } else { $input.removeClass('error'); hideTooltip($input); } } /** * Init rendering, called after template injected into the DOM * All options are listed in the QTI v2.1 information model: * http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10333 * * @param {object} interaction */ function render(interaction) { const attributes = interaction.getAttributes(); const baseType = interaction.getResponseDeclaration().attr('baseType'); const $input = interaction.getContainer(); const serial = $input.data('serial'); const patternMask = interaction.attr('patternMask'); const maxChars = parseInt(patternMaskHelper.parsePattern(patternMask, 'chars'), 10); let expectedLength; // Setting up baseType switch (baseType) { case 'integer': $input.attr('inputmode', 'numeric'); $input .on('input.commonRenderer', () => validateIntegerInput($input, { allowMinusOnly: true, withTooltip: true }) ) .on('focus.commonRenderer', () => validateIntegerInput($input, { allowMinusOnly: false, withTooltip: true }) ) .on('blur.commonRenderer', () => validateIntegerInput($input, { allowMinusOnly: false, withTooltip: false }) ); break; case 'float': $input.attr('inputmode', 'decimal'); $input .on('input.commonRenderer', () => validateDecimalInput($input, { allowMinusOnly: true, withTooltip: true }) ) .on('focus.commonRenderer', () => validateDecimalInput($input, { allowMinusOnly: false, withTooltip: true }) ) .on('blur.commonRenderer', () => validateDecimalInput($input, { allowMinusOnly: false, withTooltip: false }) ); break; default: $input.attr('inputmode', 'text'); } //setting up the width of the input field if (attributes.expectedLength) { //adding 2 chars to include reasonable padding size expectedLength = parseInt(attributes.expectedLength) + 2; $input.css('inline-size', expectedLength + 'ch'); $input.css('min-inline-size', expectedLength + 'ch'); } //checking if there's a placeholder for the input if (attributes.placeholderText) { $input.attr('placeholder', attributes.placeholderText); } if (maxChars) { const updateMaxCharsTooltip = () => { const count = $input.val().length; let message; let messageType; if (count) { message = __('%d/%d', count, maxChars); } else { message = __('%d characters allowed', maxChars); } if (count >= maxChars) { $input.addClass('maxed'); messageType = 'warning'; } else { $input.removeClass('maxed'); messageType = 'info'; } showTooltip($input, messageType, message); if (count && messageType === 'warning') { hideTooltip($input); } }; $input .attr('maxlength', maxChars) .on('focus.commonRenderer', function () { updateMaxCharsTooltip(); }) .on('keyup.commonRenderer', function () { updateMaxCharsTooltip(); containerHelper.triggerResponseChangeEvent(interaction); }) .on('blur.commonRenderer', function () { hideTooltip($input); }); } else if (attributes.patternMask) { const updatePatternMaskTooltip = () => { const regex = new RegExp(attributes.patternMask); hideTooltip($input); if ($input.val()) { if (regex.test($input.val())) { $input.removeClass('error'); } else { $input.addClass('error'); showTooltip($input, 'error', __('This is not a valid answer')); } } }; $input .on('focus.commonRenderer', function () { updatePatternMaskTooltip(); }) .on('keyup.commonRenderer', function () { updatePatternMaskTooltip(); containerHelper.triggerResponseChangeEvent(interaction); }) .on('blur.commonRenderer', function () { hideTooltip($input); }); } else { $input.on('keyup.commonRenderer', function () { containerHelper.triggerResponseChangeEvent(interaction); }); } //refresh tooltip position when all styles loaded. $$1(document).on(`themeapplied.textEntryInteraction-${serial}`, () => { refreshTooltip($input); }); } function resetResponse(interaction) { interaction.getContainer().val(''); } /** * Set the response to the rendered interaction. * * The response format follows the IMS PCI recommendation : * http://www.imsglobal.org/assessment/pciv1p0cf/imsPCIv1p0cf.html#_Toc353965343 * * Available base types are defined in the QTI v2.1 information model: * http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10333 * * Special value: the empty object value {} resets the interaction responses * * @param {object} interaction * @param {object} response */ function setResponse(interaction, response) { let responseValue; try { responseValue = pciResponse.unserialize(response, interaction); } catch (e) { logger.warn(`setResponse error ${e}`); } if (responseValue && responseValue.length) { interaction.getContainer().val(responseValue[0]); } } /** * Return the response of the rendered interaction * * The response format follows the IMS PCI recommendation : * http://www.imsglobal.org/assessment/pciv1p0cf/imsPCIv1p0cf.html#_Toc353965343 * * Available base types are defined in the QTI v2.1 information model: * http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10333 * * @param {object} interaction * @returns {object} */ function getResponse(interaction) { const ret = { base: {} }; const $input = interaction.getContainer(); const attributes = interaction.getAttributes(); const baseType = interaction.getResponseDeclaration().attr('baseType'); const numericBase = attributes.base || 10; const inputValue = $input.val(); let value; if ( (attributes.patternMask && $input.hasClass('error')) || (attributes.placeholderText && inputValue === attributes.placeholderText) ) { //invalid response or response equals to the placeholder text are considered empty value = ''; } else { const convertedValue = converter.convert(inputValue.trim()); if (baseType === 'integer') { value = locale.parseInt(convertedValue, numericBase); } else if (baseType === 'float') { value = locale.parseFloat(convertedValue); } else if (baseType === 'string') { value = convertedValue; } } ret.base[baseType] = isNaN(value) && typeof value === 'number' ? '' : value; return ret; } function destroy(interaction) { const $interaction = containerHelper.get(interaction); const serial = $interaction.data('serial'); $$1('input.qti-textEntryInteraction').each(function (index, el) { const $input = $$1(el); if ($input.data('$tooltip')) { $input.data('$tooltip').dispose(); $input.removeData('$tooltip'); } }); //remove event $$1(document).off('.commonRenderer'); $interaction.off('.commonRenderer'); $$1(document).off(`.textEntryInteraction-${serial}`); //remove instructions instructionMgr.removeInstructions(interaction); //remove all references to a cache container containerHelper.reset(interaction); } /** * Set the interaction state. It could be done anytime with any state. * * @param {Object} interaction - the interaction instance * @param {Object} state - the interaction state */ function setState(interaction, state) { if (_.isObject(state)) { if (state.response) { interaction.resetResponse(); interaction.setResponse(state.response); } } } /** * Get the interaction state. * * @param {Object} interaction - the interaction instance * @returns {Object} the interaction current state */ function getState(interaction) { const state = {}; const response = interaction.getResponse(); if (response) { state.response = response; } const $input = interaction.getContainer(); if ($input.hasClass('error')) { state.validity = { isValid: false }; } return state; } var TextEntryInteraction = { qtiClass: 'textEntryInteraction', template: tpl, render: render, getContainer: containerHelper.get, setResponse: setResponse, getResponse: getResponse, resetResponse: resetResponse, destroy: destroy, setState: setState, getState: getState }; return TextEntryInteraction; });