From 294fbadf6f3e5ed3537adf9df2a8a8e9b8106415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 29 May 2020 11:51:59 +0200 Subject: [PATCH] MOBILE-3438 question: Adapt ddmarker to new 3.9 changes --- src/addon/qtype/ddmarker/classes/ddmarker.ts | 278 ++++++++++-------- .../qtype/ddmarker/component/ddmarker.scss | 88 ++++-- 2 files changed, 227 insertions(+), 139 deletions(-) diff --git a/src/addon/qtype/ddmarker/classes/ddmarker.ts b/src/addon/qtype/ddmarker/classes/ddmarker.ts index d5199c656..90b5686b1 100644 --- a/src/addon/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addon/qtype/ddmarker/classes/ddmarker.ts @@ -27,6 +27,7 @@ export interface AddonQtypeDdMarkerQuestionDocStructure { dragItems?: () => HTMLElement[]; dragItemsForChoice?: (choiceNo: number) => HTMLElement[]; dragItemForChoice?: (choiceNo: number, itemNo: number) => HTMLElement; + dragItemPlaceholder?: (choiceNo: number) => HTMLElement; dragItemBeingDragged?: (choiceNo: number) => HTMLElement; dragItemHome?: (choiceNo: number) => HTMLElement; dragItemHomes?: () => HTMLElement[]; @@ -36,6 +37,14 @@ export interface AddonQtypeDdMarkerQuestionDocStructure { markerTexts?: () => HTMLElement; } +/** + * Point type. + */ +export type AddonQtypeDdMarkerQuestionPoint = { + x: number; // X axis coordinates. + y: number; // Y axis coordinates. +}; + /** * Class to make a question of ddmarker type work. */ @@ -98,14 +107,13 @@ export class AddonQtypeDdMarkerQuestion { * @return The new element. */ cloneNewDragItem(dragHome: HTMLElement, itemNo: number): HTMLElement { - const marker = dragHome.querySelector('span.markertext'); - marker.style.opacity = '0.6'; - // Clone the element and add the right classes. const drag = dragHome.cloneNode(true); drag.classList.remove('draghome'); drag.classList.add('dragitem'); drag.classList.add('item' + itemNo); + drag.classList.remove('dragplaceholder'); // In case it has it. + dragHome.classList.add('dragplaceholder'); // Insert the new drag after the dragHome. dragHome.parentElement.insertBefore(drag, dragHome.nextSibling); @@ -122,15 +130,14 @@ export class AddonQtypeDdMarkerQuestion { * @param bgImgXY X and Y of the BG IMG relative position. * @return Position relative to the window. */ - convertToWindowXY(bgImgXY: number[]): number[] { - const bgImg = this.doc.bgImg(), - position = this.domUtils.getElementXY(bgImg, null, 'ddarea'); + convertToWindowXY(bgImgXY: string): number[] { + const bgImg = this.doc.bgImg(); + const position = this.domUtils.getElementXY(bgImg, null, 'ddarea'); + let coordsNumbers = this.parsePoint(bgImgXY); - // Render the position related to the current image dimensions. - bgImgXY[0] *= this.proportion; - bgImgXY[1] *= this.proportion; + coordsNumbers = this.makePointProportional(coordsNumbers); - return [Number(bgImgXY[0]) + position[0], Number(bgImgXY[1]) + position[1]]; + return [coordsNumbers.x + position[0], coordsNumbers.y + position[1]]; } /** @@ -139,10 +146,10 @@ export class AddonQtypeDdMarkerQuestion { * @param coords Coordinates to check. * @return Whether they're inside the background image. */ - coordsInImg(coords: number[]): boolean { + coordsInImg(coords: AddonQtypeDdMarkerQuestionPoint): boolean { const bgImg = this.doc.bgImg(); - return (coords[0] * this.proportion <= bgImg.width && coords[1] * this.proportion <= bgImg.height); + return (coords.x * this.proportion <= bgImg.width + 1) && (coords.y * this.proportion <= bgImg.height + 1); } /** @@ -173,7 +180,7 @@ export class AddonQtypeDdMarkerQuestion { */ docStructure(slot: number): AddonQtypeDdMarkerQuestionDocStructure { const topNode = this.container.querySelector('.addon-qtype-ddmarker-container'), - dragItemsArea = topNode.querySelector('div.dragitems'); + dragItemsArea = topNode.querySelector('div.dragitems, div.draghomes'); return { topNode: (): HTMLElement => { @@ -194,14 +201,18 @@ export class AddonQtypeDdMarkerQuestion { dragItemForChoice: (choiceNo: number, itemNo: number): HTMLElement => { return dragItemsArea.querySelector('span.dragitem.choice' + choiceNo + '.item' + itemNo); }, + dragItemPlaceholder: (choiceNo: number): HTMLElement => { + return dragItemsArea.querySelector('span.dragplaceholder.choice' + choiceNo); + }, dragItemBeingDragged: (choiceNo: number): HTMLElement => { return dragItemsArea.querySelector('span.dragitem.beingdragged.choice' + choiceNo); }, dragItemHome: (choiceNo: number): HTMLElement => { - return dragItemsArea.querySelector('span.draghome.choice' + choiceNo); + return dragItemsArea.querySelector('span.draghome.choice' + choiceNo + + ', span.marker.choice' + choiceNo); }, dragItemHomes: (): HTMLElement[] => { - return Array.from(dragItemsArea.querySelectorAll('span.draghome')); + return Array.from(dragItemsArea.querySelectorAll('span.draghome, span.marker')); }, getClassnameNumericSuffix: (node: HTMLElement, prefix: string): number => { @@ -328,13 +339,11 @@ export class AddonQtypeDdMarkerQuestion { const markerSpan = this.doc.topNode().querySelector( 'div.ddarea div.markertexts span.markertext' + dropZoneNo); if (markerSpan !== null) { - - xyForText[0] = (xyForText[0] - markerSpan.offsetWidth / 2) * this.proportion; - xyForText[1] = (xyForText[1] - markerSpan.offsetHeight / 2) * this.proportion; - + const width = this.domUtils.getElementMeasure(markerSpan, true, true, false, true); + const height = this.domUtils.getElementMeasure(markerSpan, false, true, false, true); markerSpan.style.opacity = '0.6'; - markerSpan.style.left = xyForText[0] + 'px'; - markerSpan.style.top = xyForText[1] + 'px'; + markerSpan.style.left = (xyForText.x - (width / 2)) + 'px'; + markerSpan.style.top = (xyForText.y - (height / 2)) + 'px'; const markerSpanAnchor = markerSpan.querySelector('a'); if (markerSpanAnchor !== null) { @@ -364,38 +373,36 @@ export class AddonQtypeDdMarkerQuestion { * Draw a circle in a drop zone. * * @param dropZoneNo Number of the drop zone. - * @param coords Coordinates of the circle. + * @param coordinates Coordinates of the circle. * @param colour Colour of the circle. * @return X and Y position of the center of the circle. */ - drawShapeCircle(dropZoneNo: number, coords: string, colour: string): number[] { - // Extract the numbers in the coordinates. - const coordsParts = coords.match(/(\d+),(\d+);(\d+)/); + drawShapeCircle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint { + if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?$/)) { + return null; + } - if (coordsParts && coordsParts.length === 4) { - // Remove first element and convert them to number. - coordsParts.shift(); + const bits = coordinates.split(';'); + let centre = this.parsePoint(bits[0]); + const radius = Number(bits[1]); - const coordsPartsNum = coordsParts.map((i) => { - return Number(i); + // Calculate circle limits and check it's inside the background image. + const circleLimit = {x: centre.x - radius, y: centre.y - radius}; + if (this.coordsInImg(circleLimit)) { + centre = this.makePointProportional(centre); + + // All good, create the shape. + this.shapes[dropZoneNo] = this.graphics.addShape({ + type: 'circle', + color: colour + }, { + cx: centre.x, + cy: centre.y, + r: Math.round(radius * this.proportion) }); - // Calculate circle limits and check it's inside the background image. - const circleLimit = [coordsPartsNum[0] - coordsPartsNum[2], coordsPartsNum[1] - coordsPartsNum[2]]; - if (this.coordsInImg(circleLimit)) { - // All good, create the shape. - this.shapes[dropZoneNo] = this.graphics.addShape({ - type: 'circle', - color: colour - }, { - cx: coordsPartsNum[0] * this.proportion, - cy: coordsPartsNum[1] * this.proportion, - r: coordsPartsNum[2] * this.proportion - }); - - // Return the center. - return [coordsPartsNum[0], coordsPartsNum[1]]; - } + // Return the centre. + return centre; } return null; @@ -405,39 +412,40 @@ export class AddonQtypeDdMarkerQuestion { * Draw a rectangle in a drop zone. * * @param dropZoneNo Number of the drop zone. - * @param coords Coordinates of the rectangle. + * @param coordinates Coordinates of the rectangle. * @param colour Colour of the rectangle. * @return X and Y position of the center of the rectangle. */ - drawShapeRectangle(dropZoneNo: number, coords: string, colour: string): number[] { - // Extract the numbers in the coordinates. - const coordsParts = coords.match(/(\d+),(\d+);(\d+),(\d+)/); + drawShapeRectangle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint { + if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?,\d+(\.\d+)?$/)) { + return null; + } - if (coordsParts && coordsParts.length === 5) { - // Remove first element and convert them to number. - coordsParts.shift(); + const bits = coordinates.split(';'); + const startPoint = this.parsePoint(bits[0]); + const size = this.parsePoint(bits[1]); - const coordsPartsNum = coordsParts.map((i) => { - return Number(i); + // Calculate rectangle limits and check it's inside the background image. + const rectLimits = {x: startPoint.x + size.x, y: startPoint.y + size.y}; + if (this.coordsInImg(rectLimits)) { + const startPointProp = this.makePointProportional(startPoint); + const sizeProp = this.makePointProportional(size); + + // All good, create the shape. + this.shapes[dropZoneNo] = this.graphics.addShape({ + type: 'rect', + color: colour + }, { + x: startPointProp.x, + y: startPointProp.y, + width: sizeProp.x, + height: sizeProp.y }); - // Calculate rectangle limits and check it's inside the background image. - const rectLimits = [coordsPartsNum[0] + coordsPartsNum[2], coordsPartsNum[1] + coordsPartsNum[3]]; - if (this.coordsInImg(rectLimits)) { - // All good, create the shape. - this.shapes[dropZoneNo] = this.graphics.addShape({ - type: 'rect', - color: colour - }, { - x: coordsPartsNum[0] * this.proportion, - y: coordsPartsNum[1] * this.proportion, - width: coordsPartsNum[2] * this.proportion, - height: coordsPartsNum[3] * this.proportion - }); + const centre = { x: startPoint.x + (size.x / 2) , y: startPoint.y + (size.y / 2)}; - // Return the center. - return [coordsPartsNum[0] + coordsPartsNum[2] / 2, coordsPartsNum[1] + coordsPartsNum[3] / 2]; - } + // Return the centre. + return this.makePointProportional(centre); } return null; @@ -447,53 +455,83 @@ export class AddonQtypeDdMarkerQuestion { * Draw a polygon in a drop zone. * * @param dropZoneNo Number of the drop zone. - * @param coords Coordinates of the polygon. + * @param coordinates Coordinates of the polygon. * @param colour Colour of the polygon. * @return X and Y position of the center of the polygon. */ - drawShapePolygon(dropZoneNo: number, coords: string, colour: string): number[] { - const coordsParts = coords.split(';'), - points = [], - bgImg = this.doc.bgImg(), - maxXY = [0, 0], - minXY = [bgImg.width, bgImg.height]; - - for (const i in coordsParts) { - // Extract the X and Y of this point. - const partsString = coordsParts[i].match(/^(\d+),(\d+)$/), - parts = partsString && partsString.map((part) => { - return Number(part); - }); - - if (parts !== null && this.coordsInImg([parts[1], parts[2]])) { - parts[1] *= this.proportion; - parts[2] *= this.proportion; - - // Calculate min and max points to find center to show marker on. - minXY[0] = Math.min(parts[1], minXY[0]); - minXY[1] = Math.min(parts[2], minXY[1]); - maxXY[0] = Math.max(parts[1], maxXY[0]); - maxXY[1] = Math.max(parts[2], maxXY[1]); - - points.push(parts[1] + ',' + parts[2]); - } + drawShapePolygon(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint { + if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?(?:;\d+(\.\d+)?,\d+(\.\d+)?)*$/)) { + return null; } - if (points.length > 2) { + const bits = coordinates.split(';'); + const centre = {x: 0, y: 0}; + const points = bits.map((bit) => { + const point = this.parsePoint(bit); + centre.x += point.x; + centre.y += point.y; + + return point; + }); + + if (points.length > 0) { + centre.x = Math.round(centre.x / points.length); + centre.y = Math.round(centre.y / points.length); + } + + const pointsOnImg = []; + points.forEach((point) => { + if (this.coordsInImg(point)) { + point = this.makePointProportional(point); + + pointsOnImg.push(point.x + ',' + point.y); + } + }); + + if (pointsOnImg.length > 2) { this.shapes[dropZoneNo] = this.graphics.addShape({ type: 'polygon', color: colour }, { - points: points.join(' ') + points: pointsOnImg.join(' ') }); - // Return the center. - return [(minXY[0] + maxXY[0]) / 2, (minXY[1] + maxXY[1]) / 2]; + // Return the centre. + return this.makePointProportional(centre); } return null; } + /** + * Make a point from the string representation. + * + * @param coordinates "x,y". + * @return Coordinates to the point. + */ + parsePoint(coordinates: string): AddonQtypeDdMarkerQuestionPoint { + const bits = coordinates.split(','); + if (bits.length !== 2) { + throw coordinates + ' is not a valid point'; + } + + return {x: Number(bits[0]), y: Number(bits[1])}; + } + + /** + * Make proportional position of the point. + * + * @param point Point coordinates. + * @return Converted point. + */ + makePointProportional(point: AddonQtypeDdMarkerQuestionPoint): AddonQtypeDdMarkerQuestionPoint { + return { + x: Math.round(point.x * this.proportion), + y: Math.round(point.y * this.proportion) + + }; + } + /** * Drop a drag element into a certain position. * @@ -507,9 +545,6 @@ export class AddonQtypeDdMarkerQuestion { // Set the position related to the natural image dimensions. if (this.proportion < 1) { position[0] = Math.round(position[0] / this.proportion); - } - - if (this.proportion < 1) { position[1] = Math.round(position[1] / this.proportion); } } @@ -538,11 +573,7 @@ export class AddonQtypeDdMarkerQuestion { const coordsStrings = fv.split(';'); for (let i = 0; i < coordsStrings.length; i++) { - const coordsNumbers = coordsStrings[i].split(',').map((i) => { - return Number(i); - }); - - coords[coords.length] = this.convertToWindowXY(coordsNumbers); + coords[coords.length] = this.convertToWindowXY(coordsStrings[i]); } } @@ -581,9 +612,6 @@ export class AddonQtypeDdMarkerQuestion { // Set the position related to the natural image dimensions. if (this.proportion < 1) { position[0] = Math.round(position[0] / this.proportion); - } - - if (this.proportion < 1) { position[1] = Math.round(position[1] / this.proportion); } @@ -723,6 +751,12 @@ export class AddonQtypeDdMarkerQuestion { this.question.loaded = true; }; + if (bgImg.complete && bgImg.naturalWidth) { + imgLoaded(); + + return; + } + bgImg.addEventListener('load', imgLoaded); // Try again after a while. @@ -764,13 +798,25 @@ export class AddonQtypeDdMarkerQuestion { dragItem.classList.remove('unneeded'); } + const placeholder = this.doc.dragItemPlaceholder(choiceNo); + // Remove the class only if is placed on the image. if (homePosition[0] != coords[i][0] || homePosition[1] != coords[i][1]) { dragItem.classList.remove('unplaced'); - } + dragItem.classList.add('placed'); - dragItem.style.left = coords[i][0] + 'px'; - dragItem.style.top = coords[i][1] + 'px'; + const computedStyle = getComputedStyle(dragItem); + const left = coords[i][0] - this.domUtils.getComputedStyleMeasure(computedStyle, 'marginLeft'); + const top = coords[i][1] - this.domUtils.getComputedStyleMeasure(computedStyle, 'marginTop'); + + dragItem.style.left = left + 'px'; + dragItem.style.top = top + 'px'; + placeholder.classList.add('active'); + } else { + dragItem.classList.remove('placed'); + dragItem.classList.add('unplaced'); + placeholder.classList.remove('active'); + } } } diff --git a/src/addon/qtype/ddmarker/component/ddmarker.scss b/src/addon/qtype/ddmarker/component/ddmarker.scss index cba59c4e7..7c33d5302 100644 --- a/src/addon/qtype/ddmarker/component/ddmarker.scss +++ b/src/addon/qtype/ddmarker/component/ddmarker.scss @@ -9,24 +9,15 @@ addon-qtype-ddmarker { display: block; } + .droparea { + display: inline-block; + } + div.droparea img { border: 1px solid $gray-darker; max-width: 100%; } - .draghome img, .draghome span { - visibility: hidden; - } - - .dragitems .dragitem { - cursor: pointer; - position: absolute; - z-index: 2; - } - - .dropzones { - position: absolute; - } .dropzones svg { z-index: 3; } @@ -35,31 +26,81 @@ addon-qtype-ddmarker { z-index: 5; box-shadow: $core-dd-question-selected-shadow; } - .dragitems .draghome { - margin: 10px; - display: inline-block; + + .dragitems, // Previous to 3.9. + .draghomes { + &.readonly { + .dragitem, + .marker { + cursor: auto; + } + } + + .dragitem, // Previous to 3.9. + .draghome, + .marker { + vertical-align: top; + cursor: pointer; + position: relative; + margin: 10px; + display: inline-block; + &.dragplaceholder { + display: none; + visibility: hidden; + + &.active { + display: inline-block; + } + } + + &.unplaced { + position: relative; + } + &.placed { + position: absolute; + opacity: 0.6; + } + } } - .dragitems.readonly .dragitem { - cursor: auto; + .droparea { + .dragitem, + .marker { + cursor: pointer; + position: absolute; + vertical-align: top; + z-index: 2; + } } + div.ddarea { text-align: center; + position: relative; } + div.ddarea .dropzones, div.ddarea .markertexts { + top: 0; + left: 0; min-height: 80px; position: absolute; @include text-align('start'); } + .dropbackground { margin: 0 auto; } - div.dragitems div.draghome, div.dragitems div.dragitem, - div.draghome, div.drag { + div.dragitems div.draghome, + div.dragitems div.dragitem, + div.draghome, + div.drag, + div.draghomes div.marker, + div.marker, + div.drag { font: 13px/1.231 arial,helvetica,clean,sans-serif; } div.dragitems span.markertext, + div.draghomes span.markertext, div.markertexts span.markertext { margin: 0 5px; z-index: 2; @@ -86,17 +127,18 @@ addon-qtype-ddmarker { border-color: $yellow; padding: 5px; border-radius: 10px; - filter: alpha(opacity=60); opacity: 0.6; margin: 5px; display: inline-block; } - div.dragitems img.target { + div.dragitems img.target, + div.draghomes img.target { position: absolute; left: -7px; /* This must be half the size of the target image, minus 0.5. */ top: -7px; /* In other words, this works for a 15x15 cross-hair. */ } - div.dragitems div.draghome img.target { + div.dragitems div.draghome img.target, + div.draghomes div.marker img.target { display: none; } }