@oat-sa/tao-item-runner-qti
Version:
TAO QTI Item Runner modules
993 lines (880 loc) • 74 kB
JavaScript
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);