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 { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module';
|
||||
import { AddonQtypeCalculatedSimpleModule } from './calculatedsimple/calculatedsimple.module';
|
||||
import { AddonQtypeDdwtosModule } from './ddwtos/ddwtos.module';
|
||||
import { AddonQtypeDescriptionModule } from './description/description.module';
|
||||
import { AddonQtypeEssayModule } from './essay/essay.module';
|
||||
import { AddonQtypeGapSelectModule } from './gapselect/gapselect.module';
|
||||
|
@ -33,6 +34,7 @@ import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
|
|||
AddonQtypeCalculatedModule,
|
||||
AddonQtypeCalculatedMultiModule,
|
||||
AddonQtypeCalculatedSimpleModule,
|
||||
AddonQtypeDdwtosModule,
|
||||
AddonQtypeDescriptionModule,
|
||||
AddonQtypeEssayModule,
|
||||
AddonQtypeGapSelectModule,
|
||||
|
|
Loading…
Reference in New Issue