UNPKG

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

Version:
993 lines (880 loc) 74 kB
define(['jquery', 'lodash', 'i18n', 'services/features', 'util/strLimiter', 'handlebars', 'lib/handlebars/helpers', 'taoQtiItem/qtiCommonRenderer/helpers/container', 'taoQtiItem/qtiCommonRenderer/helpers/instructions/instructionManager', 'ckeditor', 'taoQtiItem/qtiCommonRenderer/helpers/ckConfigurator', 'taoQtiItem/qtiCommonRenderer/helpers/patternMask', 'taoQtiItem/qtiCommonRenderer/helpers/userAgent', 'ui/tooltip', 'util/converter', 'core/logger', 'taoQtiItem/qtiCommonRenderer/helpers/verticalWriting'], function ($$1, _, __, features, strLimiter, Handlebars, Helpers0, containerHelper, instructionMgr, ckEditor, ckConfigurator, patternMaskHelper, userAgent, tooltip, converter, loggerFactory, 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'] : __; features = features && Object.prototype.hasOwnProperty.call(features, 'default') ? features['default'] : features; strLimiter = strLimiter && Object.prototype.hasOwnProperty.call(strLimiter, 'default') ? strLimiter['default'] : strLimiter; 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; ckEditor = ckEditor && Object.prototype.hasOwnProperty.call(ckEditor, 'default') ? ckEditor['default'] : ckEditor; ckConfigurator = ckConfigurator && Object.prototype.hasOwnProperty.call(ckConfigurator, 'default') ? ckConfigurator['default'] : ckConfigurator; patternMaskHelper = patternMaskHelper && Object.prototype.hasOwnProperty.call(patternMaskHelper, 'default') ? patternMaskHelper['default'] : patternMaskHelper; tooltip = tooltip && Object.prototype.hasOwnProperty.call(tooltip, 'default') ? tooltip['default'] : tooltip; converter = converter && Object.prototype.hasOwnProperty.call(converter, 'default') ? converter['default'] : converter; loggerFactory = loggerFactory && Object.prototype.hasOwnProperty.call(loggerFactory, 'default') ? loggerFactory['default'] : loggerFactory; 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, helperMissing=helpers.helperMissing; 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; } function program5(depth0,data) { var buffer = "", stack1; buffer += " lang=\"" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['xml:lang'])),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + "\""; return buffer; } function program7(depth0,data) { var stack1, helper; if (helper = helpers.prompt) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.prompt); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } if(stack1 || stack1 === 0) { return stack1; } else { return ''; } } function program9(depth0,data) { var buffer = "", stack1, helper, options; buffer += "\n "; stack1 = (helper = helpers.equal || (depth0 && depth0.equal),options={hash:{},inverse:self.program(14, program14, data),fn:self.program(10, program10, data),data:data},helper ? helper.call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.format), "xhtml", options) : helperMissing.call(depth0, "equal", ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.format), "xhtml", options)); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n "; return buffer; } function program10(depth0,data) { var buffer = "", stack1; buffer += "\n "; stack1 = helpers.each.call(depth0, (depth0 && depth0.maxStringLoop), {hash:{},inverse:self.noop,fn:self.program(11, program11, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n "; return buffer; } function program11(depth0,data) { var buffer = "", stack1; buffer += "\n <div class=\"text-container text-" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.format)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + " solid"; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class']), {hash:{},inverse:self.noop,fn:self.program(12, program12, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\" name=\"" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.identifier)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + "_" + escapeExpression((typeof depth0 === functionType ? depth0.apply(depth0) : depth0)) + "\" contenteditable></div>\n "; return buffer; } function program12(depth0,data) { return " attributes.class"; } function program14(depth0,data) { var buffer = "", stack1; buffer += "\n "; stack1 = helpers.each.call(depth0, (depth0 && depth0.maxStringLoop), {hash:{},inverse:self.noop,fn:self.program(15, program15, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n "; return buffer; } function program15(depth0,data) { var buffer = "", stack1, helper; buffer += "\n <textarea\n class=\"text-container text-" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.format)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + " solid"; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class']), {hash:{},inverse:self.noop,fn:self.program(12, program12, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\"\n name=\"" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.identifier)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + "_" + escapeExpression((typeof depth0 === functionType ? depth0.apply(depth0) : depth0)) + "\"\n "; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.patternMask), {hash:{},inverse:self.noop,fn:self.program(16, program16, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n aria-labelledby=\""; if (helper = helpers.promptId) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.promptId); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\"\n ></textarea>\n "; return buffer; } function program16(depth0,data) { var buffer = "", stack1; buffer += "pattern=\"" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.patternMask)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + "\""; return buffer; } function program18(depth0,data) { var buffer = "", stack1, helper, options; buffer += "\n "; stack1 = (helper = helpers.equal || (depth0 && depth0.equal),options={hash:{},inverse:self.program(21, program21, data),fn:self.program(19, program19, data),data:data},helper ? helper.call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.format), (depth0 && depth0.xhtml), options) : helperMissing.call(depth0, "equal", ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.format), (depth0 && depth0.xhtml), options)); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n "; return buffer; } function program19(depth0,data) { var buffer = "", stack1; buffer += "\n <div class=\"text-container text-" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.format)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + " solid"; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class']), {hash:{},inverse:self.noop,fn:self.program(12, program12, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\" contenteditable></div>\n "; return buffer; } function program21(depth0,data) { var buffer = "", stack1, helper; buffer += "\n <textarea\n class=\"text-container text-" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.format)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + " solid"; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class']), {hash:{},inverse:self.noop,fn:self.program(12, program12, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\"\n "; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.patternMask), {hash:{},inverse:self.noop,fn:self.program(16, program16, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n aria-labelledby=\""; if (helper = helpers.promptId) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.promptId); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\"\n ></textarea>\n "; return buffer; } function program23(depth0,data) { var buffer = "", stack1, helper, options; buffer += "\n "; stack1 = (helper = helpers.dompurify || (depth0 && depth0.dompurify),options={hash:{},data:data},helper ? helper.call(depth0, ((stack1 = (depth0 && depth0.constraintHints)),stack1 == null || stack1 === false ? stack1 : stack1.expectedLength), options) : helperMissing.call(depth0, "dompurify", ((stack1 = (depth0 && depth0.constraintHints)),stack1 == null || stack1 === false ? stack1 : stack1.expectedLength), options)); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n "; return buffer; } function program25(depth0,data) { var buffer = "", stack1, helper, options; buffer += "\n <span class=\"text-counter-chars\""; stack1 = helpers.unless.call(depth0, (depth0 && depth0.maxLength), {hash:{},inverse:self.noop,fn:self.program(26, program26, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += ">"; stack1 = (helper = helpers.dompurify || (depth0 && depth0.dompurify),options={hash:{},data:data},helper ? helper.call(depth0, ((stack1 = (depth0 && depth0.constraintHints)),stack1 == null || stack1 === false ? stack1 : stack1.maxLength), options) : helperMissing.call(depth0, "dompurify", ((stack1 = (depth0 && depth0.constraintHints)),stack1 == null || stack1 === false ? stack1 : stack1.maxLength), options)); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "</span>\n <span class=\"text-counter-words\""; stack1 = helpers.unless.call(depth0, (depth0 && depth0.maxWords), {hash:{},inverse:self.noop,fn:self.program(26, program26, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += ">"; stack1 = (helper = helpers.dompurify || (depth0 && depth0.dompurify),options={hash:{},data:data},helper ? helper.call(depth0, ((stack1 = (depth0 && depth0.constraintHints)),stack1 == null || stack1 === false ? stack1 : stack1.maxWords), options) : helperMissing.call(depth0, "dompurify", ((stack1 = (depth0 && depth0.constraintHints)),stack1 == null || stack1 === false ? stack1 : stack1.maxWords), options)); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "</span>\n "; return buffer; } function program26(depth0,data) { return " style=\"display: none\""; } buffer += "<div "; 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 += " class=\"qti-interaction qti-blockInteraction qti-extendedTextInteraction"; 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 += "\" 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) + "\" data-qti-class=\"extendedTextInteraction\""; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['xml:lang']), {hash:{},inverse:self.noop,fn:self.program(5, program5, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += ">\n "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.prompt), {hash:{},inverse:self.noop,fn:self.program(7, program7, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n <div class=\"instruction-container\"></div>\n "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.multiple), {hash:{},inverse:self.program(18, program18, data),fn:self.program(9, program9, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n <div class=\"text-counter\">\n "; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.expectedLength), {hash:{},inverse:self.program(25, program25, data),fn:self.program(23, program23, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n </div>\n</div>\n"; return buffer; }); function template(data, options, asString) { var html = Template(data, options); return (asString || true) ? html : $(html); } if (!Helpers0.__initialized) { Helpers0(Handlebars); Helpers0.__initialized = true; } var Template$1 = 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; buffer += "<span class=\""; if (helper = helpers.name) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.name); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\">"; if (helper = helpers.value) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.value); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "</span>\n"; return buffer; }); function countTpl(data, options, asString) { var html = Template$1(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-2023 (original work) Open Assessment Technlogies SA (under the project TAO-PRODUCT); * */ /** * Create a logger */ const logger = loggerFactory('taoQtiItem/qtiCommonRenderer/renderers/interactions/ExtendedTextInteraction.js'); const hideXhtmlConstraints = !features.isVisible( 'taoQtiItem/creator/interaction/extendedText/property/xhtmlConstraints' ); const hideXhtmlRecommendations = !features.isVisible( 'taoQtiItem/creator/interaction/extendedText/property/xhtmlRecommendations' ); /** * 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#element10296 * * @param {Object} interaction - the extended text interaction model * @returns {Promise} rendering is async */ function render(interaction) { return new Promise(function (resolve, reject) { let $el, expectedLength, minStrings, patternMask, placeholderType, editor; let _styleUpdater, themeLoaded, _getNumStrings; let $container = containerHelper.get(interaction); const multiple = _isMultiple(interaction); const limiter = inputLimiter(interaction); const placeholderText = interaction.attr('placeholderText'); const serial = $container.data('serial'); const getItemLanguage = () => { let itemLang = $container.closest('.qti-item').attr('lang'); let itemLocale = itemLang && itemLang.split('-')[0]; if (!itemLocale) { itemLang = window.document.documentElement.getAttribute('lang'); itemLocale = itemLang && itemLang.split('-')[0]; } return itemLocale; }; const toolbarType = 'extendedText'; const ckOptions = { resize_enabled: true, secure: location.protocol === 'https:', forceCustomDomain: true, language: getItemLanguage() }; if (!multiple) { $el = $container.find('textarea'); if (placeholderText) { $el.attr('placeholder', placeholderText); } if (_getFormat(interaction) === 'xhtml') { if (hideXhtmlConstraints && hideXhtmlRecommendations) { $container.find('.text-counter').hide(); } if (hideXhtmlConstraints) { limiter.enabled = false; } _styleUpdater = function () { let qtiItemStyle, $editorBody, qtiItem; if (editor.document) { qtiItem = $$1('.qti-item').get(0); qtiItemStyle = qtiItem.currentStyle || window.getComputedStyle(qtiItem); if (editor.document.$ && editor.document.$.body) { $editorBody = $$1(editor.document.$.body); } else { $editorBody = $$1(editor.document.getBody().$); } $editorBody.css({ 'background-color': 'transparent', color: qtiItemStyle.color }); } }; themeLoaded = function () { _styleUpdater(); }; editor = _setUpCKEditor(interaction, ckOptions); if (!editor) { reject('Unable to instantiate ckEditor'); } editor.on('instanceReady', function () { _styleUpdater(); //TAO-6409, disable navigation from cke toolbar if (editor.container && editor.container.$) { $$1(editor.container.$).addClass('no-key-navigation'); } //it seems there's still something done after loaded, so resolved must be defered _.delay(resolve, 300); }); if (editor.status === 'ready' || editor.status === 'loaded') { _.defer(resolve); } editor.on('configLoaded', function () { editor.config = ckConfigurator.getConfig(editor, toolbarType, ckOptions); if (limiter.enabled) { limiter.listenTextInput(); } }); editor.on('change', function () { containerHelper.triggerResponseChangeEvent(interaction, {}); }); $$1(document).on('themechange.themeloader', themeLoaded); } else { const isVertical = verticalWriting.getIsItemWritingModeVerticalRl(); if (isVertical) { const textareaSupportsVertical = verticalWriting.supportsVerticalFormElement(); if (!textareaSupportsVertical) { $el.addClass('vertical-unsupported'); } } $el.on('keyup.commonRenderer change.commonRenderer', function () { containerHelper.triggerResponseChangeEvent(interaction, {}); }); if (limiter.enabled) { limiter.listenTextInput(); } interaction.safariVerticalRlPatch = _patchSafariVerticalRl($el, serial); resolve(); } //multiple inputs } else { $el = $container.find('input'); minStrings = interaction.attr('minStrings'); expectedLength = interaction.attr('expectedLength'); patternMask = interaction.attr('patternMask'); //setting the checking for minimum number of answers if (minStrings) { //get the number of filled inputs _getNumStrings = function ($element) { let num = 0; $element.each(function () { if ($$1(this).val() !== '') { num++; } }); return num; }; minStrings = parseInt(minStrings, 10); if (minStrings > 0) { $el.on('blur.commonRenderer', function () { setTimeout(function () { //checking if the user was clicked outside of the input fields //TODO remove notifications in favor of instructions if (!$el.is(':focus') && _getNumStrings($el) < minStrings) { instructionMgr.appendNotification( interaction, `${__('The minimum number of answers is ')} : ${minStrings}`, 'warning' ); } }, 100); }); } } //set the fields width if (expectedLength) { expectedLength = parseInt(expectedLength, 10); if (expectedLength > 0) { $el.each(function () { $$1(this).css('width', `${expectedLength}em`); }); } } //set the fields pattern mask if (patternMask) { $el.each(function () { _setPattern($$1(this), patternMask); }); } //set the fields placeholder if (placeholderText) { /** * The type of the fileds placeholder: * multiple - set placeholder for each field * first - set placeholder only for first field * none - dont set placeholder */ placeholderType = 'first'; if (placeholderType === 'multiple') { $el.each(function () { $$1(this).attr('placeholder', placeholderText); }); } else if (placeholderType === 'first') { $el.first().attr('placeholder', placeholderText); } } resolve(); } }); } /** * Reset the textarea / ckEditor * @param {Object} interaction - the extended text interaction model */ function resetResponse(interaction) { if (_getFormat(interaction) === 'xhtml') { _getCKEditor(interaction).setData(''); } else { containerHelper.get(interaction).find('input, textarea').val(''); if (interaction.safariVerticalRlPatch) { interaction.safariVerticalRlPatch.syncValue(); } } } /** * 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#element10296 * * @param {Object} interaction - the extended text interaction model * @param {object} response */ function setResponse(interaction, response) { const _setMultipleVal = (identifier, value) => { interaction.getContainer().find(`#${identifier}`).val(value); }; const baseType = interaction.getResponseDeclaration().attr('baseType'); if (response.base === null && Object.keys(response).length === 1) { response = { base: { string: '' } }; } if (response.base && typeof response.base[baseType] !== 'undefined') { setText(interaction, response.base[baseType]); } else if (response.list && response.list[baseType]) { for (let i in response.list[baseType]) { const serial = typeof response.list.serial === 'undefined' ? '' : response.list.serial[i]; _setMultipleVal(`${serial}_${i}`, response.list[baseType][i]); } } else { throw new Error('wrong response format in argument.'); } } /** * 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#element10296 * * @param {Object} interaction - the extended text interaction model * @returns {object} */ function getResponse(interaction) { const $container = containerHelper.get(interaction); const attributes = interaction.getAttributes(); const responseDeclaration = interaction.getResponseDeclaration(); const baseType = responseDeclaration.attr('baseType'); const numericBase = attributes.base || 10; const multiple = !!( attributes.maxStrings && (responseDeclaration.attr('cardinality') === 'multiple' || responseDeclaration.attr('cardinality') === 'ordered') ); const ret = multiple ? { list: {} } : { base: {} }; let values; let value = ''; if (multiple) { values = []; $container.find('input').each(function (i) { const editorValue = $$1(this).val(); if (attributes.placeholderText && value === attributes.placeholderText) { values[i] = ''; } else { const convertedValue = converter.convert(editorValue); if (baseType === 'integer') { values[i] = parseInt(convertedValue, numericBase); values[i] = isNaN(values[i]) ? '' : values[i]; } else if (baseType === 'float') { values[i] = parseFloat(convertedValue); values[i] = isNaN(values[i]) ? '' : values[i]; } else if (baseType === 'string') { values[i] = convertedValue; } } }); ret.list[baseType] = values; } else { if (attributes.placeholderText && _getTextareaValue(interaction) === attributes.placeholderText) { value = ''; } else { if (baseType === 'integer') { value = parseInt(converter.convert(_getTextareaValue(interaction)), numericBase); } else if (baseType === 'float') { value = converter.convert(_getTextareaValue(interaction)); } else if (baseType === 'string') { value = converter.convert(_getTextareaValue(interaction, true)); } } ret.base[baseType] = isNaN(value) && typeof value === 'number' ? '' : value; } return ret; } /** * Creates an input limiter object * @param {Object} interaction - the extended text interaction * @returns {Object} the limiter */ function inputLimiter(interaction) { const $container = containerHelper.get(interaction); const expectedLength = interaction.attr('expectedLength'); const expectedLines = interaction.attr('expectedLines'); const patternMask = interaction.attr('patternMask'); const isCke = _getFormat(interaction) === 'xhtml'; const isVertical = verticalWriting.getIsItemWritingModeVerticalRl(); let patternRegEx; let $textarea, $charsCounter, $wordsCounter, maxWords, maxLength, $maxLengthCounter, $maxWordsCounter, $expectedLengthCounter; let enabled = false; if (expectedLength || expectedLines || patternMask) { enabled = true; $textarea = $$1('.text-container', $container); $charsCounter = $$1('.count-chars', $container); $wordsCounter = $$1('.count-words', $container); $maxLengthCounter = $$1('.count-max-length', $container); $maxWordsCounter = $$1('.count-max-words', $container); $expectedLengthCounter = $$1('.count-expected-length', $container); if (patternMask !== '') { maxWords = parseInt(patternMaskHelper.parsePattern(patternMask, 'words'), 10); maxLength = parseInt(patternMaskHelper.parsePattern(patternMask, 'chars'), 10); maxWords = _.isNaN(maxWords) ? 0 : maxWords; maxLength = _.isNaN(maxLength) ? 0 : maxLength; if (!maxLength && !maxWords) { patternRegEx = new RegExp(patternMask); } $maxLengthCounter.html(maxLength); $maxWordsCounter.text(maxWords); } if (expectedLength || expectedLines) { $expectedLengthCounter.html($expectedLengthCounter.text()); } } /** * The limiter instance */ const limiter = { /** * Is the limiter enabled regarding the interaction configuration */ enabled, /** * Listen for text input into the interaction and limit it if necessary */ listenTextInput() { const ignoreKeyCodes = [ 8, // backspace 13, // enter 16, // shift 17, // control 46, // delete 37, // arrow left 38, // arrow up 39, // arrow right 40, // arrow down 35, // home 36, // end // ckeditor specific: 1114177, // home 3342401, // Shift + home 1114181, // end 3342405, // Shift + end 2228232, // Shift + backspace 2228261, // Shift + arrow left 4456485, // Alt + arrow left 2228262, // Shift + arrow up 2228263, // Shift + arrow right 4456487, // Alt + arrow right 2228264, // Shift + arrow down 2228237, // Shift + enter 1114120, // Ctrl + backspace 1114177, // Ctrl + a 1114202, // Ctrl + z 1114200 // Ctrl + x ]; const spaceKeyCodes = [ 32, // space 13, // enter 2228237 // shift + enter in ckEditor ]; let isComposing = false; let hasCompositionJustEnded = false; const acceptKeyCode = keyCode => ignoreKeyCodes.includes(keyCode); const emptyOrSpace = txt => (txt && txt.trim() === '') || /\^s*$/.test(txt); const hasSpace = txt => /\s+/.test(txt); const getCharBefore = (str, pos) => str && str.substring(Math.max(0, pos - 1), pos); const getCharAfter = (str, pos) => str && str.substring(pos, pos + 1); const noSpaceNode = node => node.type === ckEditor.NODE_TEXT || (!node.isBlockBoundary() && node.getName() !== 'br'); const getPreviousNotEmptyNode = range => { let node = range.getPreviousNode(); /** * The previous node isn't always the right one, because it can be an empty <b> tag for example. * So we need to get the previous node until we find a non empty one, but we should not go above body. */ while (node && (node.isEmpty ? node.isEmpty() : node.getText() === '')) { let previousSourceNode = node.getPreviousSourceNode(); let nodeElement = previousSourceNode; if (previousSourceNode && previousSourceNode.type === ckEditor.NODE_TEXT) { nodeElement = previousSourceNode.parentNode || previousSourceNode.$.parentNode; } if ( !nodeElement || !nodeElement.ownerDocument || !nodeElement.ownerDocument.body.contains(nodeElement) ) { return null; } node = previousSourceNode; } return node; }; const getNextNotEmptyNode = range => { let node = range.getNextNode(); while (node && (node.isEmpty ? node.isEmpty() : node.getText() === '')) { let nextSourceNode = node.getNextSourceNode(); let nodeElement = nextSourceNode; if (nextSourceNode && nextSourceNode.type === ckEditor.NODE_TEXT) { nodeElement = nextSourceNode.parentNode || nextSourceNode.$.parentNode; } if ( !nodeElement || !nodeElement.ownerDocument || !nodeElement.ownerDocument.body.contains(nodeElement) ) { return null; } node = nextSourceNode; } return node; }; const cancelEvent = e => { if (e.cancel) { e.cancel(); } else { e.preventDefault(); e.stopImmediatePropagation(); } return false; }; const invalidToolip = tooltip.error($container, __('This is not a valid answer'), { position: 'bottom', trigger: 'manual' }); const patternHandler = function patternHandler(e) { if (isComposing || hasCompositionJustEnded) { // IME composing fires keydown/keyup events hasCompositionJustEnded = false; return; } if (patternRegEx) { let newValue; if (isCke) { // cke has its own object structure newValue = this.getData(); } else { // covers input newValue = e.currentTarget.value; } if (!newValue) { return false; } _.debounce(function () { if (!patternRegEx.test(newValue)) { $container.addClass('invalid'); $container.show(); invalidToolip.show(); containerHelper.triggerResponseChangeEvent(interaction); } else { $container.removeClass('invalid'); invalidToolip.dispose(); } }, 400)(); } }; /** * This part works on keyboard input * * @param {Event} e * @returns {boolean} */ const keyLimitHandler = e => { if (isComposing) { return; } // Safari on OS X may send a keydown of 229 after compositionend if (e.which !== 229) { hasCompositionJustEnded = false; } const keyCode = e.data ? e.data.keyCode : e.which; const wordsCount = maxWords && this.getWordsCount(); const charsCount = maxLength && this.getCharsCount(); if (maxWords && wordsCount >= maxWords) { let left, right, middle; if (isCke) { const editor = _getCKEditor(interaction); const sel = editor.getSelection(); const range = sel.getRanges()[0]; if (range.startContainer && range.startContainer.type === ckEditor.NODE_TEXT) { left = getCharBefore(range.startContainer.getText(), range.startOffset); } if (!left) { const node = getPreviousNotEmptyNode(range); if (node && noSpaceNode(node)) { const text = node.getText(); left = getCharBefore(text, text && text.length); } else { left = ' '; } } if (range.endContainer && range.endContainer.type === ckEditor.NODE_TEXT) { right = getCharAfter(range.endContainer.getText(), range.endOffset); } if (!right) { const node = getNextNotEmptyNode(range); if (node && noSpaceNode(node)) { right = getCharAfter(node.getText(), 0); } else { right = ' '; } } middle = sel.getSelectedText(); } else { const { selectionStart, selectionEnd, value } = $textarea[0]; left = getCharBefore(value, selectionStart); right = getCharAfter(value, selectionEnd); middle = value.substring(selectionStart, selectionEnd); } // Will prevent the keystroke: // - IF there is a word part before and after the selection, // AND the selection does not contain spaces, // AND the keystroke is either a space or enter // - IF there is no word part before and after the selection, // AND the selection is empty, // AND the keystroke is not from the list of accepted codes, // AND the keystroke is not a space if ( (!emptyOrSpace(left) && !emptyOrSpace(right) && !hasSpace(middle) && spaceKeyCodes.includes(keyCode)) || (emptyOrSpace(left) && emptyOrSpace(right) && !middle && !acceptKeyCode(keyCode) && keyCode !== 32) ) { return cancelEvent(e); } } if (maxLength && charsCount >= maxLength && !acceptKeyCode(keyCode)) { if (!isCke && charsCount > maxLength) { const textarea = $textarea[0]; textarea.value = textarea.value.substring(0, maxLength); $textarea.trigger('inputlimiter-limited'); textarea.focus(); } return cancelEvent(e); } _.defer(() => this.updateCounter()); }; /** * This part works on drop or paste * @param {Event} e * @returns {boolean} */ const nonKeyLimitHandler = e => { let newValue; if (typeof $$1(e.target).attr('data-clipboard') === 'string') { newValue = $$1(e.target).attr('data-clipboard'); } else if (isCke) { // cke has its own object structure newValue = e.data.dataValue; } else { // covers input via paste or drop newValue = e.originalEvent.clipboardData ? e.originalEvent.clipboardData.getData('text') : e.originalEvent.dataTransfer.getData('text') || e.originalEvent.dataTransfer.getData('text/plain') || ''; } // prevent insertion of non-limited data cancelEvent(e); if (!newValue) { return false; } // limit by word or character count if required if (maxWords) { newValue = strLimiter.limitByWordCount(newValue, maxWords - this.getWordsCount()); } else if (maxLength) { newValue = strLimiter.limitByCharCount(newValue, maxLength - this.getCharsCount()); } // insert the cut-off text if (isCke) { _getCKEditor(interaction).insertHtml(newValue); } else { let elements = containerHelper.get(interaction).find('textarea'); let el = elements[0]; let { selectionStart: start, selectionEnd: end, value: text } = el; elements.val(text.substring(0, start) + newValue + text.substring(end, text.length)); el.focus(); el.selectionStart = start + newValue.length; el.selectionEnd = el.selectionStart; elements.trigger('inputlimiter-limited'); } _.defer(() => this.updateCounter()); }; const handleCompositionStart = e => { isComposing = true; return e; }; const handleCompositionEnd = e => { isComposing = false; hasCompositionJustEnded = true; // if plain text - then limit input right after composition end event if (_getFormat(interaction) !== 'xhtml' && maxLength) { const currentValue = $textarea[0].value; const currentLength = this.getCharsCount(); if (currentLength > maxLength) { $textarea[0].value = currentValue.slice(0, maxLength - currentLength);