MOBILE-2390 qtype: Implement ddwtos type
parent
cd68db376a
commit
d14d700f7b
|
@ -0,0 +1,550 @@
|
||||||
|
// (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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of functions to get the CSS selectors.
|
||||||
|
*/
|
||||||
|
export interface AddonQtypeDdwtosQuestionCSSSelectors {
|
||||||
|
topNode?: () => string;
|
||||||
|
dragContainer?: () => string;
|
||||||
|
drags?: () => string;
|
||||||
|
drag?: (no: number) => string;
|
||||||
|
dragsInGroup?: (groupNo: number) => string;
|
||||||
|
unplacedDragsInGroup?: (groupNo: number) => string;
|
||||||
|
dragsForChoiceInGroup?: (choiceNo: number, groupNo: number) => string;
|
||||||
|
unplacedDragsForChoiceInGroup?: (choiceNo: number, groupNo: number) => string;
|
||||||
|
drops?: () => string;
|
||||||
|
dropForPlace?: (placeNo: number) => string;
|
||||||
|
dropsInGroup?: (groupNo: number) => string;
|
||||||
|
dragHomes?: () => string;
|
||||||
|
dragHomesGroup?: (groupNo: number) => string;
|
||||||
|
dragHome?: (groupNo: number, choiceNo: number) => string;
|
||||||
|
dropsGroup?: (groupNo: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to make a question of ddwtos type work.
|
||||||
|
*/
|
||||||
|
export class AddonQtypeDdwtosQuestion {
|
||||||
|
|
||||||
|
protected logger: any;
|
||||||
|
protected nextDragItemNo = 1;
|
||||||
|
protected selectors: AddonQtypeDdwtosQuestionCSSSelectors; // Result of cssSelectors.
|
||||||
|
protected placed: {[no: number]: number}; // Map that relates drag elements numbers with drop zones numbers.
|
||||||
|
protected selected: HTMLElement; // Selected element (being "dragged").
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the instance.
|
||||||
|
*
|
||||||
|
* @param {CoreLoggerProvider} logger Logger provider.
|
||||||
|
* @param {CoreDomUtilsProvider} domUtils Dom 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 {string[]} inputIds Ids of the inputs of the question (where the answers will be stored).
|
||||||
|
*/
|
||||||
|
constructor(logger: CoreLoggerProvider, protected domUtils: CoreDomUtilsProvider, protected container: HTMLElement,
|
||||||
|
protected question: any, protected readOnly: boolean, protected inputIds: string[]) {
|
||||||
|
this.logger = logger.getInstance('AddonQtypeDdwtosQuestion');
|
||||||
|
|
||||||
|
this.initializer(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone a drag item and add it to the drag container.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} dragHome Item to clone
|
||||||
|
*/
|
||||||
|
cloneDragItem(dragHome: HTMLElement): void {
|
||||||
|
const drag = <HTMLElement> dragHome.cloneNode(true);
|
||||||
|
|
||||||
|
drag.classList.remove('draghome');
|
||||||
|
drag.classList.add('drag');
|
||||||
|
drag.classList.add('no' + this.nextDragItemNo);
|
||||||
|
this.nextDragItemNo++;
|
||||||
|
|
||||||
|
drag.style.visibility = 'visible';
|
||||||
|
drag.style.position = 'absolute';
|
||||||
|
|
||||||
|
const container = this.container.querySelector(this.selectors.dragContainer());
|
||||||
|
container.appendChild(drag);
|
||||||
|
|
||||||
|
if (!this.readOnly) {
|
||||||
|
this.makeDraggable(drag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone the 'drag homes'.
|
||||||
|
* Invisible 'drag homes' are output in the question. These have the same properties as the drag items but are invisible.
|
||||||
|
* We clone these invisible elements to make the actual drag items.
|
||||||
|
*/
|
||||||
|
cloneDragItems(): void {
|
||||||
|
const dragHomes = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dragHomes()));
|
||||||
|
for (let x = 0; x < dragHomes.length; x++) {
|
||||||
|
this.cloneDragItemsForOneChoice(dragHomes[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone a certain 'drag home'. If it's an "infinite" drag, clone it several times.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} dragHome Element to clone.
|
||||||
|
*/
|
||||||
|
cloneDragItemsForOneChoice(dragHome: HTMLElement): void {
|
||||||
|
if (dragHome.classList.contains('infinite')) {
|
||||||
|
const groupNo = this.getGroup(dragHome),
|
||||||
|
noOfDrags = this.container.querySelectorAll(this.selectors.dropsInGroup(groupNo)).length;
|
||||||
|
|
||||||
|
for (let x = 0; x < noOfDrags; x++) {
|
||||||
|
this.cloneDragItem(dragHome);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.cloneDragItem(dragHome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an object with a set of functions to get the CSS selectors.
|
||||||
|
*
|
||||||
|
* @param {number} slot Question slot.
|
||||||
|
* @return {AddonQtypeDdwtosQuestionCSSSelectors} Object with the functions to get the selectors.
|
||||||
|
*/
|
||||||
|
cssSelectors(slot: number): AddonQtypeDdwtosQuestionCSSSelectors {
|
||||||
|
const topNode = '#core-question-' + slot + ' .addon-qtype-ddwtos-container',
|
||||||
|
selectors: AddonQtypeDdwtosQuestionCSSSelectors = {};
|
||||||
|
|
||||||
|
selectors.topNode = (): string => {
|
||||||
|
return topNode;
|
||||||
|
};
|
||||||
|
selectors.dragContainer = (): string => {
|
||||||
|
return topNode + ' div.drags';
|
||||||
|
};
|
||||||
|
selectors.drags = (): string => {
|
||||||
|
return selectors.dragContainer() + ' span.drag';
|
||||||
|
};
|
||||||
|
selectors.drag = (no: number): string => {
|
||||||
|
return selectors.drags() + '.no' + no;
|
||||||
|
};
|
||||||
|
selectors.dragsInGroup = (groupNo: number): string => {
|
||||||
|
return selectors.drags() + '.group' + groupNo;
|
||||||
|
};
|
||||||
|
selectors.unplacedDragsInGroup = (groupNo: number): string => {
|
||||||
|
return selectors.dragsInGroup(groupNo) + '.unplaced';
|
||||||
|
};
|
||||||
|
selectors.dragsForChoiceInGroup = (choiceNo: number, groupNo: number): string => {
|
||||||
|
return selectors.dragsInGroup(groupNo) + '.choice' + choiceNo;
|
||||||
|
};
|
||||||
|
selectors.unplacedDragsForChoiceInGroup = (choiceNo: number, groupNo: number): string => {
|
||||||
|
return selectors.unplacedDragsInGroup(groupNo) + '.choice' + choiceNo;
|
||||||
|
};
|
||||||
|
selectors.drops = (): string => {
|
||||||
|
return topNode + ' span.drop';
|
||||||
|
};
|
||||||
|
selectors.dropForPlace = (placeNo: number): string => {
|
||||||
|
return selectors.drops() + '.place' + placeNo;
|
||||||
|
};
|
||||||
|
selectors.dropsInGroup = (groupNo: number): string => {
|
||||||
|
return selectors.drops() + '.group' + groupNo;
|
||||||
|
};
|
||||||
|
selectors.dragHomes = (): string => {
|
||||||
|
return topNode + ' span.draghome';
|
||||||
|
};
|
||||||
|
selectors.dragHomesGroup = (groupNo: number): string => {
|
||||||
|
return topNode + ' .draggrouphomes' + groupNo + ' span.draghome';
|
||||||
|
};
|
||||||
|
selectors.dragHome = (groupNo: number, choiceNo: number): string => {
|
||||||
|
return topNode + ' .draggrouphomes' + groupNo + ' span.draghome.choice' + choiceNo;
|
||||||
|
};
|
||||||
|
selectors.dropsGroup = (groupNo: number): string => {
|
||||||
|
return topNode + ' span.drop.group' + groupNo;
|
||||||
|
};
|
||||||
|
|
||||||
|
return selectors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselect all drags.
|
||||||
|
*/
|
||||||
|
deselectDrags(): void {
|
||||||
|
// Remove the selected class from all drags.
|
||||||
|
const drags = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drags()));
|
||||||
|
drags.forEach((drag) => {
|
||||||
|
drag.classList.remove('selected');
|
||||||
|
});
|
||||||
|
this.selected = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to call when the instance is no longer needed.
|
||||||
|
*/
|
||||||
|
destroy(): void {
|
||||||
|
window.removeEventListener('resize', this.resizeFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the choice number of an element. It is extracted from the classes.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} node Element to check.
|
||||||
|
* @return {number} Choice number.
|
||||||
|
*/
|
||||||
|
getChoice(node: HTMLElement): number {
|
||||||
|
return this.getClassnameNumericSuffix(node, 'choice');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number in a certain class name of an element.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} node The element to check.
|
||||||
|
* @param {string} prefix Prefix of the class to check.
|
||||||
|
* @return {number} The number in the class.
|
||||||
|
*/
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the group number of an element. It is extracted from the classes.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} node Element to check.
|
||||||
|
* @return {number} Group number.
|
||||||
|
*/
|
||||||
|
getGroup(node: HTMLElement): number {
|
||||||
|
return this.getClassnameNumericSuffix(node, 'group');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of an element ('no'). It is extracted from the classes.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} node Element to check.
|
||||||
|
* @return {number} Number.
|
||||||
|
*/
|
||||||
|
getNo(node: HTMLElement): number {
|
||||||
|
return this.getClassnameNumericSuffix(node, 'no');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the place number of an element. It is extracted from the classes.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} node Element to check.
|
||||||
|
* @return {number} Place number.
|
||||||
|
*/
|
||||||
|
getPlace(node: HTMLElement): number {
|
||||||
|
return this.getClassnameNumericSuffix(node, 'place');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the question.
|
||||||
|
*
|
||||||
|
* @param {any} question Question.
|
||||||
|
*/
|
||||||
|
initializer(question: any): void {
|
||||||
|
this.selectors = this.cssSelectors(question.slot);
|
||||||
|
|
||||||
|
const container = <HTMLElement> this.container.querySelector(this.selectors.topNode());
|
||||||
|
if (this.readOnly) {
|
||||||
|
container.classList.add('readonly');
|
||||||
|
} else {
|
||||||
|
container.classList.add('notreadonly');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setPaddingSizesAll();
|
||||||
|
this.cloneDragItems();
|
||||||
|
this.initialPlaceOfDragItems();
|
||||||
|
this.makeDropZones();
|
||||||
|
|
||||||
|
// Wait the DOM to be rendered.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.positionDragItems();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.resizeFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize drag items, putting them in their initial place.
|
||||||
|
*/
|
||||||
|
initialPlaceOfDragItems(): void {
|
||||||
|
const drags = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drags()));
|
||||||
|
|
||||||
|
// Add the class 'unplaced' to all elements.
|
||||||
|
drags.forEach((drag) => {
|
||||||
|
drag.classList.add('unplaced');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.placed = {};
|
||||||
|
for (const placeNo in this.inputIds) {
|
||||||
|
const inputId = this.inputIds[placeNo],
|
||||||
|
inputNode = this.container.querySelector('input#' + inputId),
|
||||||
|
choiceNo = Number(inputNode.getAttribute('value'));
|
||||||
|
|
||||||
|
if (choiceNo !== 0) {
|
||||||
|
const drop = <HTMLElement> this.container.querySelector(this.selectors.dropForPlace(parseInt(placeNo, 10) + 1)),
|
||||||
|
groupNo = this.getGroup(drop),
|
||||||
|
drag = <HTMLElement> this.container.querySelector(
|
||||||
|
this.selectors.unplacedDragsForChoiceInGroup(choiceNo, groupNo));
|
||||||
|
|
||||||
|
this.placeDragInDrop(drag, drop);
|
||||||
|
this.positionDragItem(drag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an element "draggable". In the mobile app, items are "dragged" using tap and drop.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} drag Element.
|
||||||
|
*/
|
||||||
|
makeDraggable(drag: HTMLElement): void {
|
||||||
|
drag.addEventListener('click', () => {
|
||||||
|
if (drag.classList.contains('selected')) {
|
||||||
|
this.deselectDrags();
|
||||||
|
} else {
|
||||||
|
this.selectDrag(drag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an element into a drop zone.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} drop Element.
|
||||||
|
*/
|
||||||
|
makeDropZone(drop: HTMLElement): void {
|
||||||
|
drop.addEventListener('click', () => {
|
||||||
|
const drag = this.selected;
|
||||||
|
if (!drag) {
|
||||||
|
// No element selected, nothing to do.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place it only if the same group is selected.
|
||||||
|
if (this.getGroup(drag) === this.getGroup(drop)) {
|
||||||
|
this.placeDragInDrop(drag, drop);
|
||||||
|
this.deselectDrags();
|
||||||
|
this.positionDragItem(drag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create all drop zones.
|
||||||
|
*/
|
||||||
|
makeDropZones(): void {
|
||||||
|
if (this.readOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create all the drop zones.
|
||||||
|
const drops = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drops()));
|
||||||
|
drops.forEach((drop) => {
|
||||||
|
this.makeDropZone(drop);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If home answer zone is clicked, return drag home.
|
||||||
|
const home = <HTMLElement> this.container.querySelector(this.selectors.topNode() + ' .answercontainer');
|
||||||
|
|
||||||
|
home.addEventListener('click', () => {
|
||||||
|
const drag = this.selected;
|
||||||
|
if (!drag) {
|
||||||
|
// No element selected, nothing to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not placed yet, deselect.
|
||||||
|
if (drag.classList.contains('unplaced')) {
|
||||||
|
this.deselectDrags();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove, deselect and move back home in this order.
|
||||||
|
this.removeDragFromDrop(drag);
|
||||||
|
this.deselectDrags();
|
||||||
|
this.positionDragItem(drag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the width and height of an element.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} node Element.
|
||||||
|
* @param {number} width Width to set.
|
||||||
|
* @param {number} height Height to set.
|
||||||
|
*/
|
||||||
|
protected padToWidthHeight(node: HTMLElement, width: number, height: number): void {
|
||||||
|
node.style.width = width + 'px';
|
||||||
|
node.style.height = height + 'px';
|
||||||
|
node.style.lineHeight = height + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place a draggable element inside a drop zone.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} drag Draggable element.
|
||||||
|
* @param {HTMLElement} drop Drop zone.
|
||||||
|
*/
|
||||||
|
placeDragInDrop(drag: HTMLElement, drop: HTMLElement): void {
|
||||||
|
|
||||||
|
const placeNo = this.getPlace(drop),
|
||||||
|
inputId = this.inputIds[placeNo - 1],
|
||||||
|
inputNode = this.container.querySelector('input#' + inputId);
|
||||||
|
|
||||||
|
// Set the value of the drag element in the input of the drop zone.
|
||||||
|
if (drag !== null) {
|
||||||
|
inputNode.setAttribute('value', String(this.getChoice(drag)));
|
||||||
|
} else {
|
||||||
|
inputNode.setAttribute('value', '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the element from the "placed" map if it's there.
|
||||||
|
for (const alreadyThereDragNo in this.placed) {
|
||||||
|
if (this.placed[alreadyThereDragNo] === placeNo) {
|
||||||
|
delete this.placed[alreadyThereDragNo];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drag !== null) {
|
||||||
|
// Add the element in the "placed" map.
|
||||||
|
this.placed[this.getNo(drag)] = placeNo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position a drag element in the right drop zone or in the home zone.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} drag Drag element.
|
||||||
|
*/
|
||||||
|
positionDragItem(drag: HTMLElement): void {
|
||||||
|
let position;
|
||||||
|
|
||||||
|
const placeNo = this.placed[this.getNo(drag)];
|
||||||
|
if (!placeNo) {
|
||||||
|
// Not placed, put it in home zone.
|
||||||
|
const groupNo = this.getGroup(drag),
|
||||||
|
choiceNo = this.getChoice(drag);
|
||||||
|
|
||||||
|
position = this.domUtils.getElementXY(this.container, this.selectors.dragHome(groupNo, choiceNo), 'answercontainer');
|
||||||
|
drag.classList.add('unplaced');
|
||||||
|
} else {
|
||||||
|
// Get the drop zone position.
|
||||||
|
position = this.domUtils.getElementXY(this.container, this.selectors.dropForPlace(placeNo),
|
||||||
|
'addon-qtype-ddwtos-container');
|
||||||
|
drag.classList.remove('unplaced');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
drag.style.left = position[0] + 'px';
|
||||||
|
drag.style.top = position[1] + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Postition, or reposition, all the drag items. They're placed in the right drop zone or in the home zone.
|
||||||
|
*/
|
||||||
|
positionDragItems(): void {
|
||||||
|
const drags = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drags()));
|
||||||
|
drags.forEach((drag) => {
|
||||||
|
this.positionDragItem(drag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a draggable element from a drop zone.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} drag The draggable element.
|
||||||
|
*/
|
||||||
|
removeDragFromDrop(drag: HTMLElement): void {
|
||||||
|
const placeNo = this.placed[this.getNo(drag)],
|
||||||
|
drop = <HTMLElement> this.container.querySelector(this.selectors.dropForPlace(placeNo));
|
||||||
|
|
||||||
|
this.placeDragInDrop(null, drop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to call when the window is resized.
|
||||||
|
*/
|
||||||
|
resizeFunction(): void {
|
||||||
|
this.positionDragItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a certain element as being "dragged".
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} drag Element.
|
||||||
|
*/
|
||||||
|
selectDrag(drag: HTMLElement): void {
|
||||||
|
// Deselect previous drags, only 1 can be selected.
|
||||||
|
this.deselectDrags();
|
||||||
|
|
||||||
|
this.selected = drag;
|
||||||
|
drag.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the padding size for all groups.
|
||||||
|
*/
|
||||||
|
setPaddingSizesAll(): void {
|
||||||
|
for (let groupNo = 1; groupNo <= 8; groupNo++) {
|
||||||
|
this.setPaddingSizeForGroup(groupNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the padding size for a certain group.
|
||||||
|
*
|
||||||
|
* @param {number} groupNo Group number.
|
||||||
|
*/
|
||||||
|
setPaddingSizeForGroup(groupNo: number): void {
|
||||||
|
const groupItems = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dragHomesGroup(groupNo)));
|
||||||
|
|
||||||
|
if (groupItems.length !== 0) {
|
||||||
|
let maxWidth = 0,
|
||||||
|
maxHeight = 0;
|
||||||
|
|
||||||
|
// Find max height and width.
|
||||||
|
groupItems.forEach((item) => {
|
||||||
|
maxWidth = Math.max(maxWidth, Math.ceil(item.offsetWidth));
|
||||||
|
maxHeight = Math.max(maxHeight, Math.ceil(item.offsetHeight));
|
||||||
|
});
|
||||||
|
|
||||||
|
maxWidth += 8;
|
||||||
|
maxHeight += 2;
|
||||||
|
groupItems.forEach((item) => {
|
||||||
|
this.padToWidthHeight(item, maxWidth, maxHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dropsGroup = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dropsGroup(groupNo)));
|
||||||
|
dropsGroup.forEach((item) => {
|
||||||
|
this.padToWidthHeight(item, maxWidth + 2, maxHeight + 2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<section ion-list *ngIf="question.text || question.text === ''" class="addon-qtype-ddwtos-container">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<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.answers" [component]="component" [componentId]="componentId" [text]="question.answers"></core-format-text>
|
||||||
|
<div class="drags"></div>
|
||||||
|
</ion-item>
|
||||||
|
</section>
|
|
@ -0,0 +1,101 @@
|
||||||
|
// (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 { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render a drag-and-drop words into sentences question.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-qtype-ddwtos',
|
||||||
|
templateUrl: 'ddwtos.html'
|
||||||
|
})
|
||||||
|
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
|
protected element: HTMLElement;
|
||||||
|
protected questionInstance: AddonQtypeDdwtosQuestion;
|
||||||
|
protected inputIds: string[]; // Ids of the inputs of the question (where the answers will be stored).
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||||
|
super(logger, 'AddonQtypeDdwtosComponent', 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;
|
||||||
|
|
||||||
|
// Replace Moodle's correct/incorrect and feedback classes with our own.
|
||||||
|
this.questionHelper.replaceCorrectnessClasses(div);
|
||||||
|
this.questionHelper.replaceFeedbackClasses(div);
|
||||||
|
|
||||||
|
// Treat the correct/incorrect icons.
|
||||||
|
this.questionHelper.treatCorrectnessIcons(div, this.component, this.componentId);
|
||||||
|
|
||||||
|
const answerContainer = div.querySelector('.answercontainer');
|
||||||
|
if (!answerContainer) {
|
||||||
|
this.logger.warn('Aborting because of an error parsing question.', this.question.name);
|
||||||
|
|
||||||
|
return this.questionHelper.showComponentError(this.onAbort);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.question.readOnly = answerContainer.classList.contains('readonly');
|
||||||
|
this.question.answers = answerContainer.outerHTML;
|
||||||
|
|
||||||
|
this.question.text = this.domUtils.getContentsOfElement(div, '.qtext');
|
||||||
|
if (typeof this.question.text == 'undefined') {
|
||||||
|
this.logger.warn('Aborting because of an error parsing question.', this.question.name);
|
||||||
|
|
||||||
|
return this.questionHelper.showComponentError(this.onAbort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the inputs where the answers will be stored and add them to the question text.
|
||||||
|
const inputEls = <HTMLElement[]> Array.from(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])')),
|
||||||
|
inputIds = [];
|
||||||
|
|
||||||
|
inputEls.forEach((inputEl) => {
|
||||||
|
this.question.text += inputEl.outerHTML;
|
||||||
|
inputIds.push(inputEl.getAttribute('id'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View has been initialized.
|
||||||
|
*/
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
// Create the instance.
|
||||||
|
this.questionInstance = new AddonQtypeDdwtosQuestion(this.logger, this.domUtils, this.element, this.question,
|
||||||
|
this.question.readOnly, this.inputIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.questionInstance && this.questionInstance.destroy();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// (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 { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonQtypeDdwtosHandler } from './providers/handler';
|
||||||
|
import { AddonQtypeDdwtosComponent } from './component/ddwtos';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonQtypeDdwtosComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreDirectivesModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonQtypeDdwtosHandler
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonQtypeDdwtosComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonQtypeDdwtosComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonQtypeDdwtosModule {
|
||||||
|
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeDdwtosHandler) {
|
||||||
|
questionDelegate.registerHandler(handler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
|
||||||
|
// (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 { AddonQtypeDdwtosComponent } from '../component/ddwtos';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support drag-and-drop words into sentences question type.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonQtypeDdwtosHandler implements CoreQuestionHandler {
|
||||||
|
name = 'AddonQtypeDdwtos';
|
||||||
|
type = 'qtype_ddwtos';
|
||||||
|
|
||||||
|
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 AddonQtypeDdwtosComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
for (const name in answers) {
|
||||||
|
const value = answers[name];
|
||||||
|
if (!value || value === '0') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
for (const name in answers) {
|
||||||
|
const value = answers[name];
|
||||||
|
if (value && value !== '0') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 { AddonQtypeCalculatedModule } from './calculated/calculated.module';
|
||||||
import { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module';
|
import { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module';
|
||||||
import { AddonQtypeCalculatedSimpleModule } from './calculatedsimple/calculatedsimple.module';
|
import { AddonQtypeCalculatedSimpleModule } from './calculatedsimple/calculatedsimple.module';
|
||||||
|
import { AddonQtypeDdwtosModule } from './ddwtos/ddwtos.module';
|
||||||
import { AddonQtypeDescriptionModule } from './description/description.module';
|
import { AddonQtypeDescriptionModule } from './description/description.module';
|
||||||
import { AddonQtypeEssayModule } from './essay/essay.module';
|
import { AddonQtypeEssayModule } from './essay/essay.module';
|
||||||
import { AddonQtypeGapSelectModule } from './gapselect/gapselect.module';
|
import { AddonQtypeGapSelectModule } from './gapselect/gapselect.module';
|
||||||
|
@ -33,6 +34,7 @@ import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
|
||||||
AddonQtypeCalculatedModule,
|
AddonQtypeCalculatedModule,
|
||||||
AddonQtypeCalculatedMultiModule,
|
AddonQtypeCalculatedMultiModule,
|
||||||
AddonQtypeCalculatedSimpleModule,
|
AddonQtypeCalculatedSimpleModule,
|
||||||
|
AddonQtypeDdwtosModule,
|
||||||
AddonQtypeDescriptionModule,
|
AddonQtypeDescriptionModule,
|
||||||
AddonQtypeEssayModule,
|
AddonQtypeEssayModule,
|
||||||
AddonQtypeGapSelectModule,
|
AddonQtypeGapSelectModule,
|
||||||
|
|
Loading…
Reference in New Issue