MOBILE-2390 qtype: Implement ddimageortext type

main
Dani Palou 2018-03-22 10:55:34 +01:00
parent 7d3015015f
commit 6ebb42041a
6 changed files with 1046 additions and 0 deletions

View File

@ -0,0 +1,772 @@
// (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';
/**
* Encapsulates operations on dd area.
*/
export interface AddonQtypeDdImageOrTextQuestionDocStructure {
topNode?: () => HTMLElement;
dragItemsArea?: () => HTMLElement;
dragItems?: () => HTMLElement[];
dropZones?: () => HTMLElement[];
dropZoneGroup?: (groupNo: number) => HTMLElement[];
dragItemsClonedFrom?: (dragItemNo: number) => HTMLElement[];
dragItem?: (dragInstanceNo: number) => HTMLElement;
dragItemsInGroup?: (groupNo: number) => HTMLElement[];
dragItemHomes?: () => HTMLElement[];
bgImg?: () => HTMLImageElement;
dragItemHome?: (dragItemNo: number) => HTMLElement;
getClassnameNumericSuffix?: (node: HTMLElement, prefix: string) => number;
cloneNewDragItem?: (dragInstanceNo: number, dragItemNo: number) => HTMLElement;
}
/**
* Class to make a question of ddimageortext type work.
*/
export class AddonQtypeDdImageOrTextQuestion {
protected logger: any;
protected toLoad = 0;
protected doc: AddonQtypeDdImageOrTextQuestionDocStructure;
protected afterImageLoadDone = false;
protected topNode: HTMLElement;
protected proportion = 1;
protected selected: HTMLElement; // Selected element (being "dragged").
/**
* Create the this.
*
* @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 this.
* @param {boolean} readOnly Whether it's read only.
* @param {any[]} drops The drop zones received in the init object of the question.
*/
constructor(logger: CoreLoggerProvider, protected domUtils: CoreDomUtilsProvider, protected container: HTMLElement,
protected question: any, protected readOnly: boolean, protected drops: any[]) {
this.logger = logger.getInstance('AddonQtypeDdImageOrTextQuestion');
this.initializer(question);
}
/**
* Calculate image proportion to make easy conversions.
*/
calculateImgProportion(): void {
const bgImg = this.doc.bgImg();
// Render the position related to the current image dimensions.
this.proportion = 1;
if (bgImg.width != bgImg.naturalWidth) {
this.proportion = bgImg.width / bgImg.naturalWidth;
}
}
/**
* Convert the X and Y position of the BG IMG to a position relative to the window.
*
* @param {number[]} bgImgXY X and Y of the BG IMG relative position.
* @return {number[]} Position relative to the window.
*/
convertToWindowXY(bgImgXY: number[]): number[] {
const bgImg = this.doc.bgImg(),
position = this.domUtils.getElementXY(bgImg, null, 'ddarea');
// Render the position related to the current image dimensions.
bgImgXY[0] *= this.proportion;
bgImgXY[1] *= this.proportion;
return [Number(bgImgXY[0]) + position[0] + 1, Number(bgImgXY[1]) + position[1] + 1];
}
/**
* Create and initialize all draggable elements and drop zones.
*/
createAllDragAndDrops(): void {
// Initialize drop zones.
this.initDrops();
// Initialize drag items area.
const dragItemsArea = this.doc.dragItemsArea();
dragItemsArea.classList.add('clearfix');
this.makeDragAreaClickable();
const dragItemHomes = this.doc.dragItemHomes();
let i = 0;
// Create the draggable items.
for (let x = 0; x < dragItemHomes.length; x++) {
const dragItemHome = dragItemHomes[x],
dragItemNo = this.doc.getClassnameNumericSuffix(dragItemHome, 'dragitemhomes'),
choice = this.doc.getClassnameNumericSuffix(dragItemHome, 'choice'),
group = this.doc.getClassnameNumericSuffix(dragItemHome, 'group');
// Images need to be inside a div element to admit padding with width and height.
if (dragItemHome.tagName == 'IMG') {
const wrap = document.createElement('div');
wrap.className = dragItemHome.className;
dragItemHome.className = '';
// Insert wrapper before the image in the DOM tree.
dragItemHome.parentNode.insertBefore(wrap, dragItemHome);
// Move the image into wrapper.
wrap.appendChild(dragItemHome);
}
// Create a new drag item for this home.
const dragNode = this.doc.cloneNewDragItem(i, dragItemNo);
i++;
if (!this.readOnly) {
// Make the item draggable.
this.draggableForQuestion(dragNode, group, choice);
}
// If the draggable item needs to be created more than once, create the rest of copies.
if (dragNode.classList.contains('infinite')) {
const groupSize = this.doc.dropZoneGroup(group).length;
let dragsToCreate = groupSize - 1;
while (dragsToCreate > 0) {
const newDragNode = this.doc.cloneNewDragItem(i, dragItemNo);
i++;
if (!this.readOnly) {
this.draggableForQuestion(newDragNode, group, choice);
}
dragsToCreate--;
}
}
}
// All drag items have been created, position them.
this.repositionDragsForQuestion();
if (!this.readOnly) {
const dropZones = this.doc.dropZones();
dropZones.forEach((dropZone) => {
dropZone.setAttribute('tabIndex', '0');
});
}
}
/**
* Deselect all drags.
*/
deselectDrags(): void {
const drags = this.doc.dragItems();
drags.forEach((drag) => {
drag.classList.remove('beingdragged');
});
this.selected = null;
}
/**
* Function to call when the instance is no longer needed.
*/
destroy(): void {
this.stopPolling();
window.removeEventListener('resize', this.resizeFunction);
}
/**
* Returns an object to encapsulate operations on dd area.
*
* @param {number} slot The question slot.
* @return {AddonQtypeDdImageOrTextQuestionDocStructure} The object.
*/
docStructure(slot: number): AddonQtypeDdImageOrTextQuestionDocStructure {
const topNode = <HTMLElement> this.container.querySelector(`#core-question-${slot} .addon-qtype-ddimageortext-container`),
dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems'),
doc: AddonQtypeDdImageOrTextQuestionDocStructure = {};
doc.topNode = (): HTMLElement => {
return topNode;
};
doc.dragItemsArea = (): HTMLElement => {
return dragItemsArea;
};
doc.dragItems = (): HTMLElement[] => {
return <HTMLElement[]> Array.from(dragItemsArea.querySelectorAll('.drag'));
};
doc.dropZones = (): HTMLElement[] => {
return <HTMLElement[]> Array.from(topNode.querySelectorAll('div.dropzones div.dropzone'));
};
doc.dropZoneGroup = (groupNo: number): HTMLElement[] => {
return <HTMLElement[]> Array.from(topNode.querySelectorAll('div.dropzones div.group' + groupNo));
};
doc.dragItemsClonedFrom = (dragItemNo: number): HTMLElement[] => {
return <HTMLElement[]> Array.from(dragItemsArea.querySelectorAll('.dragitems' + dragItemNo));
};
doc.dragItem = (dragInstanceNo: number): HTMLElement => {
return <HTMLElement> dragItemsArea.querySelector('.draginstance' + dragInstanceNo);
};
doc.dragItemsInGroup = (groupNo: number): HTMLElement[] => {
return <HTMLElement[]> Array.from(dragItemsArea.querySelectorAll('.drag.group' + groupNo));
};
doc.dragItemHomes = (): HTMLElement[] => {
return <HTMLElement[]> Array.from(dragItemsArea.querySelectorAll('.draghome'));
};
doc.bgImg = (): HTMLImageElement => {
return <HTMLImageElement> topNode.querySelector('.dropbackground');
};
doc.dragItemHome = (dragItemNo: number): HTMLElement => {
return <HTMLElement> dragItemsArea.querySelector('.dragitemhomes' + dragItemNo);
};
doc.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.');
};
doc.cloneNewDragItem = (dragInstanceNo: number, dragItemNo: number): HTMLElement => {
const dragHome = doc.dragItemHome(dragItemNo);
if (dragHome === null) {
return null;
}
const dragHomeImg = dragHome.querySelector('img');
let divDrag: HTMLElement;
// Images need to be inside a div element to admit padding with width and height.
if (dragHomeImg) {
// Clone the image.
const drag = <HTMLElement> dragHomeImg.cloneNode(true);
// Create a div and put the image in it.
divDrag = document.createElement('div');
divDrag.appendChild(drag);
divDrag.className = dragHome.className;
drag.className = '';
} else {
// The drag item doesn't have an image, just clone it.
divDrag = <HTMLElement> dragHome.cloneNode(true);
}
// Set the right classes and styles.
divDrag.classList.remove('dragitemhomes' + dragItemNo);
divDrag.classList.remove('draghome');
divDrag.classList.add('dragitems' + dragItemNo);
divDrag.classList.add('draginstance' + dragInstanceNo);
divDrag.classList.add('drag');
divDrag.style.visibility = 'inherit';
divDrag.style.position = 'absolute';
divDrag.setAttribute('draginstanceno', String(dragInstanceNo));
divDrag.setAttribute('dragitemno', String(dragItemNo));
// Insert the new drag after the dragHome.
dragHome.parentElement.insertBefore(divDrag, dragHome.nextSibling);
return divDrag;
};
return doc;
}
/**
* Make an element draggable.
*
* @param {HTMLElement} drag Element to make draggable.
* @param {number} group Group the element belongs to.
* @param {number} choice Choice the element belongs to.
*/
draggableForQuestion(drag: HTMLElement, group: number, choice: number): void {
// Set attributes.
drag.setAttribute('group', String(group));
drag.setAttribute('choice', String(choice));
// Listen to click events.
drag.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (drag.classList.contains('beingdragged')) {
this.deselectDrags();
} else {
this.selectDrag(drag);
}
});
}
/**
* Function called when a drop zone is clicked.
*
* @param {HTMLElement} dropNode Drop element.
*/
dropClick(dropNode: HTMLElement): void {
const drag = this.selected;
if (!drag) {
// No selected item, nothing to do.
return;
}
// Deselect the drag and place it in the position of this drop zone if it belongs to the same group.
this.deselectDrags();
if (Number(dropNode.getAttribute('group')) === Number(drag.getAttribute('group'))) {
this.placeDragInDrop(drag, dropNode);
}
}
/**
* Get all the draggable elements for a choice and a drop zone.
*
* @param {number} choice Choice number.
* @param {HTMLElement} drop Drop zone.
* @return {HTMLElement[]} Draggable elements.
*/
getChoicesForDrop(choice: number, drop: HTMLElement): HTMLElement[] {
return <HTMLElement[]> Array.from(this.doc.topNode().querySelectorAll(
'div.dragitemgroup' + drop.getAttribute('group') + ' .choice' + choice + '.drag'));
}
/**
* Get an unplaced draggable element that belongs to a certain choice and drop zone.
*
* @param {number} choice Choice number.
* @param {HTMLElement} drop Drop zone.
* @return {HTMLElement} Unplaced draggable element.
*/
getUnplacedChoiceForDrop(choice: number, drop: HTMLElement): HTMLElement {
const dragItems = this.getChoicesForDrop(choice, drop);
for (let x = 0; x < dragItems.length; x++) {
const dragItem = dragItems[x];
if (this.readOnly || (!dragItem.classList.contains('placed') && !dragItem.classList.contains('beingdragged'))) {
return dragItem;
}
}
return null;
}
/**
* Initialize drop zones.
*/
initDrops(): void {
const dropAreas = this.doc.topNode().querySelector('div.dropzones'),
groupNodes = {};
// Create all group nodes and add them to the drop area.
for (let groupNo = 1; groupNo <= 8; groupNo++) {
const groupNode = document.createElement('div');
groupNode.className = 'dropzonegroup' + groupNo;
dropAreas.appendChild(groupNode);
groupNodes[groupNo] = groupNode;
}
// Create the drops specified by the init object.
for (const dropNo in this.drops) {
const drop = this.drops[dropNo],
nodeClass = 'dropzone group' + drop.group + ' place' + dropNo,
title = drop.text.replace('"', '\"'),
dropNode = document.createElement('div');
dropNode.setAttribute('title', title);
dropNode.className = nodeClass;
groupNodes[drop.group].appendChild(dropNode);
dropNode.style.opacity = '0.5';
dropNode.setAttribute('xy', drop.xy);
dropNode.setAttribute('aria-label', drop.text);
dropNode.setAttribute('place', dropNo);
dropNode.setAttribute('inputid', drop.fieldname.replace(':', '_'));
dropNode.setAttribute('group', drop.group);
dropNode.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.dropClick(dropNode);
});
}
}
/**
* Initialize the question.
*
* @param {any} question Question.
*/
initializer(question: any): void {
this.doc = this.docStructure(question.slot);
if (this.readOnly) {
const container = this.doc.topNode();
container.classList.add('readonly');
}
// Wait the DOM to be rendered.
setTimeout(() => {
const bgImg = this.doc.bgImg();
// Wait for background image to be loaded.
// On iOS, complete is mistakenly true, check also naturalWidth for compatibility.
if (!bgImg.complete || !bgImg.naturalWidth) {
this.toLoad++;
bgImg.addEventListener('load', () => {
this.toLoad--;
});
}
const itemHomes = this.doc.dragItemHomes();
itemHomes.forEach((item) => {
if (item.tagName == 'IMG') {
// Wait for drag images to be loaded.
// On iOS, complete is mistakenly true, check also naturalWidth for compatibility.
const itemImg = <HTMLImageElement> item;
if (!itemImg.complete || !itemImg.naturalWidth) {
this.toLoad++;
itemImg.addEventListener('load', () => {
this.toLoad--;
});
}
}
});
this.pollForImageLoad();
});
window.addEventListener('resize', this.resizeFunction);
}
/**
* Make the drag items area clickable.
*/
makeDragAreaClickable(): void {
if (this.readOnly) {
return;
}
const home = this.doc.dragItemsArea();
home.addEventListener('click', (e) => {
const drag = this.selected;
if (!drag) {
// No element selected, nothing to do.
return false;
}
// An element was selected. Deselect it and move it back to the area if needed.
this.deselectDrags();
this.removeDragFromDrop(drag);
e.preventDefault();
e.stopPropagation();
});
}
/**
* Place a draggable element into a certain drop zone.
*
* @param {HTMLElement} drag Draggable element.
* @param {HTMLElement} drop Drop zone element.
*/
placeDragInDrop(drag: HTMLElement, drop: HTMLElement): void {
// Search the input related to the drop zone.
const targetInputId = drop.getAttribute('inputid'),
inputNode = <HTMLInputElement> this.doc.topNode().querySelector('input#' + targetInputId);
// Check if the draggable item is already assigned to an input and if it's the same as the one of the drop zone.
const originInputId = drag.getAttribute('inputid');
if (originInputId && originInputId != targetInputId) {
// Remove it from the previous place.
const originInputNode = <HTMLInputElement> this.doc.topNode().querySelector('input#' + originInputId);
originInputNode.setAttribute('value', '');
}
// Now position the draggable and set it to the input.
const position = this.domUtils.getElementXY(drop, null, 'ddarea');
drag.style.left = position[0] - 1 + 'px';
drag.style.top = position[1] - 1 + 'px';
drag.classList.add('placed');
inputNode.setAttribute('value', drag.getAttribute('choice'));
drag.setAttribute('inputid', targetInputId);
}
/**
* Wait for images to be loaded.
*/
pollForImageLoad(): void {
if (this.afterImageLoadDone) {
// Already done, stop.
return;
}
if (this.toLoad <= 0) {
// All images loaded.
this.createAllDragAndDrops();
this.afterImageLoadDone = true;
this.question.loaded = true;
}
// Try again after a while.
setTimeout(() => {
this.pollForImageLoad();
}, 1000);
}
/**
* Remove a draggable element from the drop zone where it is.
*
* @param {HTMLElement} drag Draggable element to remove.
*/
removeDragFromDrop(drag: HTMLElement): void {
// Check if the draggable element is assigned to an input. If so, empty the input's value.
const inputId = drag.getAttribute('inputid');
if (inputId) {
const inputNode = <HTMLInputElement> this.doc.topNode().querySelector('input#' + inputId);
inputNode.setAttribute('value', '');
}
// Move the element to its original position.
const dragItemHome = this.doc.dragItemHome(Number(drag.getAttribute('dragitemno'))),
position = this.domUtils.getElementXY(dragItemHome, null, 'ddarea');
drag.style.left = position[0] + 'px';
drag.style.top = position[1] + 'px';
drag.classList.remove('placed');
drag.setAttribute('inputid', '');
}
/**
* Reposition all the draggable elements and drop zones.
*/
repositionDragsForQuestion(): void {
const dragItems = this.doc.dragItems();
// Mark all draggable items as "unplaced", they will be placed again later.
dragItems.forEach((dragItem) => {
dragItem.classList.remove('placed');
dragItem.setAttribute('inputid', '');
});
// Calculate the proportion to apply to images.
this.calculateImgProportion();
// Apply the proportion to all images in drag item homes.
const dragItemHomes = this.doc.dragItemHomes();
for (let x = 0; x < dragItemHomes.length; x++) {
const dragItemHome = dragItemHomes[x],
dragItemHomeImg = dragItemHome.querySelector('img');
if (dragItemHomeImg && dragItemHomeImg.naturalWidth > 0) {
const widthHeight = [Math.round(dragItemHomeImg.naturalWidth * this.proportion),
Math.round(dragItemHomeImg.naturalHeight * this.proportion)];
dragItemHomeImg.style.width = widthHeight[0] + 'px';
dragItemHomeImg.style.height = widthHeight[1] + 'px';
// Apply the proportion to all the images cloned from this home.
const dragItemNo = this.doc.getClassnameNumericSuffix(dragItemHome, 'dragitemhomes'),
groupNo = this.doc.getClassnameNumericSuffix(dragItemHome, 'group'),
dragsImg = <HTMLElement[]> Array.from(this.doc.topNode().querySelectorAll(
'.drag.group' + groupNo + '.dragitems' + dragItemNo + ' img'));
dragsImg.forEach((dragImg) => {
dragImg.style.width = widthHeight[0] + 'px';
dragImg.style.height = widthHeight[1] + 'px';
});
}
}
// Update the padding of all draggable elements.
this.updatePaddingSizesAll();
const dropZones = this.doc.dropZones();
for (let x = 0; x < dropZones.length; x++) {
// Re-position the drop zone based on the proportion.
const dropZone = dropZones[x],
dropZoneXY = dropZone.getAttribute('xy').split(',').map((i) => {
return Number(i);
}),
relativeXY = this.convertToWindowXY(dropZoneXY);
dropZone.style.left = relativeXY[0] + 'px';
dropZone.style.top = relativeXY[1] + 'px';
// Re-place items got from the inputs.
const inputCss = 'input#' + dropZone.getAttribute('inputid'),
input = <HTMLInputElement> this.doc.topNode().querySelector(inputCss),
choice = Number(input.value);
if (choice > 0) {
const dragItem = this.getUnplacedChoiceForDrop(choice, dropZone);
if (dragItem !== null) {
this.placeDragInDrop(dragItem, dropZone);
}
}
}
// Re-place draggable items not placed drop zones (they will be placed in the original position).
for (let x = 0; x < dragItems.length; x++) {
const dragItem = dragItems[x];
if (!dragItem.classList.contains('placed') && !dragItem.classList.contains('beingdragged')) {
this.removeDragFromDrop(dragItem);
}
}
}
/**
* Function to call when the window is resized.
*/
resizeFunction(): void {
this.repositionDragsForQuestion();
}
/**
* Mark a draggable element as selected.
*
* @param {HTMLElement} drag Element to select.
*/
selectDrag(drag: HTMLElement): void {
// Deselect previous ones.
this.deselectDrags();
this.selected = drag;
drag.classList.add('beingdragged');
}
/**
* Stop waiting for images to be loaded.
*/
stopPolling(): void {
this.afterImageLoadDone = true;
}
/**
* Update the padding of all items in a group to make them all have the same width and height.
*
* @param {number} groupNo The group number.
*/
updatePaddingSizeForGroup(groupNo: number): void {
// Get all the items for this group.
const groupItems = <HTMLElement[]> Array.from(this.doc.topNode().querySelectorAll('.draghome.group' + groupNo));
if (groupItems.length !== 0) {
// Get the max width and height of the items.
let maxWidth = 0,
maxHeight = 0;
for (let x = 0; x < groupItems.length; x++) {
// Check if the item has an img.
const item = groupItems[x],
img = item.querySelector('img');
if (img) {
maxWidth = Math.max(maxWidth, Math.round(this.proportion * img.naturalWidth));
maxHeight = Math.max(maxHeight, Math.round(this.proportion * img.naturalHeight));
} else {
// Remove the padding to calculate the size.
const originalPadding = item.style.padding;
item.style.padding = '';
// Text is not affected by the proportion.
maxWidth = Math.max(maxWidth, Math.round(item.clientWidth));
maxHeight = Math.max(maxHeight, Math.round(item.clientHeight));
// Restore the padding.
item.style.padding = originalPadding;
}
}
if (maxWidth <= 0 || maxHeight <= 0) {
return;
}
// Add a variable padding to the image or text.
maxWidth = Math.round(maxWidth + this.proportion * 8);
maxHeight = Math.round(maxHeight + this.proportion * 8);
for (let x = 0; x < groupItems.length; x++) {
// Check if the item has an img and calculate its width and height.
const item = groupItems[x],
img = item.querySelector('img');
let width, height;
if (img) {
width = Math.round(img.naturalWidth * this.proportion);
height = Math.round(img.naturalHeight * this.proportion);
} else {
// Remove the padding to calculate the size.
const originalPadding = item.style.padding;
item.style.padding = '';
// Text is not affected by the proportion.
width = Math.round(item.clientWidth);
height = Math.round(item.clientHeight);
// Restore the padding.
item.style.padding = originalPadding;
}
// Now set the right padding to make this item have the max height and width.
const marginTopBottom = Math.round((maxHeight - height) / 2),
marginLeftRight = Math.round((maxWidth - width) / 2);
// Correction for the roundings.
const widthCorrection = maxWidth - (width + marginLeftRight * 2),
heightCorrection = maxHeight - (height + marginTopBottom * 2);
item.style.padding = marginTopBottom + 'px ' + marginLeftRight + 'px ' +
(marginTopBottom + heightCorrection) + 'px ' + (marginLeftRight + widthCorrection) + 'px';
const dragItemNo = this.doc.getClassnameNumericSuffix(item, 'dragitemhomes'),
drags = <HTMLElement[]> Array.from(this.doc.topNode().querySelectorAll(
'.drag.group' + groupNo + '.dragitems' + dragItemNo));
drags.forEach((drag) => {
drag.style.padding = marginTopBottom + 'px ' + marginLeftRight + 'px ' +
(marginTopBottom + heightCorrection) + 'px ' + (marginLeftRight + widthCorrection) + 'px';
});
}
// It adds the border of 1px to the width.
const zoneGroups = this.doc.dropZoneGroup(groupNo);
zoneGroups.forEach((zone) => {
zone.style.width = maxWidth + 2 + 'px ';
zone.style.height = maxHeight + 2 + 'px ';
});
}
}
/**
* Update the padding of all items in all groups.
*/
updatePaddingSizesAll(): void {
for (let groupNo = 1; groupNo <= 8; groupNo++) {
this.updatePaddingSizeForGroup(groupNo);
}
}
}

View File

@ -0,0 +1,13 @@
<section ion-list *ngIf="question.text || question.text === ''" class="addon-qtype-ddimageortext-container">
<!-- Content is outside the core-loading to let the script calculate drag items position -->
<core-loading [hideUntil]="question.loaded"></core-loading>
<ion-item text-wrap [ngClass]="{invisible: !question.loaded}">
<p *ngIf="!question.readOnly" class="core-info-card-icon">
<ion-icon name="information" item-start></ion-icon>
{{ 'core.question.howtodraganddrop' | translate }}
</p>
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea"></core-format-text>
</ion-item>
</section>

View File

@ -0,0 +1,93 @@
// (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 { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
/**
* Component to render a drag-and-drop onto image question.
*/
@Component({
selector: 'addon-qtype-ddimageortext',
templateUrl: 'ddimageortext.html'
})
export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy {
protected element: HTMLElement;
protected questionInstance: AddonQtypeDdImageOrTextQuestion;
protected drops: any[]; // The drop zones received in the init object of the question.
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) {
super(logger, 'AddonQtypeDdImageOrTextComponent', injector);
this.element = element.nativeElement;
}
/**
* Component being initialized.
*/
ngOnInit(): void {
if (!this.question) {
this.logger.warn('Aborting because of no question received.');
return this.questionHelper.showComponentError(this.onAbort);
}
const div = document.createElement('div');
div.innerHTML = this.question.html;
// Get D&D area and question text.
const ddArea = div.querySelector('.ddarea');
this.question.text = this.domUtils.getContentsOfElement(div, '.qtext');
if (!ddArea || typeof this.question.text == 'undefined') {
this.logger.warn('Aborting because of an error parsing question.', this.question.name);
return this.questionHelper.showComponentError(this.onAbort);
}
// Set the D&D area HTML.
this.question.ddArea = ddArea.outerHTML;
this.question.readOnly = false;
if (this.question.initObjects) {
if (typeof this.question.initObjects.drops != 'undefined') {
this.drops = this.question.initObjects.drops;
}
if (typeof this.question.initObjects.readonly != 'undefined') {
this.question.readOnly = this.question.initObjects.readonly;
}
}
this.question.loaded = false;
}
/**
* View has been initialized.
*/
ngAfterViewInit(): void {
// Create the instance.
this.questionInstance = new AddonQtypeDdImageOrTextQuestion(this.logger, this.domUtils, this.element,
this.question, this.question.readOnly, this.drops);
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.questionInstance && this.questionInstance.destroy();
}
}

View File

@ -0,0 +1,48 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonQtypeDdImageOrTextHandler } from './providers/handler';
import { AddonQtypeDdImageOrTextComponent } from './component/ddimageortext';
@NgModule({
declarations: [
AddonQtypeDdImageOrTextComponent
],
imports: [
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
AddonQtypeDdImageOrTextHandler
],
exports: [
AddonQtypeDdImageOrTextComponent
],
entryComponents: [
AddonQtypeDdImageOrTextComponent
]
})
export class AddonQtypeDdImageOrTextModule {
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeDdImageOrTextHandler) {
questionDelegate.registerHandler(handler);
}
}

View File

@ -0,0 +1,118 @@
// (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 { AddonQtypeDdImageOrTextComponent } from '../component/ddimageortext';
/**
* Handler to support drag-and-drop onto image question type.
*/
@Injectable()
export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler {
name = 'AddonQtypeDdImageOrText';
type = 'qtype_ddimageortext';
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 AddonQtypeDdImageOrTextComponent;
}
/**
* 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 {
// An answer is complete if all drop zones have an answer.
// We should always receive all the drop zones with their value ('' if not answered).
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);
}
}

View File

@ -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 { AddonQtypeDdImageOrTextModule } from './ddimageortext/ddimageortext.module';
import { AddonQtypeDdMarkerModule } from './ddmarker/ddmarker.module';
import { AddonQtypeDdwtosModule } from './ddwtos/ddwtos.module';
import { AddonQtypeDescriptionModule } from './description/description.module';
@ -35,6 +36,7 @@ import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
AddonQtypeCalculatedModule,
AddonQtypeCalculatedMultiModule,
AddonQtypeCalculatedSimpleModule,
AddonQtypeDdImageOrTextModule,
AddonQtypeDdMarkerModule,
AddonQtypeDdwtosModule,
AddonQtypeDescriptionModule,