From d14d700f7b007e209916f9b84e799808b7d7fa00 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 21 Mar 2018 12:19:41 +0100 Subject: [PATCH] MOBILE-2390 qtype: Implement ddwtos type --- src/addon/qtype/ddwtos/classes/ddwtos.ts | 550 +++++++++++++++++++ src/addon/qtype/ddwtos/component/ddwtos.html | 11 + src/addon/qtype/ddwtos/component/ddwtos.ts | 101 ++++ src/addon/qtype/ddwtos/ddwtos.module.ts | 46 ++ src/addon/qtype/ddwtos/providers/handler.ts | 116 ++++ src/addon/qtype/qtype.module.ts | 2 + 6 files changed, 826 insertions(+) create mode 100644 src/addon/qtype/ddwtos/classes/ddwtos.ts create mode 100644 src/addon/qtype/ddwtos/component/ddwtos.html create mode 100644 src/addon/qtype/ddwtos/component/ddwtos.ts create mode 100644 src/addon/qtype/ddwtos/ddwtos.module.ts create mode 100644 src/addon/qtype/ddwtos/providers/handler.ts diff --git a/src/addon/qtype/ddwtos/classes/ddwtos.ts b/src/addon/qtype/ddwtos/classes/ddwtos.ts new file mode 100644 index 000000000..648c907fa --- /dev/null +++ b/src/addon/qtype/ddwtos/classes/ddwtos.ts @@ -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 = 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 = 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 = 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 = 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 = 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 = this.container.querySelector(this.selectors.dropForPlace(parseInt(placeNo, 10) + 1)), + groupNo = this.getGroup(drop), + drag = 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 = 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 = 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 = 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 = 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 = 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 = Array.from(this.container.querySelectorAll(this.selectors.dropsGroup(groupNo))); + dropsGroup.forEach((item) => { + this.padToWidthHeight(item, maxWidth + 2, maxHeight + 2); + }); + } + } +} diff --git a/src/addon/qtype/ddwtos/component/ddwtos.html b/src/addon/qtype/ddwtos/component/ddwtos.html new file mode 100644 index 000000000..689a75406 --- /dev/null +++ b/src/addon/qtype/ddwtos/component/ddwtos.html @@ -0,0 +1,11 @@ +
+ +

+ + {{ 'core.question.howtodraganddrop' | translate }} +

+

+ +
+
+
diff --git a/src/addon/qtype/ddwtos/component/ddwtos.ts b/src/addon/qtype/ddwtos/component/ddwtos.ts new file mode 100644 index 000000000..18a4a121a --- /dev/null +++ b/src/addon/qtype/ddwtos/component/ddwtos.ts @@ -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 = 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(); + } +} diff --git a/src/addon/qtype/ddwtos/ddwtos.module.ts b/src/addon/qtype/ddwtos/ddwtos.module.ts new file mode 100644 index 000000000..00e0c1dbf --- /dev/null +++ b/src/addon/qtype/ddwtos/ddwtos.module.ts @@ -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); + } +} diff --git a/src/addon/qtype/ddwtos/providers/handler.ts b/src/addon/qtype/ddwtos/providers/handler.ts new file mode 100644 index 000000000..6a8a6eae4 --- /dev/null +++ b/src/addon/qtype/ddwtos/providers/handler.ts @@ -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} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, question: any): any | Promise { + 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} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + 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); + } +} diff --git a/src/addon/qtype/qtype.module.ts b/src/addon/qtype/qtype.module.ts index a8563022c..513587617 100644 --- a/src/addon/qtype/qtype.module.ts +++ b/src/addon/qtype/qtype.module.ts @@ -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,