MOBILE-3438 question: Adapt ddmarker to new 3.9 changes

main
Pau Ferrer Ocaña 2020-05-29 11:51:59 +02:00
parent 03671ebbf8
commit 294fbadf6f
2 changed files with 227 additions and 139 deletions

View File

@ -27,6 +27,7 @@ export interface AddonQtypeDdMarkerQuestionDocStructure {
dragItems?: () => HTMLElement[]; dragItems?: () => HTMLElement[];
dragItemsForChoice?: (choiceNo: number) => HTMLElement[]; dragItemsForChoice?: (choiceNo: number) => HTMLElement[];
dragItemForChoice?: (choiceNo: number, itemNo: number) => HTMLElement; dragItemForChoice?: (choiceNo: number, itemNo: number) => HTMLElement;
dragItemPlaceholder?: (choiceNo: number) => HTMLElement;
dragItemBeingDragged?: (choiceNo: number) => HTMLElement; dragItemBeingDragged?: (choiceNo: number) => HTMLElement;
dragItemHome?: (choiceNo: number) => HTMLElement; dragItemHome?: (choiceNo: number) => HTMLElement;
dragItemHomes?: () => HTMLElement[]; dragItemHomes?: () => HTMLElement[];
@ -36,6 +37,14 @@ export interface AddonQtypeDdMarkerQuestionDocStructure {
markerTexts?: () => HTMLElement; 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. * Class to make a question of ddmarker type work.
*/ */
@ -98,14 +107,13 @@ export class AddonQtypeDdMarkerQuestion {
* @return The new element. * @return The new element.
*/ */
cloneNewDragItem(dragHome: HTMLElement, itemNo: number): HTMLElement { cloneNewDragItem(dragHome: HTMLElement, itemNo: number): HTMLElement {
const marker = <HTMLElement> dragHome.querySelector('span.markertext');
marker.style.opacity = '0.6';
// Clone the element and add the right classes. // Clone the element and add the right classes.
const drag = <HTMLElement> dragHome.cloneNode(true); const drag = <HTMLElement> dragHome.cloneNode(true);
drag.classList.remove('draghome'); drag.classList.remove('draghome');
drag.classList.add('dragitem'); drag.classList.add('dragitem');
drag.classList.add('item' + itemNo); 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. // Insert the new drag after the dragHome.
dragHome.parentElement.insertBefore(drag, dragHome.nextSibling); dragHome.parentElement.insertBefore(drag, dragHome.nextSibling);
@ -122,15 +130,14 @@ export class AddonQtypeDdMarkerQuestion {
* @param bgImgXY X and Y of the BG IMG relative position. * @param bgImgXY X and Y of the BG IMG relative position.
* @return Position relative to the window. * @return Position relative to the window.
*/ */
convertToWindowXY(bgImgXY: number[]): number[] { convertToWindowXY(bgImgXY: string): number[] {
const bgImg = this.doc.bgImg(), const bgImg = this.doc.bgImg();
position = this.domUtils.getElementXY(bgImg, null, 'ddarea'); const position = this.domUtils.getElementXY(bgImg, null, 'ddarea');
let coordsNumbers = this.parsePoint(bgImgXY);
// Render the position related to the current image dimensions. coordsNumbers = this.makePointProportional(coordsNumbers);
bgImgXY[0] *= this.proportion;
bgImgXY[1] *= this.proportion;
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. * @param coords Coordinates to check.
* @return Whether they're inside the background image. * @return Whether they're inside the background image.
*/ */
coordsInImg(coords: number[]): boolean { coordsInImg(coords: AddonQtypeDdMarkerQuestionPoint): boolean {
const bgImg = this.doc.bgImg(); 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 { docStructure(slot: number): AddonQtypeDdMarkerQuestionDocStructure {
const topNode = <HTMLElement> this.container.querySelector('.addon-qtype-ddmarker-container'), const topNode = <HTMLElement> this.container.querySelector('.addon-qtype-ddmarker-container'),
dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems'); dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems, div.draghomes');
return { return {
topNode: (): HTMLElement => { topNode: (): HTMLElement => {
@ -194,14 +201,18 @@ export class AddonQtypeDdMarkerQuestion {
dragItemForChoice: (choiceNo: number, itemNo: number): HTMLElement => { dragItemForChoice: (choiceNo: number, itemNo: number): HTMLElement => {
return <HTMLElement> dragItemsArea.querySelector('span.dragitem.choice' + choiceNo + '.item' + itemNo); return <HTMLElement> dragItemsArea.querySelector('span.dragitem.choice' + choiceNo + '.item' + itemNo);
}, },
dragItemPlaceholder: (choiceNo: number): HTMLElement => {
return <HTMLElement> dragItemsArea.querySelector('span.dragplaceholder.choice' + choiceNo);
},
dragItemBeingDragged: (choiceNo: number): HTMLElement => { dragItemBeingDragged: (choiceNo: number): HTMLElement => {
return <HTMLElement> dragItemsArea.querySelector('span.dragitem.beingdragged.choice' + choiceNo); return <HTMLElement> dragItemsArea.querySelector('span.dragitem.beingdragged.choice' + choiceNo);
}, },
dragItemHome: (choiceNo: number): HTMLElement => { dragItemHome: (choiceNo: number): HTMLElement => {
return <HTMLElement> dragItemsArea.querySelector('span.draghome.choice' + choiceNo); return <HTMLElement> dragItemsArea.querySelector('span.draghome.choice' + choiceNo +
', span.marker.choice' + choiceNo);
}, },
dragItemHomes: (): HTMLElement[] => { dragItemHomes: (): HTMLElement[] => {
return <HTMLElement[]> Array.from(dragItemsArea.querySelectorAll('span.draghome')); return <HTMLElement[]> Array.from(dragItemsArea.querySelectorAll('span.draghome, span.marker'));
}, },
getClassnameNumericSuffix: (node: HTMLElement, prefix: string): number => { getClassnameNumericSuffix: (node: HTMLElement, prefix: string): number => {
@ -328,13 +339,11 @@ export class AddonQtypeDdMarkerQuestion {
const markerSpan = <HTMLElement> this.doc.topNode().querySelector( const markerSpan = <HTMLElement> this.doc.topNode().querySelector(
'div.ddarea div.markertexts span.markertext' + dropZoneNo); 'div.ddarea div.markertexts span.markertext' + dropZoneNo);
if (markerSpan !== null) { if (markerSpan !== null) {
const width = this.domUtils.getElementMeasure(markerSpan, true, true, false, true);
xyForText[0] = (xyForText[0] - markerSpan.offsetWidth / 2) * this.proportion; const height = this.domUtils.getElementMeasure(markerSpan, false, true, false, true);
xyForText[1] = (xyForText[1] - markerSpan.offsetHeight / 2) * this.proportion;
markerSpan.style.opacity = '0.6'; markerSpan.style.opacity = '0.6';
markerSpan.style.left = xyForText[0] + 'px'; markerSpan.style.left = (xyForText.x - (width / 2)) + 'px';
markerSpan.style.top = xyForText[1] + 'px'; markerSpan.style.top = (xyForText.y - (height / 2)) + 'px';
const markerSpanAnchor = markerSpan.querySelector('a'); const markerSpanAnchor = markerSpan.querySelector('a');
if (markerSpanAnchor !== null) { if (markerSpanAnchor !== null) {
@ -364,38 +373,36 @@ export class AddonQtypeDdMarkerQuestion {
* Draw a circle in a drop zone. * Draw a circle in a drop zone.
* *
* @param dropZoneNo Number of the 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. * @param colour Colour of the circle.
* @return X and Y position of the center of the circle. * @return X and Y position of the center of the circle.
*/ */
drawShapeCircle(dropZoneNo: number, coords: string, colour: string): number[] { drawShapeCircle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint {
// Extract the numbers in the coordinates. if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?$/)) {
const coordsParts = coords.match(/(\d+),(\d+);(\d+)/); return null;
}
if (coordsParts && coordsParts.length === 4) { const bits = coordinates.split(';');
// Remove first element and convert them to number. let centre = this.parsePoint(bits[0]);
coordsParts.shift(); const radius = Number(bits[1]);
const coordsPartsNum = coordsParts.map((i) => { // Calculate circle limits and check it's inside the background image.
return Number(i); 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. // Return the centre.
const circleLimit = [coordsPartsNum[0] - coordsPartsNum[2], coordsPartsNum[1] - coordsPartsNum[2]]; return centre;
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 null; return null;
@ -405,39 +412,40 @@ export class AddonQtypeDdMarkerQuestion {
* Draw a rectangle in a drop zone. * Draw a rectangle in a drop zone.
* *
* @param dropZoneNo Number of the 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. * @param colour Colour of the rectangle.
* @return X and Y position of the center of the rectangle. * @return X and Y position of the center of the rectangle.
*/ */
drawShapeRectangle(dropZoneNo: number, coords: string, colour: string): number[] { drawShapeRectangle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint {
// Extract the numbers in the coordinates. if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?,\d+(\.\d+)?$/)) {
const coordsParts = coords.match(/(\d+),(\d+);(\d+),(\d+)/); return null;
}
if (coordsParts && coordsParts.length === 5) { const bits = coordinates.split(';');
// Remove first element and convert them to number. const startPoint = this.parsePoint(bits[0]);
coordsParts.shift(); const size = this.parsePoint(bits[1]);
const coordsPartsNum = coordsParts.map((i) => { // Calculate rectangle limits and check it's inside the background image.
return Number(i); 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 centre = { x: startPoint.x + (size.x / 2) , y: startPoint.y + (size.y / 2)};
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
});
// Return the center. // Return the centre.
return [coordsPartsNum[0] + coordsPartsNum[2] / 2, coordsPartsNum[1] + coordsPartsNum[3] / 2]; return this.makePointProportional(centre);
}
} }
return null; return null;
@ -447,53 +455,83 @@ export class AddonQtypeDdMarkerQuestion {
* Draw a polygon in a drop zone. * Draw a polygon in a drop zone.
* *
* @param dropZoneNo Number of the 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. * @param colour Colour of the polygon.
* @return X and Y position of the center of the polygon. * @return X and Y position of the center of the polygon.
*/ */
drawShapePolygon(dropZoneNo: number, coords: string, colour: string): number[] { drawShapePolygon(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint {
const coordsParts = coords.split(';'), if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?(?:;\d+(\.\d+)?,\d+(\.\d+)?)*$/)) {
points = [], return null;
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]);
}
} }
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({ this.shapes[dropZoneNo] = this.graphics.addShape({
type: 'polygon', type: 'polygon',
color: colour color: colour
}, { }, {
points: points.join(' ') points: pointsOnImg.join(' ')
}); });
// Return the center. // Return the centre.
return [(minXY[0] + maxXY[0]) / 2, (minXY[1] + maxXY[1]) / 2]; return this.makePointProportional(centre);
} }
return null; 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. * Drop a drag element into a certain position.
* *
@ -507,9 +545,6 @@ export class AddonQtypeDdMarkerQuestion {
// Set the position related to the natural image dimensions. // Set the position related to the natural image dimensions.
if (this.proportion < 1) { if (this.proportion < 1) {
position[0] = Math.round(position[0] / this.proportion); position[0] = Math.round(position[0] / this.proportion);
}
if (this.proportion < 1) {
position[1] = Math.round(position[1] / this.proportion); position[1] = Math.round(position[1] / this.proportion);
} }
} }
@ -538,11 +573,7 @@ export class AddonQtypeDdMarkerQuestion {
const coordsStrings = fv.split(';'); const coordsStrings = fv.split(';');
for (let i = 0; i < coordsStrings.length; i++) { for (let i = 0; i < coordsStrings.length; i++) {
const coordsNumbers = coordsStrings[i].split(',').map((i) => { coords[coords.length] = this.convertToWindowXY(coordsStrings[i]);
return Number(i);
});
coords[coords.length] = this.convertToWindowXY(coordsNumbers);
} }
} }
@ -581,9 +612,6 @@ export class AddonQtypeDdMarkerQuestion {
// Set the position related to the natural image dimensions. // Set the position related to the natural image dimensions.
if (this.proportion < 1) { if (this.proportion < 1) {
position[0] = Math.round(position[0] / this.proportion); position[0] = Math.round(position[0] / this.proportion);
}
if (this.proportion < 1) {
position[1] = Math.round(position[1] / this.proportion); position[1] = Math.round(position[1] / this.proportion);
} }
@ -723,6 +751,12 @@ export class AddonQtypeDdMarkerQuestion {
this.question.loaded = true; this.question.loaded = true;
}; };
if (bgImg.complete && bgImg.naturalWidth) {
imgLoaded();
return;
}
bgImg.addEventListener('load', imgLoaded); bgImg.addEventListener('load', imgLoaded);
// Try again after a while. // Try again after a while.
@ -764,13 +798,25 @@ export class AddonQtypeDdMarkerQuestion {
dragItem.classList.remove('unneeded'); dragItem.classList.remove('unneeded');
} }
const placeholder = this.doc.dragItemPlaceholder(choiceNo);
// Remove the class only if is placed on the image. // Remove the class only if is placed on the image.
if (homePosition[0] != coords[i][0] || homePosition[1] != coords[i][1]) { if (homePosition[0] != coords[i][0] || homePosition[1] != coords[i][1]) {
dragItem.classList.remove('unplaced'); dragItem.classList.remove('unplaced');
} dragItem.classList.add('placed');
dragItem.style.left = coords[i][0] + 'px'; const computedStyle = getComputedStyle(dragItem);
dragItem.style.top = coords[i][1] + 'px'; 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');
}
} }
} }

View File

@ -9,24 +9,15 @@ addon-qtype-ddmarker {
display: block; display: block;
} }
.droparea {
display: inline-block;
}
div.droparea img { div.droparea img {
border: 1px solid $gray-darker; border: 1px solid $gray-darker;
max-width: 100%; max-width: 100%;
} }
.draghome img, .draghome span {
visibility: hidden;
}
.dragitems .dragitem {
cursor: pointer;
position: absolute;
z-index: 2;
}
.dropzones {
position: absolute;
}
.dropzones svg { .dropzones svg {
z-index: 3; z-index: 3;
} }
@ -35,31 +26,81 @@ addon-qtype-ddmarker {
z-index: 5; z-index: 5;
box-shadow: $core-dd-question-selected-shadow; box-shadow: $core-dd-question-selected-shadow;
} }
.dragitems .draghome {
margin: 10px; .dragitems, // Previous to 3.9.
display: inline-block; .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 { .droparea {
cursor: auto; .dragitem,
.marker {
cursor: pointer;
position: absolute;
vertical-align: top;
z-index: 2;
}
} }
div.ddarea { div.ddarea {
text-align: center; text-align: center;
position: relative;
} }
div.ddarea .dropzones,
div.ddarea .markertexts { div.ddarea .markertexts {
top: 0;
left: 0;
min-height: 80px; min-height: 80px;
position: absolute; position: absolute;
@include text-align('start'); @include text-align('start');
} }
.dropbackground { .dropbackground {
margin: 0 auto; margin: 0 auto;
} }
div.dragitems div.draghome, div.dragitems div.dragitem, div.dragitems div.draghome,
div.draghome, div.drag { 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; font: 13px/1.231 arial,helvetica,clean,sans-serif;
} }
div.dragitems span.markertext, div.dragitems span.markertext,
div.draghomes span.markertext,
div.markertexts span.markertext { div.markertexts span.markertext {
margin: 0 5px; margin: 0 5px;
z-index: 2; z-index: 2;
@ -86,17 +127,18 @@ addon-qtype-ddmarker {
border-color: $yellow; border-color: $yellow;
padding: 5px; padding: 5px;
border-radius: 10px; border-radius: 10px;
filter: alpha(opacity=60);
opacity: 0.6; opacity: 0.6;
margin: 5px; margin: 5px;
display: inline-block; display: inline-block;
} }
div.dragitems img.target { div.dragitems img.target,
div.draghomes img.target {
position: absolute; position: absolute;
left: -7px; /* This must be half the size of the target image, minus 0.5. */ 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. */ 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; display: none;
} }
} }