MOBILE-2390 qtype: Implement ddmarker type
parent
d14d700f7b
commit
7d3015015f
|
@ -0,0 +1,891 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { AddonQtypeDdMarkerGraphicsApi } from './graphics_api';
|
||||
|
||||
/**
|
||||
* Encapsulates operations on dd area.
|
||||
*/
|
||||
export interface AddonQtypeDdMarkerQuestionDocStructure {
|
||||
topNode?: () => HTMLElement;
|
||||
bgImg?: () => HTMLImageElement;
|
||||
dragItemsArea?: () => HTMLElement;
|
||||
dragItems?: () => HTMLElement[];
|
||||
dragItemsForChoice?: (choiceNo: number) => HTMLElement[];
|
||||
dragItemForChoice?: (choiceNo: number, itemNo: number) => HTMLElement;
|
||||
dragItemBeingDragged?: (choiceNo: number) => HTMLElement;
|
||||
dragItemHome?: (choiceNo: number) => HTMLElement;
|
||||
dragItemHomes?: () => HTMLElement[];
|
||||
getClassnameNumericSuffix?: (node: HTMLElement, prefix: string) => number;
|
||||
inputsForChoices?: () => HTMLElement[];
|
||||
inputForChoice?: (choiceNo: number) => HTMLElement;
|
||||
markerTexts?: () => HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to make a question of ddmarker type work.
|
||||
*/
|
||||
export class AddonQtypeDdMarkerQuestion {
|
||||
protected COLOURS = ['#FFFFFF', '#B0C4DE', '#DCDCDC', '#D8BFD8', '#87CEFA', '#DAA520', '#FFD700', '#F0E68C'];
|
||||
|
||||
protected logger: any;
|
||||
protected afterImageLoadDone = false;
|
||||
protected drops;
|
||||
protected topNode;
|
||||
protected nextColourIndex = 0;
|
||||
protected proportion = 1;
|
||||
protected selected: HTMLElement; // Selected element (being "dragged").
|
||||
protected graphics: AddonQtypeDdMarkerGraphicsApi;
|
||||
|
||||
doc: AddonQtypeDdMarkerQuestionDocStructure;
|
||||
shapes = [];
|
||||
|
||||
/**
|
||||
* Create the instance.
|
||||
*
|
||||
* @param {CoreLoggerProvider} logger Logger provider.
|
||||
* @param {CoreDomUtilsProvider} domUtils Dom Utils provider.
|
||||
* @param {CoreTextUtilsProvider} textUtils Text Utils provider.
|
||||
* @param {HTMLElement} container The container HTMLElement of the question.
|
||||
* @param {any} question The question instance.
|
||||
* @param {boolean} readOnly Whether it's read only.
|
||||
* @param {any[]} dropZones The drop zones received in the init object of the question.
|
||||
*/
|
||||
constructor(logger: CoreLoggerProvider, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider,
|
||||
protected container: HTMLElement, protected question: any, protected readOnly: boolean, protected dropZones: any[]) {
|
||||
this.logger = logger.getInstance('AddonQtypeDdMarkerQuestion');
|
||||
|
||||
this.graphics = new AddonQtypeDdMarkerGraphicsApi(this, this.domUtils);
|
||||
|
||||
this.initializer(question);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate image proportion to make easy conversions.
|
||||
*/
|
||||
calculateImgProportion(): void {
|
||||
const bgImg = this.doc.bgImg();
|
||||
|
||||
// Render the position related to the current image dimensions.
|
||||
this.proportion = 1;
|
||||
if (bgImg.width != bgImg.naturalWidth) {
|
||||
this.proportion = bgImg.width / bgImg.naturalWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new draggable element cloning a certain element.
|
||||
*
|
||||
* @param {HTMLElement} dragHome The element to clone.
|
||||
* @param {number} itemNo The number of the new item.
|
||||
* @return {HTMLElement} The new element.
|
||||
*/
|
||||
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.
|
||||
const drag = <HTMLElement> dragHome.cloneNode(true);
|
||||
drag.classList.remove('draghome');
|
||||
drag.classList.add('dragitem');
|
||||
drag.classList.add('item' + itemNo);
|
||||
|
||||
// Insert the new drag after the dragHome.
|
||||
dragHome.parentElement.insertBefore(drag, dragHome.nextSibling);
|
||||
if (!this.readOnly) {
|
||||
this.draggable(drag);
|
||||
}
|
||||
|
||||
return drag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the X and Y position of the BG IMG to a position relative to the window.
|
||||
*
|
||||
* @param {number[]} bgImgXY X and Y of the BG IMG relative position.
|
||||
* @return {number[]} Position relative to the window.
|
||||
*/
|
||||
convertToWindowXY(bgImgXY: number[]): number[] {
|
||||
const bgImg = this.doc.bgImg(),
|
||||
position = this.domUtils.getElementXY(bgImg, null, 'ddarea');
|
||||
|
||||
// Render the position related to the current image dimensions.
|
||||
bgImgXY[0] *= this.proportion;
|
||||
bgImgXY[1] *= this.proportion;
|
||||
|
||||
return [Number(bgImgXY[0]) + position[0], Number(bgImgXY[1]) + position[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if some coordinates (X, Y) are inside the background image.
|
||||
*
|
||||
* @param {number[]} coords Coordinates to check.
|
||||
* @return {boolean} Whether they're inside the background image.
|
||||
*/
|
||||
coordsInImg(coords: number[]): boolean {
|
||||
const bgImg = this.doc.bgImg();
|
||||
|
||||
return (coords[0] * this.proportion <= bgImg.width && coords[1] * this.proportion <= bgImg.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselect all draggable items.
|
||||
*/
|
||||
deselectDrags(): void {
|
||||
const drags = this.doc.dragItems();
|
||||
drags.forEach((drag) => {
|
||||
drag.classList.remove('beingdragged');
|
||||
});
|
||||
this.selected = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call when the instance is no longer needed.
|
||||
*/
|
||||
destroy(): void {
|
||||
window.removeEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object to encapsulate operations on dd area.
|
||||
*
|
||||
* @param {number} slot The question slot.
|
||||
* @return {AddonQtypeDdMarkerQuestionDocStructure} The object.
|
||||
*/
|
||||
docStructure(slot: number): AddonQtypeDdMarkerQuestionDocStructure {
|
||||
const topNode = <HTMLElement> this.container.querySelector('#core-question-' + slot + ' .addon-qtype-ddmarker-container'),
|
||||
dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems');
|
||||
|
||||
return {
|
||||
topNode: (): HTMLElement => {
|
||||
return topNode;
|
||||
},
|
||||
bgImg: (): HTMLImageElement => {
|
||||
return <HTMLImageElement> topNode.querySelector('.dropbackground');
|
||||
},
|
||||
dragItemsArea: (): HTMLElement => {
|
||||
return dragItemsArea;
|
||||
},
|
||||
dragItems: (): HTMLElement[] => {
|
||||
return <HTMLElement[]> Array.from(dragItemsArea.querySelectorAll('.dragitem'));
|
||||
},
|
||||
dragItemsForChoice: (choiceNo: number): HTMLElement[] => {
|
||||
return <HTMLElement[]> Array.from(dragItemsArea.querySelectorAll('span.dragitem.choice' + choiceNo));
|
||||
},
|
||||
dragItemForChoice: (choiceNo: number, itemNo: number): HTMLElement => {
|
||||
return <HTMLElement> dragItemsArea.querySelector('span.dragitem.choice' + choiceNo + '.item' + itemNo);
|
||||
},
|
||||
dragItemBeingDragged: (choiceNo: number): HTMLElement => {
|
||||
return <HTMLElement> dragItemsArea.querySelector('span.dragitem.beingdragged.choice' + choiceNo);
|
||||
},
|
||||
dragItemHome: (choiceNo: number): HTMLElement => {
|
||||
return <HTMLElement> dragItemsArea.querySelector('span.draghome.choice' + choiceNo);
|
||||
},
|
||||
dragItemHomes: (): HTMLElement[] => {
|
||||
return <HTMLElement[]> Array.from(dragItemsArea.querySelectorAll('span.draghome'));
|
||||
},
|
||||
getClassnameNumericSuffix: (node: HTMLElement, prefix: string): number => {
|
||||
|
||||
if (node.classList && node.classList.length) {
|
||||
const patt1 = new RegExp('^' + prefix + '([0-9])+$'),
|
||||
patt2 = new RegExp('([0-9])+$');
|
||||
|
||||
for (let index = 0; index < node.classList.length; index++) {
|
||||
if (patt1.test(node.classList[index])) {
|
||||
const match = patt2.exec(node.classList[index]);
|
||||
|
||||
return Number(match[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.warn('Prefix "' + prefix + '" not found in class names.');
|
||||
},
|
||||
inputsForChoices: (): HTMLElement[] => {
|
||||
return <HTMLElement[]> Array.from(topNode.querySelectorAll('input.choices'));
|
||||
},
|
||||
inputForChoice: (choiceNo: number): HTMLElement => {
|
||||
return <HTMLElement> topNode.querySelector('input.choice' + choiceNo);
|
||||
},
|
||||
markerTexts: (): HTMLElement => {
|
||||
return <HTMLElement> topNode.querySelector('div.markertexts');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an element "draggable". In the mobile app, items are "dragged" using tap and drop.
|
||||
*
|
||||
* @param {HTMLElement} drag Element.
|
||||
*/
|
||||
draggable(drag: HTMLElement): void {
|
||||
drag.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const dragging = this.selected;
|
||||
if (dragging && !drag.classList.contains('unplaced')) {
|
||||
|
||||
const position = this.domUtils.getElementXY(drag, null, 'ddarea'),
|
||||
bgImg = this.doc.bgImg(),
|
||||
bgImgPos = this.domUtils.getElementXY(bgImg, null, 'ddarea');
|
||||
|
||||
position[0] = position[0] - bgImgPos[0] + e.offsetX;
|
||||
position[1] = position[1] - bgImgPos[1] + e.offsetY;
|
||||
|
||||
// Ensure the we click on a placed dragitem.
|
||||
if (position[0] <= bgImg.width && position[1] <= bgImg.height) {
|
||||
this.deselectDrags();
|
||||
this.dropDrag(dragging, position);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (drag.classList.contains('beingdragged')) {
|
||||
this.deselectDrags();
|
||||
} else {
|
||||
this.selectDrag(drag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coordinates of the drag home of a certain choice.
|
||||
*
|
||||
* @param {number} choiceNo Choice number.
|
||||
* @return {number[]} Coordinates.
|
||||
*/
|
||||
dragHomeXY(choiceNo: number): number[] {
|
||||
const dragItemHome = this.doc.dragItemHome(choiceNo),
|
||||
position = this.domUtils.getElementXY(dragItemHome, null, 'ddarea');
|
||||
|
||||
return [position[0], position[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a drop zone.
|
||||
*
|
||||
* @param {number} dropZoneNo Number of the drop zone.
|
||||
* @param {string} markerText The marker text to set.
|
||||
* @param {string} shape Name of the shape of the drop zone (circle, rectangle, polygon).
|
||||
* @param {string} coords Coordinates of the shape.
|
||||
* @param {string} colour Colour of the shape.
|
||||
* @param {boolean} link Whether the marker should have a link in it.
|
||||
*/
|
||||
drawDropZone(dropZoneNo: number, markerText: string, shape: string, coords: string, colour: string, link: boolean): void {
|
||||
let existingMarkerText: HTMLElement;
|
||||
|
||||
const markerTexts = this.doc.markerTexts();
|
||||
// Check if there is already a marker text for this drop zone.
|
||||
if (link) {
|
||||
existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo + ' a');
|
||||
} else {
|
||||
existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo);
|
||||
}
|
||||
|
||||
if (existingMarkerText) {
|
||||
// Marker text already exists. Update it or remove it if empty.
|
||||
if (markerText !== '') {
|
||||
existingMarkerText.innerHTML = markerText;
|
||||
} else {
|
||||
existingMarkerText.remove();
|
||||
}
|
||||
} else if (markerText !== '') {
|
||||
// Create and add the marker text.
|
||||
const classNames = 'markertext markertext' + dropZoneNo,
|
||||
span = document.createElement('span');
|
||||
|
||||
span.className = classNames;
|
||||
|
||||
if (link) {
|
||||
span.innerHTML = '<a href="#">' + markerText + '</a>';
|
||||
} else {
|
||||
span.innerHTML = markerText;
|
||||
}
|
||||
|
||||
markerTexts.appendChild(span);
|
||||
}
|
||||
|
||||
// Check that a function to draw this shape exists.
|
||||
const drawFunc = 'drawShape' + this.textUtils.ucFirst(shape);
|
||||
if (this[drawFunc] instanceof Function) {
|
||||
|
||||
// Call the function.
|
||||
const xyForText = this[drawFunc](dropZoneNo, coords, colour);
|
||||
if (xyForText !== null) {
|
||||
|
||||
// Search the marker for the drop zone.
|
||||
const markerSpan = <HTMLElement> 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;
|
||||
|
||||
markerSpan.style.opacity = '0.6';
|
||||
markerSpan.style.left = xyForText[0] + 'px';
|
||||
markerSpan.style.top = xyForText[1] + 'px';
|
||||
|
||||
const markerSpanAnchor = markerSpan.querySelector('a');
|
||||
if (markerSpanAnchor !== null) {
|
||||
|
||||
markerSpanAnchor.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.shapes.forEach((elem) => {
|
||||
elem.css('fill-opacity', 0.5);
|
||||
});
|
||||
|
||||
this.shapes[dropZoneNo].css('fill-opacity', 1);
|
||||
setTimeout(() => {
|
||||
this.shapes[dropZoneNo].css('fill-opacity', 0.5);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
markerSpanAnchor.setAttribute('tabIndex', '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a circle in a drop zone.
|
||||
*
|
||||
* @param {number} dropZoneNo Number of the drop zone.
|
||||
* @param {string} coords Coordinates of the circle.
|
||||
* @param {string} colour Colour of the circle.
|
||||
* @return {number[]} 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+)/);
|
||||
|
||||
if (coordsParts && coordsParts.length === 4) {
|
||||
// Remove first element and convert them to number.
|
||||
coordsParts.shift();
|
||||
|
||||
const coordsPartsNum = coordsParts.map((i) => {
|
||||
return Number(i);
|
||||
});
|
||||
|
||||
// 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 null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a rectangle in a drop zone.
|
||||
*
|
||||
* @param {number} dropZoneNo Number of the drop zone.
|
||||
* @param {string} coords Coordinates of the rectangle.
|
||||
* @param {string} colour Colour of the rectangle.
|
||||
* @return {number[]} 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+)/);
|
||||
|
||||
if (coordsParts && coordsParts.length === 5) {
|
||||
// Remove first element and convert them to number.
|
||||
coordsParts.shift();
|
||||
|
||||
const coordsPartsNum = coordsParts.map((i) => {
|
||||
return Number(i);
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
// Return the center.
|
||||
return [coordsPartsNum[0] + coordsPartsNum[2] / 2, coordsPartsNum[1] + coordsPartsNum[3] / 2];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a polygon in a drop zone.
|
||||
*
|
||||
* @param {number} dropZoneNo Number of the drop zone.
|
||||
* @param {string} coords Coordinates of the polygon.
|
||||
* @param {string} colour Colour of the polygon.
|
||||
* @return {number[]} 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]);
|
||||
}
|
||||
}
|
||||
|
||||
if (points.length > 2) {
|
||||
this.shapes[dropZoneNo] = this.graphics.addShape({
|
||||
type: 'polygon',
|
||||
color: colour
|
||||
}, {
|
||||
points: points.join(' ')
|
||||
});
|
||||
|
||||
// Return the center.
|
||||
return [(minXY[0] + maxXY[0]) / 2, (minXY[1] + maxXY[1]) / 2];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a drag element into a certain position.
|
||||
*
|
||||
* @param {HTMLElement} drag The element to drop.
|
||||
* @param {number[]} position Position to drop to (X, Y).
|
||||
*/
|
||||
dropDrag(drag: HTMLElement, position: number[]): void {
|
||||
const choiceNo = this.getChoiceNoForNode(drag);
|
||||
|
||||
if (position) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
this.saveAllXYForChoice(choiceNo, drag, position);
|
||||
this.redrawDragsAndDrops();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which drag items need to be shown and return coords of all drag items except any that are currently being
|
||||
* dragged based on contents of hidden inputs and whether drags are 'infinite' or how many drags should be shown.
|
||||
*
|
||||
* @param {HTMLElement} input The input element.
|
||||
* @return {number[][]} List of coordinates.
|
||||
*/
|
||||
getCoords(input: HTMLElement): number[][] {
|
||||
const choiceNo = this.getChoiceNoForNode(input),
|
||||
fv = input.getAttribute('value'),
|
||||
infinite = input.classList.contains('infinite'),
|
||||
noOfDrags = this.getNoOfDragsForNode(input),
|
||||
dragging = (this.doc.dragItemBeingDragged(choiceNo) !== null),
|
||||
coords: number[][] = [];
|
||||
|
||||
if (fv !== '' && typeof fv != 'undefined') {
|
||||
// Get all the coordinates in the input and add them to the coords list.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const displayedDrags = coords.length + (dragging ? 1 : 0);
|
||||
if (infinite || (displayedDrags < noOfDrags)) {
|
||||
coords[coords.length] = this.dragHomeXY(choiceNo);
|
||||
}
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the choice number from an HTML element.
|
||||
*
|
||||
* @param {HTMLElement} node Element to check.
|
||||
* @return {number} Choice number.
|
||||
*/
|
||||
getChoiceNoForNode(node: HTMLElement): number {
|
||||
return Number(this.doc.getClassnameNumericSuffix(node, 'choice'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coordinates (X, Y) of a draggable element.
|
||||
*
|
||||
* @param {HTMLElement} dragItem The draggable item.
|
||||
* @return {number[]} Coordinates.
|
||||
*/
|
||||
getDragXY(dragItem: HTMLElement): number[] {
|
||||
const position = this.domUtils.getElementXY(dragItem, null, 'ddarea'),
|
||||
bgImg = this.doc.bgImg(),
|
||||
bgImgXY = this.domUtils.getElementXY(bgImg, null, 'ddarea');
|
||||
|
||||
position[0] -= bgImgXY[0];
|
||||
position[1] -= bgImgXY[1];
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item number from an HTML element.
|
||||
*
|
||||
* @param {HTMLElement} node Element to check.
|
||||
* @return {number} Choice number.
|
||||
*/
|
||||
getItemNoForNode(node: HTMLElement): number {
|
||||
return Number(this.doc.getClassnameNumericSuffix(node, 'item'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next colour.
|
||||
*
|
||||
* @return {string} Colour.
|
||||
*/
|
||||
getNextColour(): string {
|
||||
const colour = this.COLOURS[this.nextColourIndex];
|
||||
this.nextColourIndex++;
|
||||
|
||||
// If we reached the end of the list, start again.
|
||||
if (this.nextColourIndex === this.COLOURS.length) {
|
||||
this.nextColourIndex = 0;
|
||||
}
|
||||
|
||||
return colour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of drags from an HTML element.
|
||||
*
|
||||
* @param {HTMLElement} node Element to check.
|
||||
* @return {number} Choice number.
|
||||
*/
|
||||
getNoOfDragsForNode(node: HTMLElement): number {
|
||||
return Number(this.doc.getClassnameNumericSuffix(node, 'noofdrags'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the question.
|
||||
*
|
||||
* @param {any} question Question.
|
||||
*/
|
||||
initializer(question: any): void {
|
||||
this.doc = this.docStructure(question.slot);
|
||||
|
||||
// Wait the DOM to be rendered.
|
||||
setTimeout(() => {
|
||||
this.pollForImageLoad();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make background image and home zone dropable.
|
||||
*/
|
||||
makeImageDropable(): void {
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for click events in the background image to make it dropable.
|
||||
const bgImg = this.doc.bgImg();
|
||||
bgImg.addEventListener('click', (e) => {
|
||||
|
||||
const drag = this.selected;
|
||||
if (!drag) {
|
||||
// No draggable element selected, nothing to do.
|
||||
return false;
|
||||
}
|
||||
|
||||
// There's an element being dragged. Deselect it and drop it in the position.
|
||||
const position = [e.offsetX, e.offsetY];
|
||||
this.deselectDrags();
|
||||
this.dropDrag(drag, position);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
const home = this.doc.dragItemsArea();
|
||||
home.addEventListener('click', (e) => {
|
||||
|
||||
const drag = this.selected;
|
||||
if (!drag) {
|
||||
// No draggable element selected, nothing to do.
|
||||
return false;
|
||||
}
|
||||
|
||||
// There's an element being dragged but it's not placed yet, deselect.
|
||||
if (drag.classList.contains('unplaced')) {
|
||||
this.deselectDrags();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// There's an element being dragged and it's placed somewhere. Move it back to the home area.
|
||||
this.deselectDrags();
|
||||
this.dropDrag(drag, null);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the background image to be loaded.
|
||||
*/
|
||||
pollForImageLoad(): void {
|
||||
if (this.afterImageLoadDone) {
|
||||
// Already treated.
|
||||
return;
|
||||
}
|
||||
|
||||
const bgImg = this.doc.bgImg(),
|
||||
imgLoaded = (): void => {
|
||||
bgImg.removeEventListener('load', imgLoaded);
|
||||
|
||||
this.makeImageDropable();
|
||||
|
||||
setTimeout(() => {
|
||||
this.redrawDragsAndDrops();
|
||||
});
|
||||
|
||||
this.afterImageLoadDone = true;
|
||||
this.question.loaded = true;
|
||||
};
|
||||
|
||||
bgImg.addEventListener('load', imgLoaded);
|
||||
|
||||
// Try again after a while.
|
||||
setTimeout(() => {
|
||||
this.pollForImageLoad();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
redrawDragsAndDrops(): void {
|
||||
// Mark all the draggable items as not placed.
|
||||
const drags = this.doc.dragItems();
|
||||
drags.forEach((drag) => {
|
||||
drag.classList.add('unneeded', 'unplaced');
|
||||
});
|
||||
|
||||
// Re-calculate the image proportion.
|
||||
this.calculateImgProportion();
|
||||
|
||||
// Get all the inputs.
|
||||
const inputs = this.doc.inputsForChoices();
|
||||
for (let x = 0; x < inputs.length; x++) {
|
||||
|
||||
// Get all the drag items for the choice.
|
||||
const input = inputs[x],
|
||||
choiceNo = this.getChoiceNoForNode(input),
|
||||
coords = this.getCoords(input),
|
||||
dragItemHome = this.doc.dragItemHome(choiceNo),
|
||||
homePosition = this.dragHomeXY(choiceNo);
|
||||
|
||||
for (let i = 0; i < coords.length; i++) {
|
||||
let dragItem = this.doc.dragItemForChoice(choiceNo, i);
|
||||
|
||||
if (!dragItem || dragItem.classList.contains('beingdragged')) {
|
||||
dragItem = this.cloneNewDragItem(dragItemHome, i);
|
||||
} else {
|
||||
dragItem.classList.remove('unneeded');
|
||||
}
|
||||
|
||||
// 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.style.left = coords[i][0] + 'px';
|
||||
dragItem.style.top = coords[i][1] + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unneeded draggable items.
|
||||
for (let y = 0; y < drags.length; y++) {
|
||||
const item = drags[y];
|
||||
if (item.classList.contains('unneeded') && !item.classList.contains('beingdragged')) {
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Re-draw drop zones.
|
||||
if (this.dropZones.length !== 0) {
|
||||
this.graphics.clear();
|
||||
this.restartColours();
|
||||
|
||||
for (const dropZoneNo in this.dropZones) {
|
||||
const colourForDropZone = this.getNextColour(),
|
||||
dropZone = this.dropZones[dropZoneNo],
|
||||
dzNo = Number(dropZoneNo);
|
||||
|
||||
this.drawDropZone(dzNo, dropZone.markerText, dropZone.shape, dropZone.coords, colourForDropZone, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the coordinates stored for a choice.
|
||||
*
|
||||
* @param {number} choiceNo Choice number.
|
||||
*/
|
||||
resetDragXY(choiceNo: number): void {
|
||||
this.setFormValue(choiceNo, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call when the window is resized.
|
||||
*/
|
||||
resizeFunction(): void {
|
||||
this.redrawDragsAndDrops();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the colour index.
|
||||
*/
|
||||
restartColours(): void {
|
||||
this.nextColourIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all the coordinates of a choice into the right input.
|
||||
*
|
||||
* @param {number} choiceNo Number of the choice.
|
||||
* @param {HTMLElement} dropped Element being dropped.
|
||||
* @param {number[]} position Position where the element is dropped.
|
||||
*/
|
||||
saveAllXYForChoice(choiceNo: number, dropped: HTMLElement, position: number[]): void {
|
||||
const coords = [];
|
||||
let bgImgXY;
|
||||
|
||||
// Calculate the coords for the choice.
|
||||
const dragItemsChoice = this.doc.dragItemsForChoice(choiceNo);
|
||||
for (let i = 0; i < dragItemsChoice.length; i++) {
|
||||
|
||||
const dragItem = this.doc.dragItemForChoice(choiceNo, i);
|
||||
if (dragItem) {
|
||||
|
||||
dragItem.classList.remove('item' + i);
|
||||
bgImgXY = this.getDragXY(dragItem);
|
||||
dragItem.classList.add('item' + coords.length);
|
||||
coords.push(bgImgXY);
|
||||
}
|
||||
}
|
||||
|
||||
if (position !== null) {
|
||||
// Element dropped into a certain position. Mark it as placed and save the position.
|
||||
dropped.classList.remove('unplaced');
|
||||
dropped.classList.add('item' + coords.length);
|
||||
coords.push(position);
|
||||
} else {
|
||||
// Element back at home, mark it as unplaced.
|
||||
dropped.classList.add('unplaced');
|
||||
}
|
||||
|
||||
if (coords.length > 0) {
|
||||
// Save the coordinates in the input.
|
||||
this.setFormValue(choiceNo, coords.join(';'));
|
||||
} else {
|
||||
// Empty the input.
|
||||
this.resetDragXY(choiceNo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a certain value in the input of a choice.
|
||||
*
|
||||
* @param {number} choiceNo Choice number.
|
||||
* @param {string} value The value to set.
|
||||
*/
|
||||
setFormValue(choiceNo: number, value: string): void {
|
||||
this.doc.inputForChoice(choiceNo).setAttribute('value', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a draggable element.
|
||||
*
|
||||
* @param {HTMLElement} drag Element.
|
||||
*/
|
||||
selectDrag(drag: HTMLElement): void {
|
||||
// Deselect previous drags.
|
||||
this.deselectDrags();
|
||||
|
||||
this.selected = drag;
|
||||
drag.classList.add('beingdragged');
|
||||
|
||||
const itemNo = this.getItemNoForNode(drag);
|
||||
if (itemNo !== null) {
|
||||
drag.classList.remove('item' + itemNo);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { AddonQtypeDdMarkerQuestion } from './ddmarker';
|
||||
|
||||
/**
|
||||
* Graphics API for drag-and-drop markers question type.
|
||||
*/
|
||||
export class AddonQtypeDdMarkerGraphicsApi {
|
||||
|
||||
protected NS = 'http://www.w3.org/2000/svg';
|
||||
protected dropZone: SVGSVGElement;
|
||||
|
||||
/**
|
||||
* Create the instance.
|
||||
*
|
||||
* @param {AddonQtypeDdMarkerQuestion} instance Question instance.
|
||||
* @param {CoreDomUtilsProvider} domUtils Dom Utils provider.
|
||||
*/
|
||||
constructor(protected instance: AddonQtypeDdMarkerQuestion, protected domUtils: CoreDomUtilsProvider) { }
|
||||
|
||||
/**
|
||||
* Add a shape.
|
||||
*
|
||||
* @param {{type: string, color: string}} shapeAttribs Attributes for the shape: type and color.
|
||||
* @param {{[name: string]: number|string} styles Object with the styles for the shape (name -> value).
|
||||
* @return {Element} The new shape.
|
||||
*/
|
||||
addShape(shapeAttribs: {type: string, color: string}, styles: {[name: string]: number | string}): Element {
|
||||
const shape = document.createElementNS(this.NS, shapeAttribs.type);
|
||||
shape.setAttribute('fill', shapeAttribs.color);
|
||||
shape.setAttribute('fill-opacity', '0.5');
|
||||
shape.setAttribute('stroke', 'black');
|
||||
|
||||
for (const x in styles) {
|
||||
shape.setAttribute(x, String(styles[x]));
|
||||
}
|
||||
|
||||
this.dropZone.appendChild(shape);
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the shapes.
|
||||
*/
|
||||
clear(): void {
|
||||
const bgImg = this.instance.doc.bgImg(),
|
||||
position = this.domUtils.getElementXY(bgImg, null, 'ddarea'),
|
||||
dropZones = <HTMLElement> this.instance.doc.topNode().querySelector('div.ddarea div.dropzones');
|
||||
|
||||
dropZones.style.left = position[0] + 'px';
|
||||
dropZones.style.top = position[1] + 'px';
|
||||
dropZones.style.width = bgImg.width + 'px';
|
||||
dropZones.style.height = bgImg.height + 'px';
|
||||
|
||||
const markerTexts = this.instance.doc.markerTexts();
|
||||
markerTexts.style.left = position[0] + 'px';
|
||||
markerTexts.style.top = position[1] + 'px';
|
||||
markerTexts.style.width = bgImg.width + 'px';
|
||||
markerTexts.style.height = bgImg.height + 'px';
|
||||
|
||||
if (!this.dropZone) {
|
||||
this.dropZone = <SVGSVGElement> document.createElementNS(this.NS, 'svg');
|
||||
dropZones.appendChild(this.dropZone);
|
||||
} else {
|
||||
// Remove all children.
|
||||
while (this.dropZone.firstChild) {
|
||||
this.dropZone.removeChild(this.dropZone.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
this.dropZone.style.width = bgImg.width + 'px';
|
||||
this.dropZone.style.height = bgImg.height + 'px';
|
||||
|
||||
this.instance.shapes = [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<section ion-list *ngIf="question.text || question.text === ''" class="addon-qtype-ddmarker-container">
|
||||
<!-- Content is outside the core-loading to let the script calculate drag items position -->
|
||||
<core-loading [hideUntil]="question.loaded"></core-loading>
|
||||
|
||||
<ion-item text-wrap [ngClass]="{invisible: !question.loaded}">
|
||||
<p *ngIf="!question.readOnly" class="core-info-card-icon">
|
||||
<ion-icon name="information" item-start></ion-icon>
|
||||
{{ 'core.question.howtodraganddrop' | translate }}
|
||||
</p>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea"></core-format-text>
|
||||
</ion-item>
|
||||
</section>
|
|
@ -0,0 +1,100 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, AfterViewInit, Injector, ElementRef } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
|
||||
|
||||
/**
|
||||
* Component to render a drag-and-drop markers question.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-qtype-ddmarker',
|
||||
templateUrl: 'ddmarker.html'
|
||||
})
|
||||
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
protected element: HTMLElement;
|
||||
protected questionInstance: AddonQtypeDdMarkerQuestion;
|
||||
protected dropZones: any[]; // The drop zones received in the init object of the question.
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||
super(logger, 'AddonQtypeDdMarkerComponent', injector);
|
||||
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (!this.question) {
|
||||
this.logger.warn('Aborting because of no question received.');
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = this.question.html;
|
||||
|
||||
// Get D&D area, form and question text.
|
||||
const ddArea = div.querySelector('.ddarea'),
|
||||
ddForm = div.querySelector('.ddform');
|
||||
|
||||
this.question.text = this.domUtils.getContentsOfElement(div, '.qtext');
|
||||
if (!ddArea || !ddForm || typeof this.question.text == 'undefined') {
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
// Build the D&D area HTML.
|
||||
this.question.ddArea = ddArea.outerHTML;
|
||||
|
||||
const wrongParts = div.querySelector('.wrongparts');
|
||||
if (wrongParts) {
|
||||
this.question.ddArea += wrongParts.outerHTML;
|
||||
}
|
||||
this.question.ddArea += ddForm.outerHTML;
|
||||
this.question.readOnly = false;
|
||||
|
||||
if (this.question.initObjects) {
|
||||
if (typeof this.question.initObjects.dropzones != 'undefined') {
|
||||
this.dropZones = this.question.initObjects.dropzones;
|
||||
}
|
||||
if (typeof this.question.initObjects.readonly != 'undefined') {
|
||||
this.question.readOnly = this.question.initObjects.readonly;
|
||||
}
|
||||
}
|
||||
|
||||
this.question.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* View has been initialized.
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdMarkerQuestion(this.logger, this.domUtils, this.textUtils, this.element,
|
||||
this.question, this.question.readOnly, this.dropZones);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.questionInstance && this.questionInstance.destroy();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonQtypeDdMarkerHandler } from './providers/handler';
|
||||
import { AddonQtypeDdMarkerComponent } from './component/ddmarker';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonQtypeDdMarkerComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeDdMarkerHandler
|
||||
],
|
||||
exports: [
|
||||
AddonQtypeDdMarkerComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonQtypeDdMarkerComponent
|
||||
]
|
||||
})
|
||||
export class AddonQtypeDdMarkerModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeDdMarkerHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { CoreQuestionProvider } from '@core/question/providers/question';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeDdMarkerComponent } from '../component/ddmarker';
|
||||
|
||||
/**
|
||||
* Handler to support drag-and-drop markers question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeDdMarker';
|
||||
type = 'qtype_ddmarker';
|
||||
|
||||
constructor(private questionProvider: CoreQuestionProvider) { }
|
||||
|
||||
/**
|
||||
* Return the name of the behaviour to use for the question.
|
||||
* If the question should use the default behaviour you shouldn't implement this function.
|
||||
*
|
||||
* @param {any} question The question.
|
||||
* @param {string} behaviour The default behaviour.
|
||||
* @return {string} The behaviour to use.
|
||||
*/
|
||||
getBehaviour(question: any, behaviour: string): string {
|
||||
if (behaviour === 'interactive') {
|
||||
return 'interactivecountback';
|
||||
}
|
||||
|
||||
return behaviour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the question.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @param {Injector} injector Injector.
|
||||
* @param {any} question The question to render.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(injector: Injector, question: any): any | Promise<any> {
|
||||
return AddonQtypeDdMarkerComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a response is complete.
|
||||
*
|
||||
* @param {any} question The question.
|
||||
* @param {any} answers Object with the question answers (without prefix).
|
||||
* @return {number} 1 if complete, 0 if not complete, -1 if cannot determine.
|
||||
*/
|
||||
isCompleteResponse(question: any, answers: any): number {
|
||||
// If 1 dragitem is set we assume the answer is complete (like Moodle does).
|
||||
for (const name in answers) {
|
||||
if (answers[name]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a student has provided enough of an answer for the question to be graded automatically,
|
||||
* or whether it must be considered aborted.
|
||||
*
|
||||
* @param {any} question The question.
|
||||
* @param {any} answers Object with the question answers (without prefix).
|
||||
* @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine.
|
||||
*/
|
||||
isGradableResponse(question: any, answers: any): number {
|
||||
return this.isCompleteResponse(question, answers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two responses are the same.
|
||||
*
|
||||
* @param {any} question Question.
|
||||
* @param {any} prevAnswers Object with the previous question answers.
|
||||
* @param {any} newAnswers Object with the new question answers.
|
||||
* @return {boolean} Whether they're the same.
|
||||
*/
|
||||
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
|
||||
return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
|
|||
import { AddonQtypeCalculatedModule } from './calculated/calculated.module';
|
||||
import { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module';
|
||||
import { AddonQtypeCalculatedSimpleModule } from './calculatedsimple/calculatedsimple.module';
|
||||
import { AddonQtypeDdMarkerModule } from './ddmarker/ddmarker.module';
|
||||
import { AddonQtypeDdwtosModule } from './ddwtos/ddwtos.module';
|
||||
import { AddonQtypeDescriptionModule } from './description/description.module';
|
||||
import { AddonQtypeEssayModule } from './essay/essay.module';
|
||||
|
@ -34,6 +35,7 @@ import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
|
|||
AddonQtypeCalculatedModule,
|
||||
AddonQtypeCalculatedMultiModule,
|
||||
AddonQtypeCalculatedSimpleModule,
|
||||
AddonQtypeDdMarkerModule,
|
||||
AddonQtypeDdwtosModule,
|
||||
AddonQtypeDescriptionModule,
|
||||
AddonQtypeEssayModule,
|
||||
|
|
Loading…
Reference in New Issue