UNPKG

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

Version:
1,028 lines (888 loc) 43.8 kB
define(['jquery', 'lodash', 'i18n', 'ui/hider', 'handlebars', 'lib/handlebars/helpers', 'taoQtiItem/qtiCommonRenderer/helpers/container', 'taoQtiItem/qtiCommonRenderer/helpers/instructions/instructionManager', 'taoQtiItem/qtiCommonRenderer/helpers/PciResponse', 'taoQtiItem/qtiCommonRenderer/helpers/sizeAdapter', 'interact', 'ui/interactUtils'], function ($$1, _, __, hider, Handlebars, Helpers0, containerHelper, instructionMgr, pciResponse, sizeAdapter, interact, interactUtils) { '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'] : __; hider = hider && Object.prototype.hasOwnProperty.call(hider, 'default') ? hider['default'] : hider; 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; sizeAdapter = sizeAdapter && Object.prototype.hasOwnProperty.call(sizeAdapter, 'default') ? sizeAdapter['default'] : sizeAdapter; interact = interact && Object.prototype.hasOwnProperty.call(interact, 'default') ? interact['default'] : interact; interactUtils = interactUtils && Object.prototype.hasOwnProperty.call(interactUtils, 'default') ? interactUtils['default'] : interactUtils; 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, options, functionType="function", escapeExpression=this.escapeExpression, self=this, blockHelperMissing=helpers.blockHelperMissing; 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 stack1; stack1 = (typeof depth0 === functionType ? depth0.apply(depth0) : depth0); if(stack1 || stack1 === 0) { return stack1; } else { return ''; } } buffer += "<div\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-blockInteraction qti-associateInteraction"; 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=\"associateInteraction\"\n "; 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>\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 <ul class=\"choice-area clearfix none block-listing solid horizontal source\">\n "; options={hash:{},inverse:self.noop,fn:self.program(9, program9, data),data:data}; if (helper = helpers.choices) { stack1 = helper.call(depth0, options); } else { helper = (depth0 && depth0.choices); stack1 = typeof helper === functionType ? helper.call(depth0, options) : helper; } if (!helpers.choices) { stack1 = blockHelperMissing.call(depth0, stack1, {hash:{},inverse:self.noop,fn:self.program(9, program9, data),data:data}); } if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n </ul>\n <ul class=\"result-area none target clearfix\">\n </ul>\n <div class=\"notification-container\"></div>\n</div>\n"; return buffer; }); function tpl(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, functionType="function", escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { return "hidden incomplete-pair"; } function program3(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; } buffer += "<li class=\""; stack1 = helpers['if'].call(depth0, (depth0 && depth0.empty), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\""; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['xml:lang']), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += ">\n <div class=\"target lft\"></div>\n <div class=\"target rgt\"></div>\n</li>\n"; return buffer; }); function pairTpl(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-2019 (original work) Open Assessment Technlogies SA (under the project TAO-PRODUCT); * */ const setChoice = function (interaction, $choice, $target) { const $container = containerHelper.get(interaction); const choiceSerial = $choice.data('serial'); const choice = interaction.getChoice(choiceSerial); let usage = $choice.data('usage') || 0; if (!choiceSerial) { throw new Error('empty choice serial'); } //to track number of times a choice is used in a pair usage++; $choice.data('usage', usage); const _setChoice = function () { $target.data('serial', choiceSerial).html($choice.html()).addClass('filled'); if (!interaction.responseMappingMode && choice.attr('matchMax') && usage >= choice.attr('matchMax')) { $choice.addClass('deactivated'); } }; if ($target.siblings('div').hasClass('filled')) { const $resultArea = $$1('.result-area', $container); const $pair = $target.parent(); const thisPairSerial = [$target.siblings('div').data('serial'), choiceSerial]; let $otherRepeatedPair = $$1(); //check if it is not a repeating association! $resultArea .children() .not($pair) .each(function () { let $otherPair = $$1(this).children('.filled'); if ($otherPair.length === 2) { let otherPairSerial = [$$1($otherPair[0]).data('serial'), $$1($otherPair[1]).data('serial')]; if (_.intersection(thisPairSerial, otherPairSerial).length === 2) { $otherRepeatedPair = $otherPair; return false; } } }); if ($otherRepeatedPair.length === 0) { //no repeated pair, so allow the choice to be set: _setChoice(); //trigger pair made event containerHelper.triggerResponseChangeEvent(interaction, { type: 'added', $pair: $pair, choices: thisPairSerial }); instructionMgr.validateInstructions(interaction, { choice: $choice, target: $target }); if (interaction.responseMappingMode || parseInt(interaction.attr('maxAssociations')) === 0) { $pair.removeClass('incomplete-pair'); //append new pair option? if (!$resultArea.children('.incomplete-pair').length) { $resultArea.append(pairTpl({ empty: true })); $resultArea.children('.incomplete-pair').fadeIn(600, function () { hider.show(this); }); } } } else { //repeating pair: show it: //@todo add a notification message here in warning $otherRepeatedPair.css('border', '1px solid orange'); $target.html(__('identical pair already exists')).css({ color: 'orange', border: '1px solid orange' }); setTimeout(function () { $otherRepeatedPair.removeAttr('style'); $target.empty().css({ color: '', border: '' }); }, 2000); } } else { _setChoice(); } }; const unsetChoice = function (interaction, $filledChoice, animate, triggerChange) { const $container = containerHelper.get(interaction); const choiceSerial = $filledChoice.data('serial'); const $choice = $container.find('.choice-area [data-serial=' + choiceSerial + ']'); const $parent = $filledChoice.parent(); const $sibling = $container.find( '.choice-area [data-serial=' + $filledChoice.siblings('.target').data('serial') + ']' ); const isNumberOfMaxAssociationsZero = parseInt(interaction.attr('maxAssociations')) === 0; let usage = $choice.data('usage') || 0; //decrease the use for this choice usage--; $choice.data('usage', usage).removeClass('deactivated'); $filledChoice.removeClass('filled').removeData('serial').empty(); if (!interaction.swapping) { if (triggerChange !== false) { //a pair with one single element is not valid, so consider the response to be modified: containerHelper.triggerResponseChangeEvent(interaction, { type: 'removed', $pair: $filledChoice.parent() }); instructionMgr.validateInstructions(interaction, { choice: $choice }); } // if we are removing the sibling too, update its usage // but only if number of maximum assotiations is zero if (isNumberOfMaxAssociationsZero) { $sibling.data('usage', $sibling.data('usage') - 1).removeClass('deactivated'); } //completely empty pair: if ( !$choice.siblings('div').hasClass('filled') && (isNumberOfMaxAssociationsZero || interaction.responseMappingMode) ) { //shall we remove it? if (!$parent.hasClass('incomplete-pair')) { if (animate) { $parent.addClass('removing').fadeOut(500, function () { $$1(this).remove(); }); } else { $parent.remove(); } } } } }; const getChoice = function (interaction, identifier) { const $container = containerHelper.get(interaction); //warning: do not use selector data-identifier=identifier because data-identifier may change dynamically const choice = interaction.getChoiceByIdentifier(identifier); if (!choice) { throw new Error('cannot find a choice with the identifier : ' + identifier); } return $$1('.choice-area [data-serial=' + choice.getSerial() + ']', $container); }; const renderEmptyPairs = function (interaction) { const $container = containerHelper.get(interaction); const max = parseInt(interaction.attr('maxAssociations')); const $resultArea = $$1('.result-area', $container); if (interaction.responseMappingMode || max === 0) { $resultArea.append(pairTpl({ empty: true })); hider.show($resultArea.children('.incomplete-pair')); } else { for (let i = 0; i < max; i++) { $resultArea.append(pairTpl()); } } }; /** * Builds a scroll observer that will make sure the dragged element keeps an accurate positioning * @param {jQuery} $scrollContainer * @returns {scrollObserver} */ const scrollObserverFactory = function scrollObserverFactory($scrollContainer) { let currentDraggable = null; let beforeY = 0; let beforeX = 0; let afterY = 0; let afterX = 0; // reset the scroll observer context function resetScrollObserver() { currentDraggable = null; beforeY = 0; beforeX = 0; afterY = 0; afterX = 0; } // keep the position of the dragged element accurate with the scroll position function onScrollCb() { let x; let y; if (currentDraggable) { beforeY = afterY; beforeX = afterX; if (afterY === 0 && beforeY === 0) beforeY = this.scrollTop; if (afterX === 0 && beforeX === 0) beforeX = this.scrollLeft; afterY = this.scrollTop; afterX = this.scrollLeft; y = (parseInt(currentDraggable.getAttribute('data-y'), 10) || 0) + (afterY - beforeY); x = (parseInt(currentDraggable.getAttribute('data-x'), 10) || 0) + (afterX - beforeX); // translate the element currentDraggable.style.webkitTransform = currentDraggable.style.transform = `translate(${x}px, ${y}px)`; // update the position attributes currentDraggable.setAttribute('data-x', x); currentDraggable.setAttribute('data-y', y); } } // find the scroll container within the parents if any $scrollContainer.parents().each(function findScrollContainer() { const $el = $$1(this); const ovf = $el.css('overflow'); if (ovf !== 'hidden' && ovf !== 'visible') { $scrollContainer = $el; return false; } }); // make sure the drop zones will follow the scroll interact.dynamicDrop(true); /** * @typedef {Object} scrollObserver */ return { /** * Gets the scroll container * @returns {jQuery} */ getScrollContainer: function getScrollContainer() { return $scrollContainer; }, /** * Initializes the scroll observer while dragging a choice * @param {HTMLElement|jQuery} draggedElement */ start: function start(draggedElement) { resetScrollObserver(); currentDraggable = draggedElement instanceof $$1 ? draggedElement.get(0) : draggedElement; $scrollContainer.on('scroll.scrollObserver', _.throttle(onScrollCb, 50)); }, /** * Tears down the the scroll observer once the dragging is done */ stop: function stop() { $scrollContainer.off('.scrollObserver'); resetScrollObserver(); } }; }; const _getRawResponse = function (interaction) { const response = []; const $container = containerHelper.get(interaction); $$1('.result-area>li', $container).each(function () { const pair = []; $$1(this) .find('div') .each(function () { const serial = $$1(this).data('serial'); if (serial) { const choice = interaction.getChoice(serial); if (choice) { pair.push(choice.id()); } } }); if (pair.length === 2) { response.push(pair); } }); return response; }; const _setInstructions = function (interaction) { const min = parseInt(interaction.attr('minAssociations'), 10); const max = parseInt(interaction.attr('maxAssociations'), 10); //infinite association: if (min === 0) { if (max === 0) { instructionMgr.appendInstruction(interaction, __('You may make as many association pairs as you want.')); } } else { if (max === 0) { instructionMgr.appendInstruction(interaction, __('The maximum number of association is unlimited.')); } //the max value is implicit since the appropriate number of empty pairs have already been created let msg = __('You need to make') + ' '; msg += min > 1 ? __('at least') + ' ' + min + ' ' + __('association pairs') : __('one association pair'); instructionMgr.appendInstruction(interaction, msg, function () { if (_getRawResponse(interaction).length >= min) { this.setLevel('success'); } else { this.reset(); } }); } }; /** * 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#element10291 * * @param {object} interaction */ const render = function (interaction) { const self = this; return new Promise(function (resolve) { const $container = containerHelper.get(interaction); const $choiceArea = $container.find('.choice-area'); const $resultArea = $container.find('.result-area'); let $activeChoice = null; let scrollObserver = null; let isDragAndDropEnabled; let dragOptions; let dropOptions; let scaleX, scaleY; let $bin = $$1('<span>', { class: 'icon-undo remove-choice', title: __('remove') }); let choiceSelector = $choiceArea.selector + ' >li'; let resultSelector = $resultArea.selector + ' >li>div'; let binSelector = $container.selector + ' .remove-choice'; let _getChoice = function (serial) { return $choiceArea.find('[data-serial=' + serial + ']'); }; /** * @todo Tried to store $resultArea.find[...] in a variable but this fails * @param $choice * @param $target * @private */ const _setChoice = function ($choice, $target) { setChoice(interaction, $choice, $target); sizeAdapter.adaptSize( $$1('.result-area .target, .choice-area .qti-choice', containerHelper.get(interaction)) ); }; const _resetSelection = function () { if ($activeChoice) { $resultArea.find('.remove-choice').remove(); $activeChoice.removeClass('active'); $container.find('.empty').removeClass('empty'); $activeChoice = null; } }; const _unsetChoice = function ($choice) { unsetChoice(interaction, $choice, true); sizeAdapter.adaptSize( $$1('.result-area .target, .choice-area .qti-choice', containerHelper.get(interaction)) ); }; const _isInsertionMode = function () { return $activeChoice && $activeChoice.data('identifier'); }; const _isModeEditing = function () { return $activeChoice && !$activeChoice.data('identifier'); }; const _activateChoice = function ($choice) { _resetSelection(); $activeChoice = $choice; $choice.addClass('active'); $resultArea.find('>li>.target').addClass('empty'); }; const _handleChoiceActivate = function ($target) { if ($target.hasClass('deactivated')) { return; } if (_isModeEditing()) { //swapping: interaction.swapping = true; _unsetChoice($activeChoice); _setChoice($target, $activeChoice); _resetSelection(); interaction.swapping = false; } else { if ($target.hasClass('active')) { _resetSelection(); } else { _activateChoice($target); } } }; const _activateResult = function ($target) { const targetSerial = $target.data('serial'); $activeChoice = $target; $activeChoice.addClass('active'); $resultArea .find('>li>.target') .filter(function () { return $$1(this).data('serial') !== targetSerial; }) .addClass('empty'); $choiceArea .find('>li:not(.deactivated)') .filter(function () { return $$1(this).data('serial') !== targetSerial; }) .addClass('empty'); }; const _handleResultActivate = function ($target) { let choiceSerial, targetSerial = $target.data('serial'); if (_isInsertionMode()) { choiceSerial = $activeChoice.data('serial'); if (targetSerial !== choiceSerial) { if ($target.hasClass('filled')) { interaction.swapping = true; //hack to prevent deleting empty pair in infinite association mode } //set choices: if (targetSerial) { _unsetChoice($target); } _setChoice($activeChoice, $target); //always reset swapping mode after the choice is set interaction.swapping = false; } _resetSelection(); } else if (_isModeEditing()) { choiceSerial = $activeChoice.data('serial'); if (targetSerial !== choiceSerial) { if ($target.hasClass('filled') || $activeChoice.siblings('div')[0] === $target[0]) { interaction.swapping = true; //hack to prevent deleting empty pair in infinite association mode } _unsetChoice($activeChoice); if (targetSerial) { //swapping: _unsetChoice($target); _setChoice(_getChoice(targetSerial), $activeChoice); } _setChoice(_getChoice(choiceSerial), $target); //always reset swapping mode after the choice is set interaction.swapping = false; } _resetSelection(); } else if (targetSerial) { _activateResult($target); $target.append($bin); } }; // Point & click handlers interact($container.selector).on('tap', function (e) { //if tts component is loaded and click-to-speak function is activated - we should prevent this listener to go further if ($$1(e.currentTarget).closest('.qti-item').hasClass('prevent-click-handler')) { return; } _resetSelection(); }); interact($choiceArea.selector + ' >li').on('tap', function (e) { const $target = $$1(e.currentTarget); //if tts component is loaded and click-to-speak function is activated - we should prevent this listener to go further if ($target.closest('.qti-item').hasClass('prevent-click-handler')) { return; } e.stopPropagation(); _handleChoiceActivate($target); e.preventDefault(); }); interact($resultArea.selector + ' >li>div').on('tap', function (e) { const $target = $$1(e.currentTarget); //if tts component is loaded and click-to-speak function is activated - we should prevent this listener to go further if ($target.closest('.qti-item').hasClass('prevent-click-handler')) { return; } e.stopPropagation(); _handleResultActivate($target); e.preventDefault(); }); interact(binSelector).on('tap', function (e) { //if tts component is loaded and click-to-speak function is activated - we should prevent this listener to go further if ($$1(e.currentTarget).closest('.qti-item').hasClass('prevent-click-handler')) { return; } e.stopPropagation(); _unsetChoice($activeChoice); _resetSelection(); e.preventDefault(); }); if (!interaction.responseMappingMode) { _setInstructions(interaction); } // Drag & drop handlers if (self.getOption && self.getOption('enableDragAndDrop') && self.getOption('enableDragAndDrop').associate) { isDragAndDropEnabled = self.getOption('enableDragAndDrop').associate; } function _iFrameDragFix(draggableSelector, target) { interactUtils.iFrameDragFixOn(function () { let $activeDrop = $$1(resultSelector + '.dropzone'); if ($activeDrop.length) { interact(resultSelector).fire({ type: 'drop', target: $activeDrop.eq(0), relatedTarget: target }); } $activeDrop = $$1(choiceSelector + '.dropzone'); if ($activeDrop.length) { interact(choiceSelector + '.empty').fire({ type: 'drop', target: $activeDrop.eq(0), relatedTarget: target }); } interact(draggableSelector).fire({ type: 'dragend', target: target }); }); } if (isDragAndDropEnabled) { scrollObserver = scrollObserverFactory($container); dragOptions = { inertia: false, autoScroll: { container: scrollObserver.getScrollContainer().get(0) }, restrict: { restriction: '.qti-interaction', endOnly: false, elementRect: { top: 0, left: 0, bottom: 1, right: 1 } } }; // makes choices draggables interact(choiceSelector + ':not(.deactivated)') .draggable( _.defaults( { onstart: function (e) { let $target = $$1(e.target); let scale; $target.addClass('dragged'); _activateChoice($target); _iFrameDragFix(choiceSelector + ':not(.deactivated)', e.target); scale = interactUtils.calculateScale(e.target); scaleX = scale[0]; scaleY = scale[1]; scrollObserver.start($activeChoice); }, onmove: function (e) { interactUtils.moveElement(e.target, e.dx / scaleX, e.dy / scaleY); }, onend: function (e) { let $target = $$1(e.target); $target.removeClass('dragged'); // The reason of placing delay here is that there was timing conflict between "draggable" and "drag-zone" elements. _.delay(function () { _resetSelection(); }); interactUtils.restoreOriginalPosition($target); interactUtils.iFrameDragFixOff(); scrollObserver.stop(); } }, dragOptions ) ) .styleCursor(false); // makes results draggables interact(resultSelector + '.filled') .draggable( _.defaults( { onstart: function (e) { let $target = $$1(e.target); let scale; $target.addClass('dragged'); _resetSelection(); _activateResult($target); _iFrameDragFix(resultSelector + '.filled', e.target); scale = interactUtils.calculateScale(e.target); scaleX = scale[0]; scaleY = scale[1]; scrollObserver.start($activeChoice); }, onmove: function (e) { interactUtils.moveElement(e.target, e.dx / scaleX, e.dy / scaleY); }, onend: function (e) { let $target = $$1(e.target); $target.removeClass('dragged'); interactUtils.restoreOriginalPosition($target); if ($activeChoice) { _unsetChoice($activeChoice); } _resetSelection(); interactUtils.iFrameDragFixOff(); scrollObserver.stop(); } }, dragOptions ) ) .styleCursor(false); dropOptions = { overlap: 'pointer', ondragenter: function (e) { $$1(e.target).addClass('dropzone'); $$1(e.relatedTarget).addClass('droppable'); }, ondragleave: function (e) { $$1(e.target).removeClass('dropzone'); $$1(e.relatedTarget).removeClass('droppable'); } }; // makes hotspots droppables interact(resultSelector).dropzone( _.defaults( { ondrop: function (e) { this.ondragleave(e); _handleResultActivate($$1(e.target)); } }, dropOptions ) ); // makes available choices droppables interact(choiceSelector + '.empty').dropzone( _.defaults( { ondrop: function (e) { this.ondragleave(e); _handleChoiceActivate($$1(e.target)); } }, dropOptions ) ); } // interaction init renderEmptyPairs(interaction); sizeAdapter.adaptSize($$1('.result-area .target, .choice-area .qti-choice', $container)); resolve(); }); }; const resetResponse = function (interaction) { const $container = containerHelper.get(interaction); //destroy selected choice: $container.find('.result-area .active').each(function () { interactUtils.tapOn(this); }); $$1('.result-area>li>div', $container).each(function () { unsetChoice(interaction, $$1(this), false, false); }); containerHelper.triggerResponseChangeEvent(interaction); instructionMgr.validateInstructions(interaction); }; const _setPairs = function (interaction, pairs) { const $container = containerHelper.get(interaction); let addedPairs = 0; let $emptyPair = $$1('.result-area>li:first', $container); if (pairs && interaction.getResponseDeclaration().attr('cardinality') === 'single' && pairs.length) { pairs = [pairs]; } _.forEach(pairs, function (pair) { if ($emptyPair.length) { let $divs = $emptyPair.children('div'); setChoice(interaction, getChoice(interaction, pair[0]), $$1($divs[0])); setChoice(interaction, getChoice(interaction, pair[1]), $$1($divs[1])); addedPairs++; $emptyPair = $emptyPair.next('li'); } else { //the number of pairs exceeds the maximum allowed pairs: break; return false; } }); return addedPairs; }; /** * 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#element10291 * * @param {object} interaction * @param {object} response */ const setResponse = function (interaction, response) { _setPairs(interaction, pciResponse.unserialize(response, interaction)); }; /** * 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#element10291 * * @param {object} interaction * @returns {object} */ const getResponse = function (interaction) { return pciResponse.serialize(_getRawResponse(interaction), interaction); }; /** * Destroy the interaction by leaving the DOM exactly in the same state it was before loading the interaction. * @param {Object} interaction - the interaction */ const destroy = function (interaction) { const $container = containerHelper.get(interaction); //remove event interact($container.selector).unset(); interact($container.find('.choice-area').selector + ' >li').unset(); interact($container.find('.result-area').selector + ' >li>div').unset(); interact($container.find('.remove-choice').selector).unset(); //remove instructions instructionMgr.removeInstructions(interaction); $$1('.result-area', $container).empty(); //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 */ const setState = function setState(interaction, state) { let $container; if (_.isObject(state)) { if (state.response) { interaction.resetResponse(); interaction.setResponse(state.response); } //restore order of previously shuffled choices if (_.isArray(state.order) && state.order.length === _.size(interaction.getChoices())) { $container = containerHelper.get(interaction); $$1('.choice-area .qti-choice', $container) .sort(function (a, b) { let aIndex = _.indexOf(state.order, $$1(a).data('identifier')); let bIndex = _.indexOf(state.order, $$1(b).data('identifier')); if (aIndex > bIndex) { return 1; } if (aIndex < bIndex) { return -1; } return 0; }) .detach() .appendTo($$1('.choice-area', $container)); } } }; /** * Get the interaction state. * * @param {Object} interaction - the interaction instance * @returns {Object} the interaction current state */ const getState = function getState(interaction) { let $container; let state = {}; let response = interaction.getResponse(); if (response) { state.response = response; } //we store also the choice order if shuffled if (interaction.attr('shuffle') === true) { $container = containerHelper.get(interaction); state.order = []; $$1('.choice-area .qti-choice', $container).each(function () { state.order.push($$1(this).data('identifier')); }); } return state; }; /** * Expose the common renderer for the associate interaction * @exports qtiCommonRenderer/renderers/interactions/AssociateInteraction */ var AssociateInteraction = { qtiClass: 'associateInteraction', template: tpl, render: render, getContainer: containerHelper.get, setResponse: setResponse, getResponse: getResponse, resetResponse: resetResponse, destroy: destroy, setState: setState, getState: getState, renderEmptyPairs: renderEmptyPairs }; return AssociateInteraction; });