diff --git a/package-lock.json b/package-lock.json index 2722f428..666bfbe4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-item-runner-qti", - "version": "1.6.6", + "version": "1.6.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 33b5e015..77729af9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-item-runner-qti", - "version": "1.6.6", + "version": "1.6.7", "displayName": "TAO Item Runner QTI", "description": "TAO QTI Item Runner modules", "files": [ diff --git a/src/qtiCommonRenderer/helpers/Graphic.js b/src/qtiCommonRenderer/helpers/Graphic.js index a962f2bb..906fc4aa 100644 --- a/src/qtiCommonRenderer/helpers/Graphic.js +++ b/src/qtiCommonRenderer/helpers/Graphic.js @@ -13,7 +13,7 @@ * 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) 2015 (original work) Open Assessment Technologies SA ; + * Copyright (c) 2015-2023 (original work) Open Assessment Technologies SA ; */ /** @@ -27,13 +27,13 @@ import scaleRaphael from 'scale.raphael'; import gstyle from 'taoQtiItem/qtiCommonRenderer/renderers/graphic-style'; //maps the QTI shapes to Raphael shapes -var shapeMap = { +const shapeMap = { default: 'rect', poly: 'path' }; //length constraints to validate coords -var coordsValidator = { +const coordsValidator = { rect: 4, ellipse: 4, circle: 3, @@ -42,7 +42,7 @@ var coordsValidator = { }; //transform the coords from the QTI system to Raphael system -var qti2raphCoordsMapper = { +const qti2raphCoordsMapper = { /** * Rectangle coordinate mapper: from left-x,top-y,right-x-bottom-y to x,y,w,h * @param {Array} coords - QTI coords @@ -67,8 +67,8 @@ var qti2raphCoordsMapper = { * @returns {Array} path desc */ poly: function(coords) { - var a; - var size = coords.length; + let a; + const size = coords.length; // autoClose if needed if (coords[0] !== coords[size - 2] && coords[1] !== coords[size - 1]) { @@ -90,7 +90,7 @@ var qti2raphCoordsMapper = { }; //transform the coords from a raphael shape to the QTI system -var raph2qtiCoordsMapper = { +const raph2qtiCoordsMapper = { /** * Rectangle coordinate mapper: from x,y,w,h to left-x,top-y,right-x-bottom-y * @param {Object} attr - Raphael Element's attributes @@ -133,8 +133,8 @@ var raph2qtiCoordsMapper = { * @returns {Array} raphael coords */ path: function(attr) { - var poly = []; - var i; + const poly = []; + let i; if (_.isArray(attr.path)) { for (i = 1; i < attr.path.length; i++) { @@ -153,7 +153,7 @@ var raph2qtiCoordsMapper = { * Graphic interaction helper * @exports qtiCommonRenderer/helpers/Graphic */ -var GraphicHelper = { +const GraphicHelper = { /** * Raw access to the styles * @type {Object} @@ -178,6 +178,7 @@ var GraphicHelper = { * @param {Object} options - the paper parameters * @param {String} options.img - the url of the background image * @param {jQueryElement} [options.container] - the parent of the paper element (got the closest parent by default) + * @param {Boolean} [options.responsive] - scale to container * @param {Number} [options.width] - the paper width * @param {Number} [options.height] - the paper height * @param {String} [options.imgId] - an identifier for the image element @@ -185,19 +186,17 @@ var GraphicHelper = { * @returns {Raphael.Paper} the paper */ responsivePaper: function(id, serial, options) { - var paper, image; + const $container = options.container || $('#' + id).parent(); + const $editor = $('.image-editor', $container); + const $body = $container.closest('.qti-itemBody'); + const resizer = _.throttle(resizePaper, 10); - var $container = options.container || $('#' + id).parent(); - var $editor = $('.image-editor', $container); - var $body = $container.closest('.qti-itemBody'); - var resizer = _.throttle(resizePaper, 10); + const imgWidth = options.width || $container.innerWidth(); + const imgHeight = options.height || $container.innerHeight(); - var imgWidth = options.width || $container.innerWidth(); - var imgHeight = options.height || $container.innerHeight(); - - paper = scaleRaphael(id, imgWidth, imgHeight); - image = paper.image(options.img, 0, 0, imgWidth, imgHeight); + const paper = scaleRaphael(id, imgWidth, imgHeight); + const image = paper.image(options.img, 0, 0, imgWidth, imgHeight); image.id = options.imgId || image.id; paper.setViewBox(0, 0, imgWidth, imgHeight); @@ -226,24 +225,39 @@ var GraphicHelper = { * @private */ function resizePaper(e, givenWidth) { - var maxWidth, containerWidth, containerHeight, factor; + let containerWidth; if (e) { e.stopPropagation(); } - maxWidth = $body.width(); - containerWidth = $editor.innerWidth(); + const diff = $editor.outerWidth() - $editor.width() + ($container.outerWidth() - $container.width()) + 1; + const maxWidth = $body.width(); + if (options.responsive) { + containerWidth = $container.innerWidth(); + } else { + containerWidth = $editor.innerWidth(); + } - if (givenWidth > 0 || containerWidth > maxWidth) { - if (givenWidth > 0 && givenWidth < maxWidth) { - containerWidth = givenWidth; - } else if (containerWidth > maxWidth) { - containerWidth = maxWidth; + if (options.responsive && containerWidth > 0 || givenWidth > 0 || containerWidth > maxWidth) { + if (options.responsive) { + if (givenWidth < containerWidth && givenWidth < maxWidth) { + containerWidth = givenWidth - diff; + } else if (containerWidth > maxWidth) { + containerWidth = maxWidth - diff; + } else { + containerWidth -= diff; + } + } else { + if (givenWidth > 0 && givenWidth < maxWidth) { + containerWidth = givenWidth; + } else if (containerWidth > maxWidth) { + containerWidth = maxWidth; + } } - factor = containerWidth / imgWidth; - containerHeight = imgHeight * factor; + const factor = containerWidth / imgWidth; + const containerHeight = imgHeight * factor; if (containerWidth > 0) { paper.changeSize(containerWidth, containerHeight, false, false); @@ -274,10 +288,10 @@ var GraphicHelper = { * @returns {Raphael.Element} the created element */ createElement: function(paper, type, coords, options) { - var self = this; - var element; - var shaper = shapeMap[type] ? paper[shapeMap[type]] : paper[type]; - var shapeCoords = options.qtiCoords !== false ? self.raphaelCoords(paper, type, coords) : coords; + const self = this; + let element; + const shaper = shapeMap[type] ? paper[shapeMap[type]] : paper[type]; + const shapeCoords = options.qtiCoords !== false ? self.raphaelCoords(paper, type, coords) : coords; if (typeof shaper === 'function') { element = shaper.apply(paper, shapeCoords); @@ -339,23 +353,20 @@ var GraphicHelper = { * @param {Function} [options.remove] - call once removed */ createTarget: function createTarget(paper, options) { - var baseSize, count, factor, half, hover, layer, point, self, tBBox, targetSize, x, y, target; - options = options || {}; - self = this; - point = options.point || { x: 0, y: 0 }; - factor = paper.w !== 0 ? paper.width / paper.w : 1; - hover = typeof options.hover === 'undefined' ? true : !!options.hover; + const point = options.point || { x: 0, y: 0 }; + const factor = paper.w !== 0 ? paper.width / paper.w : 1; + const hover = typeof options.hover === 'undefined' ? true : !!options.hover; - baseSize = 18; // this is the base size of the path element to be placed on svg (i.e. the path element crosshair is created to have a size of 18) - half = baseSize / 2; - x = point.x - half; - y = point.y - half; - targetSize = factor !== 0 ? 2 / factor : 2; + const baseSize = 18; // this is the base size of the path element to be placed on svg (i.e. the path element crosshair is created to have a size of 18) + const half = baseSize / 2; + const x = point.x - half; + const y = point.y - half; + const targetSize = factor !== 0 ? 2 / factor : 2; //create the target from a path - target = paper + const target = paper .path(gstyle.target.path) .transform('t' + x + ',' + y + 's' + targetSize) .attr(gstyle.target) @@ -365,7 +376,7 @@ var GraphicHelper = { if (options.id) { target.id = options.id; } else { - count = 0; + let count = 0; paper.forEach(function(element) { if (element.data('target')) { count++; @@ -374,15 +385,15 @@ var GraphicHelper = { target.id = 'target-' + count; } - tBBox = target.getBBox(); + const tBBox = target.getBBox(); //create an invisible rect over the target to ensure path selection - layer = paper + const layer = paper .rect(tBBox.x, tBBox.y, tBBox.width, tBBox.height) .attr(gstyle.layer) .click(function() { - var id = target.id; - var p = this.data('point'); + const id = target.id; + const p = this.data('point'); if (_.isFunction(options.select)) { options.select(target, p, this); @@ -397,14 +408,14 @@ var GraphicHelper = { if (hover) { layer.hover( - function() { + () => { if (!target.flashing) { - self.setStyle(target, 'target-hover'); + this.setStyle(target, 'target-hover'); } }, - function() { + () => { if (!target.flashing) { - self.setStyle(target, 'target-success'); + this.setStyle(target, 'target-success'); } } ); @@ -429,7 +440,7 @@ var GraphicHelper = { * @returns {Array} the arguments array of coordinate to give to the approriate raphael shapre creator */ raphaelCoords: function raphaelCoords(paper, type, coords) { - var shapeCoords; + let shapeCoords; if (_.isString(coords)) { coords = _.map(coords.split(','), function(coord) { @@ -467,9 +478,9 @@ var GraphicHelper = { * @returns {String} the QTI coords */ qtiCoords: function qtiCoords(element, paper, width) { - var mapper = raph2qtiCoordsMapper[element.type]; - var result = ''; - var factor = paper && width ? width / paper.w : 1; + const mapper = raph2qtiCoordsMapper[element.type]; + let result = ''; + const factor = paper && width ? width / paper.w : 1; if (_.isFunction(mapper)) { result = _.map(mapper.call(raph2qtiCoordsMapper, element.attr()), function(coord) { @@ -487,8 +498,8 @@ var GraphicHelper = { * @param {Raphael.Element} element - used to get the bbox from */ createTouchCircle: function(paper, bbox) { - var radius = bbox.width > bbox.height ? bbox.width : bbox.height; - var tCircle = paper.circle(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2, radius); + const radius = bbox.width > bbox.height ? bbox.width : bbox.height; + const tCircle = paper.circle(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2, radius); tCircle.attr(gstyle['touch-circle']); @@ -515,20 +526,19 @@ var GraphicHelper = { * @returns {Raphael.Element} the created text */ createText: function(paper, options) { - var fontSize, scaledFontSize, text; - var top = options.top || 0; - var left = options.left || 0; - var content = options.content || ''; - var style = options.style || 'small-text'; - var title = options.title || ''; - var disableEvents = options.disableEvents || false; - var factor = 1; + const top = options.top || 0; + const left = options.left || 0; + const content = options.content || ''; + const style = options.style || 'small-text'; + const title = options.title || ''; + const disableEvents = options.disableEvents || false; + let factor = 1; if (paper.width && paper.w) { factor = paper.width / paper.w; } - text = paper.text(left, top, content).toFront(); + const text = paper.text(left, top, content).toFront(); if (options.id) { text.id = options.id; } @@ -544,8 +554,8 @@ var GraphicHelper = { } if (typeof factor !== 'undefined' && factor !== 1) { - fontSize = parseInt(text.attr('font-size'), 10); - scaledFontSize = Math.floor(fontSize / factor) + 1; + const fontSize = parseInt(text.attr('font-size'), 10); + const scaledFontSize = Math.floor(fontSize / factor) + 1; text.attr('font-size', scaledFontSize); } @@ -572,10 +582,9 @@ var GraphicHelper = { * @returns {Raphael.Element} the created text */ createShapeText: function(paper, shape, options) { - var self = this; - var bbox = shape.getBBox(); + const bbox = shape.getBBox(); - var text = this.createText( + const text = this.createText( paper, _.merge( { @@ -587,8 +596,8 @@ var GraphicHelper = { ); if (options.shapeClick) { - text.click(function() { - self.trigger(shape, 'click'); + text.click(() => { + this.trigger(shape, 'click'); }); } @@ -611,28 +620,28 @@ var GraphicHelper = { * @returns {Raphael.Element} the created set, augmented of a move(x,y) method */ createBorderedImage: function(paper, options) { - var padding = options.padding >= 0 ? options.padding : 6; - var halfPad = padding / 2; + const padding = options.padding >= 0 ? options.padding : 6; + const halfPad = padding / 2; - var rx = options.left, + const rx = options.left, ry = options.top, rw = options.width + padding, rh = options.height + padding; - var ix = options.left + halfPad, + const ix = options.left + halfPad, iy = options.top + halfPad, iw = options.width, ih = options.height; - var set = paper.set(); + const set = paper.set(); //create a rectangle with a padding and a border. - var rect = paper + const rect = paper .rect(rx, ry, rw, rh) .attr(options.border ? gstyle['imageset-rect-stroke'] : gstyle['imageset-rect-no-stroke']); //and an image centered into the rectangle. - var image = paper.image(options.url, ix, iy, iw, ih).attr(gstyle['imageset-img']); + const image = paper.image(options.url, ix, iy, iw, ih).attr(gstyle['imageset-img']); if (options.shadow) { set.push( @@ -655,8 +664,8 @@ var GraphicHelper = { * @returns {Raphael.Element} the set for chaining */ set.move = function move(x, y, duration) { - var animation = raphael.animation({ x: x, y: y }, duration || 400); - var elt = rect.animate(animation); + const animation = raphael.animation({ x: x, y: y }, duration || 400); + const elt = rect.animate(animation); image.animateWith(elt, animation, { x: x + halfPad, y: y + halfPad }, duration || 400); return set; }; @@ -710,12 +719,11 @@ var GraphicHelper = { * @param {String} [restorState = 'basic'] - the state to restore the elt into after flash */ highlightError: function(element, restoredState) { - var self = this; if (element) { element.flashing = true; - self.updateElementState(element, 'error'); - _.delay(function() { - self.updateElementState(element, restoredState || 'basic'); + this.updateElementState(element, 'error'); + _.delay(() => { + this.updateElementState(element, restoredState || 'basic'); element.flashing = false; }, 800); } @@ -728,7 +736,7 @@ var GraphicHelper = { * */ trigger: function(element, event) { - var evt = _.where(element.events, { name: event }); + const evt = _.where(element.events, { name: event }); if (evt.length && evt[0] && typeof evt[0].f === 'function') { evt[0].f.apply(element, Array.prototype.slice.call(arguments, 2)); } @@ -743,9 +751,9 @@ var GraphicHelper = { * @returns {Object} x,y point */ getPoint: function getPoint(event, paper, $container) { - var point = this.clickPoint($container, event); - var rect = $container.get(0).getBoundingClientRect(); - var factor = paper.w / rect.width; + const point = this.clickPoint($container, event); + const rect = $container.get(0).getBoundingClientRect(); + const factor = paper.w / rect.width; point.x = Math.round(point.x * factor); point.y = Math.round(point.y * factor); @@ -760,10 +768,10 @@ var GraphicHelper = { * @returns {Object} position with top and left */ position: function($container, paper) { - var pw = parseInt(paper.w || paper.width, 10); - var cw = parseInt($container.width(), 10); - var ph = parseInt(paper.w || paper.width, 10); - var ch = parseInt($container.height(), 10); + const pw = parseInt(paper.w || paper.width, 10); + const cw = parseInt($container.width(), 10); + const ph = parseInt(paper.w || paper.width, 10); + const ch = parseInt($container.height(), 10); return { left: (cw - pw) / 2, @@ -778,8 +786,8 @@ var GraphicHelper = { * @returns {Object} the x,y point */ clickPoint: function($container, event) { - var x, y; - var offset = $container.offset(); + let x, y; + const offset = $container.offset(); if (event.pageX || event.pageY) { x = event.pageX - offset.left; @@ -789,7 +797,7 @@ var GraphicHelper = { y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop - offset.top; } - return { x: x, y: y }; + return { x, y }; } }; diff --git a/src/qtiCommonRenderer/renderers/interactions/GraphicAssociateInteraction.js b/src/qtiCommonRenderer/renderers/interactions/GraphicAssociateInteraction.js index 5e991271..3764f598 100644 --- a/src/qtiCommonRenderer/renderers/interactions/GraphicAssociateInteraction.js +++ b/src/qtiCommonRenderer/renderers/interactions/GraphicAssociateInteraction.js @@ -13,7 +13,7 @@ * 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 (original work) Open Assessment Technlogies SA (under the project TAO-PRODUCT); + * Copyright (c) 2014-2023 (original work) Open Assessment Technlogies SA (under the project TAO-PRODUCT); * */ @@ -29,150 +29,16 @@ import pciResponse from 'taoQtiItem/qtiCommonRenderer/helpers/PciResponse'; import containerHelper from 'taoQtiItem/qtiCommonRenderer/helpers/container'; import instructionMgr from 'taoQtiItem/qtiCommonRenderer/helpers/instructions/instructionManager'; -/** - * 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#element10321 - * - * @param {object} interaction - * @returns {Promise} - */ -var render = function render(interaction) { - var self = this; - - return new Promise(function(resolve) { - var $container = containerHelper.get(interaction); - var background = interaction.object.attributes; - interaction._vsets = []; - - $container.off('resized.qti-widget.resolve').one('resized.qti-widget.resolve', resolve); - - interaction.paper = graphic.responsivePaper('graphic-paper-' + interaction.serial, interaction.serial, { - width: background.width, - height: background.height, - img: self.resolveUrl(background.data), - imgId: 'bg-image-' + interaction.serial, - container: $container - }); - - //call render choice for each interaction's choices - _.forEach(interaction.getChoices(), _.partial(_renderChoice, interaction)); - - //make the paper clear the selection by clicking it - _paperUnSelect(interaction); - - //set up the constraints instructions - instructionMgr.minMaxChoiceInstructions(interaction, { - min: interaction.attr('minAssociations'), - max: interaction.attr('maxAssociations'), - getResponse: _getRawResponse, - onError: function(data) { - if (data && data.target) { - graphic.highlightError(data.target); - } - } - }); - }); -}; - -/** - * Render a choice inside the paper. - * Please note that the choice renderer isn't implemented separately because it relies on the Raphael paper instead of the DOM. - * @param {Paper} paper - the raphael paper to add the choices to - * @param {Object} interaction - * @param {Object} choice - the hotspot choice to add to the interaction - */ -var _renderChoice = function _renderChoice(interaction, choice) { - var shape = choice.attr('shape'); - var coords = choice.attr('coords'); - var maxAssociations = interaction.attr('maxAssociations'); - - var rElement = graphic - .createElement(interaction.paper, shape, coords, { - id: choice.serial, - title: __('Select this area to start an association') - }) - .data('max', choice.attr('matchMax')) - .data('matching', 0) - .removeData('assocs') - .click(function() { - var self = this; - var active, assocs; - - //can't create more associations than the maxAssociations attr - if (maxAssociations > 0 && _getRawResponse(interaction).length >= maxAssociations) { - _shapesUnSelectable(interaction); - instructionMgr.validateInstructions(interaction, { choice: choice, target: this }); - return; - } - active = _getActiveElement(interaction); - if (this.selectable) { - if (active) { - //increment the matching counter - active.data('matching', active.data('matching') + 1); - this.data('matching', this.data('matching') + 1); - - //attach the response to the active (not the dest) - assocs = active.data('assocs') || []; - assocs.push(choice.id()); - active.data('assocs', assocs); - - //and create the path - _createPath(interaction, active, this, function onRemove() { - //decrement the matching counter - active.data('matching', active.data('matching') - 1); - self.data('matching', self.data('matching') - 1); - - //detach the response from the active - active.data('assocs', _.pull(active.data('assocs') || [], choice.id())); - - containerHelper.triggerResponseChangeEvent(interaction); - instructionMgr.validateInstructions(interaction, { choice: choice, target: self }); - }); - } - _shapesUnSelectable(interaction); - } else if (this.active) { - _shapesUnSelectable(interaction); - } else if (_isMatchable(this)) { - if (active) { - _shapesUnSelectable(interaction); - } - graphic.updateElementState(this, 'active', __('Select this area to start an association')); - this.active = true; - _shapesSelectable(interaction, this); - } - - containerHelper.triggerResponseChangeEvent(interaction); - instructionMgr.validateInstructions(interaction, { choice: choice, target: this }); - }); -}; - -/** - * By clicking the paper image the shapes are restored to their default state - * @private - * @param {Object} interaction - */ -var _paperUnSelect = function _paperUnSelect(interaction) { - var $container = containerHelper.get(interaction); - var image = interaction.paper.getById('bg-image-' + interaction.serial); - if (image) { - image.click(function() { - _shapesUnSelectable(interaction); - $container.trigger('unselect.graphicassociate'); - }); - } -}; - /** * Get the element that has the active state * @private * @param {Object} interaction * @returns {Raphael.Element} the active element */ -var _getActiveElement = function _getActiveElement(interaction) { - var active; - _.forEach(interaction.getChoices(), function(choice) { - var element = interaction.paper.getById(choice.serial); +const _getActiveElement = function _getActiveElement(interaction) { + let active; + _.forEach(interaction.getChoices(), function (choice) { + const element = interaction.paper.getById(choice.serial); if (element && element.active === true) { active = element; return false; @@ -190,46 +56,43 @@ var _getActiveElement = function _getActiveElement(interaction) { * @param {Raphael.Element} destElement - the path ends to this shape * @param {Function} onRemove - called back on path remove */ -var _createPath = function _createPath(interaction, srcElement, destElement, onRemove) { - var $container = containerHelper.get(interaction); +const _createPath = function _createPath(interaction, srcElement, destElement, onRemove) { + const $container = containerHelper.get(interaction); //virtual set, not a raphael one, just to group the elements - var vset = []; + let vset = []; //get the middle point of the source shape - var src = srcElement.getBBox(); - var sx = src.x + src.width / 2; - var sy = src.y + src.height / 2; + const src = srcElement.getBBox(); + const sx = src.x + src.width / 2; + const sy = src.y + src.height / 2; //get the middle point of the source shape - var dest = destElement.getBBox(); - var dx = dest.x + dest.width / 2; - var dy = dest.y + dest.height / 2; + const dest = destElement.getBBox(); + const dx = dest.x + dest.width / 2; + const dy = dest.y + dest.height / 2; //create a path with bullets at the beginning and the end - var srcBullet = interaction.paper.circle(sx, sy, 3).attr(graphic._style['assoc-bullet']); + const srcBullet = interaction.paper.circle(sx, sy, 3).attr(graphic._style['assoc-bullet']); - var destBullet = interaction.paper.circle(dx, dy, 3).attr(graphic._style['assoc-bullet']); + const destBullet = interaction.paper.circle(dx, dy, 3).attr(graphic._style['assoc-bullet']); - var path = interaction.paper + const path = interaction.paper .path('M' + sx + ',' + sy + 'L' + sx + ',' + sy) .attr(graphic._style.assoc) .animate({ path: 'M' + sx + ',' + sy + 'L' + dx + ',' + dy }, 300); //create an overall layer that make easier the path selection - var layer = interaction.paper.path('M' + sx + ',' + sy + 'L' + dx + ',' + dy).attr(graphic._style['assoc-layer']); + const layer = interaction.paper.path('M' + sx + ',' + sy + 'L' + dx + ',' + dy).attr(graphic._style['assoc-layer']); //get the middle of the path - var midPath = layer.getPointAtLength(layer.getTotalLength() / 2); + const midPath = layer.getPointAtLength(layer.getTotalLength() / 2); //create an hidden background for the closer - var closerBg = interaction.paper - .circle(midPath.x, midPath.y, 9) - .attr(graphic._style['close-bg']) - .toBack(); + const closerBg = interaction.paper.circle(midPath.x, midPath.y, 9).attr(graphic._style['close-bg']).toBack(); //create an hidden closer - var closer = interaction.paper + const closer = interaction.paper .path(graphic._style.close.path) .attr(graphic._style.close) .transform('T' + (midPath.x - 9) + ',' + (midPath.y - 9)) @@ -256,30 +119,24 @@ var _createPath = function _createPath(interaction, srcElement, destElement, onR } }); - $container.on('unselect.graphicassociate', function() { + $container.on('unselect.graphicassociate', function () { hideCloser(); }); function showCloser() { - closerBg - .toFront() - .animate({ opacity: 0.8 }, 300) - .click(removeSet); - closer - .toFront() - .animate({ opacity: 1 }, 300) - .click(removeSet); + closerBg.toFront().animate({ opacity: 0.8 }, 300).click(removeSet); + closer.toFront().animate({ opacity: 1 }, 300).click(removeSet); } function hideCloser() { if (closerBg && closerBg.type) { closerBg - .animate({ opacity: 0 }, 300, function() { + .animate({ opacity: 0 }, 300, function () { closerBg.toBack(); }) .unclick(); closer - .animate({ opacity: 0 }, 300, function() { + .animate({ opacity: 0 }, 300, function () { closer.toBack(); }) .unclick(); @@ -295,30 +152,43 @@ var _createPath = function _createPath(interaction, srcElement, destElement, onR } } }; - +/** + * Check if a shape can accept matches + * @private + * @param {Raphael.Element} element - the shape + * @returns {Boolean} true if the element is matchable + */ +const _isMatchable = function (element) { + let matchable = false; + if (element) { + const matchMax = element.data('max') || 0; + const matching = element.data('matching') || 0; + matchable = matchMax === 0 || matchMax > matching; + } + return matchable; +}; /** * Makes the shapes selectable * @private * @param {Object} interaction * @param {Raphael.Element} active - the active shape */ -var _shapesSelectable = function _shapesSelectable(interaction, active) { - var assocs = active.data('assocs') || []; - var choices = interaction.getChoices(); - var activeChoice = choices[active.id]; +const _shapesSelectable = function _shapesSelectable(interaction, active) { + const assocs = active.data('assocs') || []; + const choices = interaction.getChoices(); + const activeChoice = choices[active.id]; //update the shape state - _.forEach(choices, function(choice) { - var element; - var assocsElement; + _.forEach(choices, function (choice) { if (!_.contains(assocs, choice.id())) { - element = interaction.paper.getById(choice.serial); - assocsElement = element.data('assocs') || []; + const element = interaction.paper.getById(choice.serial); + const assocsElement = element.data('assocs') || []; if ( !element.active && element.id !== active.id && _isMatchable(element, active) && - !_.contains(assocsElement, activeChoice.id())) { + !_.contains(assocsElement, activeChoice.id()) + ) { element.selectable = true; graphic.updateElementState(element, 'selectable'); } @@ -331,9 +201,9 @@ var _shapesSelectable = function _shapesSelectable(interaction, active) { * @private * @param {Object} interaction */ -var _shapesUnSelectable = function _shapesUnSelectable(interaction) { - _.forEach(interaction.getChoices(), function(choice) { - var element = interaction.paper.getById(choice.serial); +const _shapesUnSelectable = function _shapesUnSelectable(interaction) { + _.forEach(interaction.getChoices(), function (choice) { + const element = interaction.paper.getById(choice.serial); if (element) { element.selectable = false; element.active = false; @@ -342,37 +212,20 @@ var _shapesUnSelectable = function _shapesUnSelectable(interaction) { }); }; -/** - * Check if a shape can accept matches - * @private - * @param {Raphael.Element} element - the shape - * @returns {Boolean} true if the element is matchable - */ -var _isMatchable = function(element) { - var matchable = false; - var matching, matchMax; - if (element) { - matchMax = element.data('max') || 0; - matching = element.data('matching') || 0; - matchable = matchMax === 0 || matchMax > matching; - } - return matchable; -}; - /** * Get the response from the interaction * @private * @param {Object} interaction * @returns {Array} the response in raw format */ -var _getRawResponse = function _getRawResponse(interaction) { - var responses = []; - _.forEach(interaction.getChoices(), function(choice) { - var element = interaction.paper.getById(choice.serial); - var assocs = element.data('assocs'); +const _getRawResponse = function _getRawResponse(interaction) { + let responses = []; + _.forEach(interaction.getChoices(), function (choice) { + const element = interaction.paper.getById(choice.serial); + const assocs = element.data('assocs'); if (element && assocs) { responses = responses.concat( - _.map(assocs, function(id) { + _.map(assocs, function (id) { return [choice.id(), id]; }) ); @@ -380,6 +233,135 @@ var _getRawResponse = function _getRawResponse(interaction) { }); return responses; }; +/** + * By clicking the paper image the shapes are restored to their default state + * @private + * @param {Object} interaction + */ +const _paperUnSelect = function _paperUnSelect(interaction) { + const $container = containerHelper.get(interaction); + const image = interaction.paper.getById('bg-image-' + interaction.serial); + if (image) { + image.click(function () { + _shapesUnSelectable(interaction); + $container.trigger('unselect.graphicassociate'); + }); + } +}; +/** + * Render a choice inside the paper. + * Please note that the choice renderer isn't implemented separately because it relies on the Raphael paper instead of the DOM. + * @param {Paper} paper - the raphael paper to add the choices to + * @param {Object} interaction + * @param {Object} choice - the hotspot choice to add to the interaction + */ +const _renderChoice = function _renderChoice(interaction, choice) { + const shape = choice.attr('shape'); + const coords = choice.attr('coords'); + const maxAssociations = interaction.attr('maxAssociations'); + + graphic + .createElement(interaction.paper, shape, coords, { + id: choice.serial, + title: __('Select this area to start an association') + }) + .data('max', choice.attr('matchMax')) + .data('matching', 0) + .removeData('assocs') + .click(function () { + //can't create more associations than the maxAssociations attr + if (maxAssociations > 0 && _getRawResponse(interaction).length >= maxAssociations) { + _shapesUnSelectable(interaction); + instructionMgr.validateInstructions(interaction, { choice: choice, target: this }); + return; + } + const active = _getActiveElement(interaction); + if (this.selectable) { + if (active) { + //increment the matching counter + active.data('matching', active.data('matching') + 1); + this.data('matching', this.data('matching') + 1); + + //attach the response to the active (not the dest) + const assocs = active.data('assocs') || []; + assocs.push(choice.id()); + active.data('assocs', assocs); + + //and create the path + _createPath(interaction, active, this, () => { + //decrement the matching counter + active.data('matching', active.data('matching') - 1); + this.data('matching', this.data('matching') - 1); + + //detach the response from the active + active.data('assocs', _.pull(active.data('assocs') || [], choice.id())); + + containerHelper.triggerResponseChangeEvent(interaction); + instructionMgr.validateInstructions(interaction, { choice: choice, target: this }); + }); + } + _shapesUnSelectable(interaction); + } else if (this.active) { + _shapesUnSelectable(interaction); + } else if (_isMatchable(this)) { + if (active) { + _shapesUnSelectable(interaction); + } + graphic.updateElementState(this, 'active', __('Select this area to start an association')); + this.active = true; + _shapesSelectable(interaction, this); + } + + containerHelper.triggerResponseChangeEvent(interaction); + instructionMgr.validateInstructions(interaction, { choice: choice, target: this }); + }); +}; +/** + * 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#element10321 + * + * @param {object} interaction + * @returns {Promise} + */ +const render = function render(interaction) { + const self = this; + + return new Promise(function (resolve) { + const $container = containerHelper.get(interaction); + const background = interaction.object.attributes; + interaction._vsets = []; + + $container.off('resized.qti-widget.resolve').one('resized.qti-widget.resolve', resolve); + + interaction.paper = graphic.responsivePaper('graphic-paper-' + interaction.serial, interaction.serial, { + width: background.width, + height: background.height, + img: self.resolveUrl(background.data), + imgId: 'bg-image-' + interaction.serial, + container: $container, + responsive: $container.hasClass('responsive') + }); + + //call render choice for each interaction's choices + _.forEach(interaction.getChoices(), _.partial(_renderChoice, interaction)); + + //make the paper clear the selection by clicking it + _paperUnSelect(interaction); + + //set up the constraints instructions + instructionMgr.minMaxChoiceInstructions(interaction, { + min: interaction.attr('minAssociations'), + max: interaction.attr('maxAssociations'), + getResponse: _getRawResponse, + onError: function (data) { + if (data && data.target) { + graphic.highlightError(data.target); + } + } + }); + }); +}; /** * Set the response to the rendered interaction. @@ -395,27 +377,27 @@ var _getRawResponse = function _getRawResponse(interaction) { * @param {object} interaction * @param {object} response */ -var setResponse = function(interaction, response) { - var responseValues; - var map; +const setResponse = function (interaction, response) { + let responseValues; if (response && interaction.paper) { try { responseValues = pciResponse.unserialize(response, interaction); if (responseValues.length === 2 && !Array.isArray(responseValues[0]) && !Array.isArray(responseValues[1])) { responseValues = [responseValues]; } - } catch (e) {} + } catch (e) { + console.error(e); + } if (_.isArray(responseValues)) { //create an object with choiceId => shapeElement - map = _.transform(interaction.getChoices(), function(res, choice) { + const map = _.transform(interaction.getChoices(), function (res, choice) { res[choice.id()] = interaction.paper.getById(choice.serial); }); - _.forEach(responseValues, function(responseValue) { - var el1, el2; + _.forEach(responseValues, function (responseValue) { if (_.isArray(responseValue) && responseValue.length === 2) { - el1 = map[responseValue[0]]; - el2 = map[responseValue[1]]; + const el1 = map[responseValue[0]]; + const el2 = map[responseValue[1]]; if (el1 && el2) { graphic.trigger(el1, 'click'); graphic.trigger(el2, 'click'); @@ -440,12 +422,12 @@ var setResponse = function(interaction, response) { * @param {object} interaction * @param {object} response */ -var resetResponse = function resetResponse(interaction) { - var toRemove = []; +const resetResponse = function resetResponse(interaction) { + const toRemove = []; //reset response and state bound to shapes - _.forEach(interaction.getChoices(), function(choice) { - var element = interaction.paper.getById(choice.serial); + _.forEach(interaction.getChoices(), function (choice) { + const element = interaction.paper.getById(choice.serial); if (element) { element.data({ max: choice.attr('matchMax'), @@ -457,7 +439,7 @@ var resetResponse = function resetResponse(interaction) { if (interaction && interaction.paper) { //remove the paths, but outside the forEach as it is implemented as a linked list - interaction.paper.forEach(function(elt) { + interaction.paper.forEach(function (elt) { if (elt.data('assoc-path')) { toRemove.push(elt); } @@ -478,9 +460,9 @@ var resetResponse = function resetResponse(interaction) { * @param {object} interaction * @returns {object} */ -var getResponse = function(interaction) { - var raw = _getRawResponse(interaction); - var response = pciResponse.serialize(raw, interaction); +const getResponse = function (interaction) { + const raw = _getRawResponse(interaction); + const response = pciResponse.serialize(raw, interaction); return response; }; @@ -488,10 +470,9 @@ var getResponse = function(interaction) { * Clean interaction destroy * @param {Object} interaction */ -var destroy = function destroy(interaction) { - var $container; +const destroy = function destroy(interaction) { if (interaction.paper) { - $container = containerHelper.get(interaction); + const $container = containerHelper.get(interaction); $(window).off('resize.qti-widget.' + interaction.serial); $container.off('resize.qti-widget.' + interaction.serial); @@ -501,9 +482,7 @@ var destroy = function destroy(interaction) { $container.off('.graphicassociate'); - $('.main-image-box', $container) - .empty() - .removeAttr('style'); + $('.main-image-box', $container).empty().removeAttr('style'); $('.image-editor', $container).removeAttr('style'); $('ul', $container).empty(); } @@ -518,7 +497,7 @@ var destroy = function destroy(interaction) { * @param {Object} interaction - the interaction instance * @param {Object} state - the interaction state */ -var setState = function setState(interaction, state) { +const setState = function setState(interaction, state) { if (_.isObject(state)) { if (state.response) { interaction.resetResponse(); @@ -533,9 +512,9 @@ var setState = function setState(interaction, state) { * @param {Object} interaction - the interaction instance * @returns {Object} the interaction current state */ -var getState = function getState(interaction) { - var state = {}; - var response = interaction.getResponse(); +const getState = function getState(interaction) { + const state = {}; + const response = interaction.getResponse(); if (response) { state.response = response; diff --git a/src/qtiCommonRenderer/renderers/interactions/GraphicGapMatchInteraction.js b/src/qtiCommonRenderer/renderers/interactions/GraphicGapMatchInteraction.js index a7db6d10..f5f87472 100644 --- a/src/qtiCommonRenderer/renderers/interactions/GraphicGapMatchInteraction.js +++ b/src/qtiCommonRenderer/renderers/interactions/GraphicGapMatchInteraction.js @@ -13,7 +13,7 @@ * 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); + * Copyright (c) 2014-2023 (original work) Open Assessment Technlogies SA (under the project TAO-PRODUCT); * */ @@ -33,18 +33,18 @@ import instructionMgr from 'taoQtiItem/qtiCommonRenderer/helpers/instructions/in import interact from 'interact'; import interactUtils from 'ui/interactUtils'; -var isDragAndDropEnabled; +let isDragAndDropEnabled; // this represents the state for the active droppable zone // we need it only to access the active dropzone in the iFrameFix // should be removed when the old test runner is discarded -var activeDrop = null; +let activeDrop = null; /** * Global variable to count number of choice usages: * @type {object} */ -var _choiceUsages = {}; +const _choiceUsages = {}; /** * This options enables to support old items created with the wrong @@ -52,7 +52,7 @@ var _choiceUsages = {}; * * @deprecated */ -var isDirectedPairFlipped = module.config().flipDirectedPair; +const isDirectedPairFlipped = module.config().flipDirectedPair; /** * Check if a shape can accept matches @@ -60,12 +60,11 @@ var isDirectedPairFlipped = module.config().flipDirectedPair; * @param {Raphael.Element} element - the shape * @returns {Boolean} true if the element is matchable */ -var _isMatchable = function(element) { - var matchable = false; - var matching, matchMax; +const _isMatchable = function (element) { + let matchable = false; if (element) { - matchMax = element.data('max') || 0; - matching = element.data('matching') || []; + const matchMax = element.data('max') || 0; + const matching = element.data('matching') || []; matchable = matchMax === 0 || matchMax > matching.length; } return matchable; @@ -76,12 +75,12 @@ var _isMatchable = function(element) { * @private * @param {Object} interaction */ -var _shapesSelectable = function _shapesSelectable(interaction) { - var tooltip = __('Select the area to add an image'); +const _shapesSelectable = function _shapesSelectable(interaction) { + const tooltip = __('Select the area to add an image'); //update the shape state - _.forEach(interaction.getChoices(), function(choice) { - var element = interaction.paper.getById(choice.serial); + _.forEach(interaction.getChoices(), function (choice) { + const element = interaction.paper.getById(choice.serial); if (_isMatchable(element)) { element.selectable = true; graphic.setStyle(element, 'selectable'); @@ -90,8 +89,8 @@ var _shapesSelectable = function _shapesSelectable(interaction) { }); //update the gap images tooltip - _.forEach(interaction.gapFillers, function(gapFiller) { - gapFiller.forEach(function(element) { + _.forEach(interaction.gapFillers, function (gapFiller) { + gapFiller.forEach(function (element) { graphic.updateTitle(element, tooltip); }); }); @@ -102,9 +101,9 @@ var _shapesSelectable = function _shapesSelectable(interaction) { * @private * @param {Object} interaction */ -var _shapesUnSelectable = function _shapesUnSelectable(interaction) { - _.forEach(interaction.getChoices(), function(choice) { - var element = interaction.paper.getById(choice.serial); +const _shapesUnSelectable = function _shapesUnSelectable(interaction) { + _.forEach(interaction.getChoices(), function (choice) { + const element = interaction.paper.getById(choice.serial); if (element) { element.selectable = false; graphic.setStyle(element, 'basic'); @@ -113,8 +112,8 @@ var _shapesUnSelectable = function _shapesUnSelectable(interaction) { }); //update the gap images tooltip - _.forEach(interaction.gapFillers, function(gapFiller) { - gapFiller.forEach(function(element) { + _.forEach(interaction.gapFillers, function (gapFiller) { + gapFiller.forEach(function (element) { graphic.updateTitle(element, __('Remove')); }); }); @@ -125,12 +124,12 @@ var _shapesUnSelectable = function _shapesUnSelectable(interaction) { * @private * @param {Object} interaction */ -var _paperUnSelect = function _paperUnSelect(interaction) { - var $container = containerHelper.get(interaction); - var $gapImages = $('ul > li', $container); - var bgImage = interaction.paper.getById('bg-image-' + interaction.serial); +const _paperUnSelect = function _paperUnSelect(interaction) { + const $container = containerHelper.get(interaction); + const $gapImages = $('ul > li', $container); + const bgImage = interaction.paper.getById('bg-image-' + interaction.serial); if (bgImage) { - interact(bgImage.node).on('tap', function() { + interact(bgImage.node).on('tap', function () { _shapesUnSelectable(interaction); $gapImages.removeClass('active'); }); @@ -143,11 +142,11 @@ var _paperUnSelect = function _paperUnSelect(interaction) { * @param {Object} interaction * @param {JQuery Element} $choice */ -var _setChoice = function _setChoice(interaction, $choice) { - var choiceSerial = $choice.data('serial'); - var choice = interaction.getGapImg(choiceSerial); - var matchMax; - var usages; +const _setChoice = function _setChoice(interaction, $choice) { + const choiceSerial = $choice.data('serial'); + const choice = interaction.getGapImg(choiceSerial); + let matchMax; + let usages; if (!_choiceUsages[choiceSerial]) { _choiceUsages[choiceSerial] = 0; @@ -175,8 +174,8 @@ var _setChoice = function _setChoice(interaction, $choice) { * @param {Object} interaction * @param {JQuery Element} $choice */ -var _unsetChoice = function _unsetChoice(interaction, $choice) { - var choiceSerial = $choice.data('serial'); +const _unsetChoice = function _unsetChoice(interaction, $choice) { + const choiceSerial = $choice.data('serial'); _choiceUsages[choiceSerial]--; @@ -192,15 +191,15 @@ var _unsetChoice = function _unsetChoice(interaction, $choice) { * @param {Raphael.Element} element - the selected shape * @param {Boolean} [trackResponse = true] - if the selection trigger a response chane */ -var _selectShape = function _selectShape(interaction, element, trackResponse) { - var $img, $clone, gapFiller, id, bbox, shapeOffset, activeOffset, matching, currentCount; +const _selectShape = function _selectShape(interaction, element, trackResponse) { + let $img, $clone, id, bbox, shapeOffset, activeOffset, matching, currentCount; //lookup for the active element - var $container = containerHelper.get(interaction); - var $gapList = $('ul', $container); - var $active = $gapList.find('.active:first'); - var $imageBox = $('.main-image-box', $container); - var boxOffset = $imageBox.offset(); + const $container = containerHelper.get(interaction); + const $gapList = $('ul', $container); + const $active = $gapList.find('.active:first'); + const $imageBox = $('.main-image-box', $container); + const boxOffset = $imageBox.offset(); if (typeof trackResponse === 'undefined') { trackResponse = true; @@ -244,15 +243,13 @@ var _selectShape = function _selectShape(interaction, element, trackResponse) { }, 200, function animationEnd() { - var gapFillerImage; - $clone.remove(); //extract some coords for positioning bbox = element.getBBox(); //create an image into the paper and move it to the selected shape - gapFiller = graphic + const gapFiller = graphic .createBorderedImage(interaction.paper, { url: $img.attr('src'), left: bbox.x + 8 * (currentCount - 1), @@ -266,10 +263,10 @@ var _selectShape = function _selectShape(interaction, element, trackResponse) { .data('identifier', id) .toFront(); - gapFillerImage = gapFiller[2].node; - interact(gapFillerImage).on('tap', function(e) { - var target = e.currentTarget; - var rElement = interaction.paper.getById(target.raphaelid); + const gapFillerImage = gapFiller[2].node; + interact(gapFillerImage).on('tap', function (e) { + const target = e.currentTarget; + const rElement = interaction.paper.getById(target.raphaelid); e.preventDefault(); e.stopPropagation(); @@ -313,9 +310,9 @@ var _selectShape = function _selectShape(interaction, element, trackResponse) { * @param {Object} interaction * @param {Object} choice - the hotspot choice to add to the interaction */ -var _renderChoice = function _renderChoice(interaction, choice) { +const _renderChoice = function _renderChoice(interaction, choice) { //create the shape - var rElement = graphic + const rElement = graphic .createElement(interaction.paper, choice.attr('shape'), choice.attr('coords'), { id: choice.serial, title: __('Select an image first'), @@ -331,20 +328,20 @@ var _renderChoice = function _renderChoice(interaction, choice) { if (isDragAndDropEnabled) { interact(rElement.node).dropzone({ overlap: 0.15, - ondragenter: function() { + ondragenter: function () { if (_isMatchable(rElement)) { graphic.setStyle(rElement, 'hover'); activeDrop = rElement.node; } }, - ondrop: function() { + ondrop: function () { if (_isMatchable(rElement)) { graphic.setStyle(rElement, 'selectable'); handleShapeSelect(); activeDrop = null; } }, - ondragleave: function() { + ondragleave: function () { if (_isMatchable(rElement)) { graphic.setStyle(rElement, 'selectable'); activeDrop = null; @@ -361,8 +358,8 @@ var _renderChoice = function _renderChoice(interaction, choice) { } }; -var _iFrameDragFix = function _iFrameDragFix(draggableSelector, target) { - interactUtils.iFrameDragFixOn(function() { +const _iFrameDragFix = function _iFrameDragFix(draggableSelector, target) { + interactUtils.iFrameDragFixOn(function () { if (activeDrop) { interact(activeDrop).fire({ type: 'drop', @@ -383,10 +380,10 @@ var _iFrameDragFix = function _iFrameDragFix(draggableSelector, target) { * @param {Object} interaction * @param {jQueryElement} $gapList - the list than contains the orderers */ -var _renderGapList = function _renderGapList(interaction, $gapList) { - var gapFillersSelector = $gapList.selector + ' li'; - var dragOptions; - var scaleX, scaleY; +const _renderGapList = function _renderGapList(interaction, $gapList) { + const gapFillersSelector = $gapList.selector + ' li'; + let dragOptions; + let scaleX, scaleY; interact(gapFillersSelector).on('tap', function onClickGapImg(e) { e.stopPropagation(); @@ -405,27 +402,26 @@ var _renderGapList = function _renderGapList(interaction, $gapList) { } }; - $(gapFillersSelector).each(function(index, gap) { + $(gapFillersSelector).each(function (index, gap) { interact(gap) .draggable( _.assign({}, dragOptions, { - onstart: function(e) { - var $target = $(e.target); - var scale; + onstart: function (e) { + const $target = $(e.target); _setActiveGapState($target); $target.addClass('dragged'); _iFrameDragFix(gapFillersSelector, e.target); - scale = interactUtils.calculateScale(e.target); + const scale = interactUtils.calculateScale(e.target); scaleX = scale[0]; scaleY = scale[1]; }, - onmove: function(e) { + onmove: function (e) { interactUtils.moveElement(e.target, e.dx / scaleX, e.dy / scaleY); }, - onend: function(e) { - _.defer( () => { - var $target = $(e.target); + onend: function (e) { + _.defer(() => { + const $target = $(e.target); _setInactiveGapState($target); $target.removeClass('dragged'); interactUtils.restoreOriginalPosition($target); @@ -468,13 +464,13 @@ var _renderGapList = function _renderGapList(interaction, $gapList) { * @param {object} interaction * @return {Promise} */ -var render = function render(interaction) { - var self = this; +const render = function render(interaction) { + const self = this; - return new Promise(function(resolve) { - var $container = containerHelper.get(interaction); - var $gapList = $('ul.source', $container); - var background = interaction.object.attributes; + return new Promise(function (resolve) { + const $container = containerHelper.get(interaction); + const $gapList = $('ul.source', $container); + const background = interaction.object.attributes; interaction.gapFillers = []; @@ -495,16 +491,17 @@ var render = function render(interaction) { img: self.resolveUrl(background.data), imgId: 'bg-image-' + interaction.serial, container: $container, - resize: function(newSize, factor) { + resize: function (newSize, factor) { $gapList.css('max-width', newSize + 'px'); if (factor !== 1) { - $gapList.find('img').each(function() { - var $img = $(this); + $gapList.find('img').each(function () { + const $img = $(this); $img.width($img.attr('width') * factor); $img.height($img.attr('height') * factor); }); } - } + }, + responsive: $container.hasClass('responsive') }); //call render choice for each interaction's choices @@ -524,12 +521,12 @@ var render = function render(interaction) { * @param {Object} interaction * @returns {Array} of matches */ -var _getRawResponse = function _getRawResponse(interaction) { - var pairs = []; - _.forEach(interaction.getChoices(), function(choice) { - var element = interaction.paper.getById(choice.serial); +const _getRawResponse = function _getRawResponse(interaction) { + const pairs = []; + _.forEach(interaction.getChoices(), function (choice) { + const element = interaction.paper.getById(choice.serial); if (element && _.isArray(element.data('matching'))) { - _.forEach(element.data('matching'), function(gapImg) { + _.forEach(element.data('matching'), function (gapImg) { //backward support of previous order if (isDirectedPairFlipped) { pairs.push([choice.id(), gapImg]); @@ -556,9 +553,9 @@ var _getRawResponse = function _getRawResponse(interaction) { * @param {object} interaction * @param {object} response */ -var setResponse = function(interaction, response) { - var $container = containerHelper.get(interaction); - var responseValues; +const setResponse = function (interaction, response) { + const $container = containerHelper.get(interaction); + let responseValues; if (response && interaction.paper) { try { responseValues = pciResponse.unserialize(response, interaction); @@ -567,16 +564,14 @@ var setResponse = function(interaction, response) { } if (_.isArray(responseValues)) { - _.forEach(interaction.getChoices(), function(choice) { - var element = interaction.paper.getById(choice.serial); + _.forEach(interaction.getChoices(), function (choice) { + const element = interaction.paper.getById(choice.serial); if (element) { - _.forEach(responseValues, function(pair) { - var responseChoice; - var responseGap; + _.forEach(responseValues, function (pair) { if (pair.length === 2) { //backward support of previous order - responseChoice = isDirectedPairFlipped ? pair[0] : pair[1]; - responseGap = isDirectedPairFlipped ? pair[1] : pair[0]; + const responseChoice = isDirectedPairFlipped ? pair[0] : pair[1]; + const responseGap = isDirectedPairFlipped ? pair[1] : pair[0]; if (responseChoice === choice.id()) { $('[data-identifier="' + responseGap + '"]', $container).addClass('active'); _selectShape(interaction, element, false); @@ -602,10 +597,10 @@ var setResponse = function(interaction, response) { * * @param {object} interaction */ -var resetResponse = function resetResponse(interaction) { +const resetResponse = function resetResponse(interaction) { _shapesUnSelectable(interaction); - _.forEach(interaction.gapFillers, function(gapFiller) { + _.forEach(interaction.gapFillers, function (gapFiller) { interactUtils.tapOn(gapFiller.items[2][0]); // this refers to the gapFiller image }); }; @@ -622,8 +617,8 @@ var resetResponse = function resetResponse(interaction) { * @param {object} interaction * @returns {object} */ -var getResponse = function(interaction) { - var raw = _getRawResponse(interaction); +const getResponse = function (interaction) { + const raw = _getRawResponse(interaction); return pciResponse.serialize(raw, interaction); }; @@ -631,10 +626,9 @@ var getResponse = function(interaction) { * Clean interaction destroy * @param {Object} interaction */ -var destroy = function destroy(interaction) { - var $container; +const destroy = function destroy(interaction) { if (interaction.paper) { - $container = containerHelper.get(interaction); + const $container = containerHelper.get(interaction); $(window).off('resize.qti-widget.' + interaction.serial); $container.off('resize.qti-widget.' + interaction.serial); @@ -642,9 +636,7 @@ var destroy = function destroy(interaction) { interaction.paper.clear(); instructionMgr.removeInstructions(interaction); - $('.main-image-box', $container) - .empty() - .removeAttr('style'); + $('.main-image-box', $container).empty().removeAttr('style'); $('.image-editor', $container).removeAttr('style'); $('ul', $container).empty(); @@ -661,7 +653,7 @@ var destroy = function destroy(interaction) { * @param {Object} interaction - the interaction instance * @param {Object} state - the interaction state */ -var setState = function setState(interaction, state) { +const setState = function setState(interaction, state) { if (_.isObject(state)) { if (state.response) { interaction.resetResponse(); @@ -676,9 +668,9 @@ var setState = function setState(interaction, state) { * @param {Object} interaction - the interaction instance * @returns {Object} the interaction current state */ -var getState = function getState(interaction) { - var state = {}; - var response = interaction.getResponse(); +const getState = function getState(interaction) { + const state = {}; + const response = interaction.getResponse(); if (response) { state.response = response; diff --git a/src/qtiCommonRenderer/renderers/interactions/GraphicOrderInteraction.js b/src/qtiCommonRenderer/renderers/interactions/GraphicOrderInteraction.js index 8d204423..21c7f475 100644 --- a/src/qtiCommonRenderer/renderers/interactions/GraphicOrderInteraction.js +++ b/src/qtiCommonRenderer/renderers/interactions/GraphicOrderInteraction.js @@ -13,7 +13,7 @@ * 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 (original work) Open Assessment Technlogies SA (under the project TAO-PRODUCT); + * Copyright (c) 2014-2023 (original work) Open Assessment Technlogies SA (under the project TAO-PRODUCT); * */ @@ -30,93 +30,41 @@ import containerHelper from 'taoQtiItem/qtiCommonRenderer/helpers/container'; import instructionMgr from 'taoQtiItem/qtiCommonRenderer/helpers/instructions/instructionManager'; /** - * 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#element10321 + * Creates ALL the texts (the numbers to display in the shapes). They are created styled but hidden. * - * @param {object} interaction - */ -var render = function render(interaction) { - var self = this; - - return new Promise(function(resolve, reject) { - var $container = containerHelper.get(interaction); - var $orderList = $('ul', $container); - var background = interaction.object.attributes; - - $container.off('resized.qti-widget.resolve').one('resized.qti-widget.resolve', resolve); - - //create the paper - interaction.paper = graphic.responsivePaper('graphic-paper-' + interaction.serial, interaction.serial, { - width: background.width, - height: background.height, - img: self.resolveUrl(background.data), - imgId: 'bg-image-' + interaction.serial, - container: $container - }); - - //create the list of number to order - _renderOrderList(interaction, $orderList); - - //call render choice for each interaction's choices - _.forEach(interaction.getChoices(), _.partial(_renderChoice, interaction, $orderList)); - - //set up the constraints instructions - instructionMgr.minMaxChoiceInstructions(interaction, { - min: interaction.attr('minChoices'), - max: interaction.attr('maxChoices'), - getResponse: _getRawResponse, - onError: function(data) { - graphic.highlightError(data.target); - } - }); - }); -}; - -/** - * Render a choice inside the paper. - * Please note that the choice renderer isn't implemented separately because it relies on the Raphael paper instead of the DOM. * @private - * @param {Object} interaction + * @param {Raphael.Paper} paper - the interaction paper + * @param {Number} size - the number of numbers to create... * @param {jQueryElement} $orderList - the list than contains the orderers - * @param {Object} choice - the hotspot choice to add to the interaction + * @return {Array} the creates text element */ -var _renderChoice = function _renderChoice(interaction, $orderList, choice) { - var rElement = graphic - .createElement(interaction.paper, choice.attr('shape'), choice.attr('coords'), { - id: choice.serial, - title: __('Select this area') - }) - .click(function(e) { - //if tts component is loaded and click-to-speak function is activated - we should prevent this listener to go further - if ( - $(e.currentTarget) - .closest('.qti-item') - .hasClass('prevent-click-handler') - ) { - return; - } - if (this.active) { - _unselectShape(interaction.paper, this, $orderList); - } else { - _selectShape(interaction.paper, this, $orderList); - } - containerHelper.triggerResponseChangeEvent(interaction); - instructionMgr.validateInstructions(interaction, { choice: choice }); +const _createTexts = function _createTexts(paper, size) { + const texts = []; + _.times(size, function (index) { + const number = index + 1; + const text = graphic.createText(paper, { + id: 'text-' + number, + content: number, + title: __('Remove'), + style: 'order-text', + hide: true, + disableEvents: true }); -}; + texts.push(text); + }); + return texts; +}; /** * Render the list of numbers * @private * @param {Object} interaction * @param {jQueryElement} $orderList - the list than contains the orderers */ -var _renderOrderList = function _renderOrderList(interaction, $orderList) { - var $orderers; - var size = _.size(interaction.getChoices()); - var min = interaction.attr('minChoices'); - var max = interaction.attr('maxChoices'); +const _renderOrderList = function _renderOrderList(interaction, $orderList) { + let size = _.size(interaction.getChoices()); + const min = interaction.attr('minChoices'); + const max = interaction.attr('maxChoices'); //calculate the number of orderer to display if (max > 0 && max <= size) { @@ -126,9 +74,9 @@ var _renderOrderList = function _renderOrderList(interaction, $orderList) { } //add them to the list - _.times(size, function(index) { - var position = index + 1; - var $orderer = $('