From 0cef912704e870e194c108dcbd383dd406ac8581 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 9 Jan 2019 16:43:52 +0100 Subject: [PATCH] MOBILE-2795 qtype: Fix drag and drop questions in 3.6 sites --- .../ddimageortext/classes/ddimageortext.ts | 27 ++++++++++++- .../ddimageortext/component/ddimageortext.ts | 9 +++++ src/addon/qtype/ddmarker/classes/ddmarker.ts | 20 ++++------ .../qtype/ddmarker/component/ddmarker.ts | 39 ++++++++++++++++--- src/addon/qtype/ddmarker/providers/handler.ts | 20 +++++++++- src/core/question/providers/delegate.ts | 20 ++++++++++ src/core/question/providers/helper.ts | 15 ++++++- 7 files changed, 128 insertions(+), 22 deletions(-) diff --git a/src/addon/qtype/ddimageortext/classes/ddimageortext.ts b/src/addon/qtype/ddimageortext/classes/ddimageortext.ts index 4d77e03f0..88f5c9e18 100644 --- a/src/addon/qtype/ddimageortext/classes/ddimageortext.ts +++ b/src/addon/qtype/ddimageortext/classes/ddimageortext.ts @@ -197,9 +197,34 @@ export class AddonQtypeDdImageOrTextQuestion { */ docStructure(slot: number): AddonQtypeDdImageOrTextQuestionDocStructure { const topNode = this.container.querySelector('.addon-qtype-ddimageortext-container'), - dragItemsArea = topNode.querySelector('div.dragitems'), doc: AddonQtypeDdImageOrTextQuestionDocStructure = {}; + let dragItemsArea = topNode.querySelector('div.draghomes'); + + if (dragItemsArea) { + // 3.6+ site, transform HTML so it has the same structure as in Moodle 3.5. + + // Remove empty div.dragitems. + topNode.querySelector('div.dragitems').remove(); + + const ddArea = topNode.querySelector('div.ddarea'); + + // Move div.dropzones to div.ddarea. + ddArea.appendChild(topNode.querySelector('div.dropzones')); + + // Move div.draghomes to div.ddarea and rename the class to .dragitems. + ddArea.appendChild(dragItemsArea); + dragItemsArea.classList.remove('draghomes'); + dragItemsArea.classList.add('dragitems'); + + // Add .dragitemhomesNNN class to drag items. + Array.from(dragItemsArea.querySelectorAll('.draghome')).forEach((draghome, index) => { + draghome.classList.add('dragitemhomes' + index); + }); + } else { + dragItemsArea = topNode.querySelector('div.dragitems'); + } + doc.topNode = (): HTMLElement => { return topNode; }; diff --git a/src/addon/qtype/ddimageortext/component/ddimageortext.ts b/src/addon/qtype/ddimageortext/component/ddimageortext.ts index 2048842b5..38f65085a 100644 --- a/src/addon/qtype/ddimageortext/component/ddimageortext.ts +++ b/src/addon/qtype/ddimageortext/component/ddimageortext.ts @@ -64,12 +64,21 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent this.question.readOnly = false; if (this.question.initObjects) { + // Moodle version <= 3.5. 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; } + } else if (this.question.amdArgs) { + // Moodle version >= 3.6. + if (typeof this.question.amdArgs[1] != 'undefined') { + this.question.readOnly = this.question.amdArgs[1]; + } + if (typeof this.question.amdArgs[2] != 'undefined') { + this.drops = this.question.amdArgs[2]; + } } this.question.loaded = false; diff --git a/src/addon/qtype/ddmarker/classes/ddmarker.ts b/src/addon/qtype/ddmarker/classes/ddmarker.ts index 6ed53b45b..46cb4909f 100644 --- a/src/addon/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addon/qtype/ddmarker/classes/ddmarker.ts @@ -65,9 +65,11 @@ export class AddonQtypeDdMarkerQuestion { * @param {any} question The question instance. * @param {boolean} readOnly Whether it's read only. * @param {any[]} dropZones The drop zones received in the init object of the question. + * @param {string} [imgSrc] Background image source (3.6+ sites). */ constructor(logger: CoreLoggerProvider, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - protected container: HTMLElement, protected question: any, protected readOnly: boolean, protected dropZones: any[]) { + protected container: HTMLElement, protected question: any, protected readOnly: boolean, protected dropZones: any[], + protected imgSrc?: string) { this.logger = logger.getInstance('AddonQtypeDdMarkerQuestion'); this.graphics = new AddonQtypeDdMarkerGraphicsApi(this, this.domUtils); @@ -645,7 +647,7 @@ export class AddonQtypeDdMarkerQuestion { // Wait the DOM to be rendered. setTimeout(() => { - this.pollForImageLoad(question.scriptsCode, question.slot); + this.pollForImageLoad(); }); this.resizeFunction = this.redrawDragsAndDrops.bind(this); @@ -706,24 +708,16 @@ export class AddonQtypeDdMarkerQuestion { /** * Wait for the background image to be loaded. - * - * @param {string} scriptsCode Scripts code to fetch background image. - * @param {number} slot Question number also named slot. */ - pollForImageLoad(scriptsCode?: string, slot?: number): void { + pollForImageLoad(): void { if (this.afterImageLoadDone) { // Already treated. return; } const bgImg = this.doc.bgImg(); - - if (scriptsCode && !bgImg.src) { - const regExp = RegExp('amd\\.init\\("q' + slot + '",[ ]*"([^"]*)"', 'g'), - match = regExp.exec(scriptsCode); - if (match && match.length > 1) { - bgImg.src = match[1]; - } + if (!bgImg.src && this.imgSrc) { + bgImg.src = this.imgSrc; } const imgLoaded = (): void => { diff --git a/src/addon/qtype/ddmarker/component/ddmarker.ts b/src/addon/qtype/ddmarker/component/ddmarker.ts index 518a8b885..d21555a03 100644 --- a/src/addon/qtype/ddmarker/component/ddmarker.ts +++ b/src/addon/qtype/ddmarker/component/ddmarker.ts @@ -15,6 +15,9 @@ import { Component, OnInit, OnDestroy, Injector, ElementRef, ViewChild } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker'; /** @@ -30,9 +33,12 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple protected element: HTMLElement; protected questionInstance: AddonQtypeDdMarkerQuestion; protected dropZones: any[]; // The drop zones received in the init object of the question. + protected imgSrc: string; // Background image URL. protected destroyed = false; - constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) { + constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef, + protected sitesProvider: CoreSitesProvider, protected urlUtils: CoreUrlUtilsProvider, + protected filepoolProvider: CoreFilepoolProvider) { super(loggerProvider, 'AddonQtypeDdMarkerComponent', injector); this.element = element.nativeElement; @@ -72,12 +78,24 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple this.question.readOnly = false; if (this.question.initObjects) { + // Moodle version <= 3.5. if (typeof this.question.initObjects.dropzones != 'undefined') { this.dropZones = this.question.initObjects.dropzones; } if (typeof this.question.initObjects.readonly != 'undefined') { this.question.readOnly = this.question.initObjects.readonly; } + } else if (this.question.amdArgs) { + // Moodle version >= 3.6. + if (typeof this.question.amdArgs[1] != 'undefined') { + this.imgSrc = this.question.amdArgs[1]; + } + if (typeof this.question.amdArgs[2] != 'undefined') { + this.question.readOnly = this.question.amdArgs[2]; + } + if (typeof this.question.amdArgs[3] != 'undefined') { + this.dropZones = this.question.amdArgs[3]; + } } this.question.loaded = false; @@ -88,10 +106,21 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple */ questionRendered(): void { if (!this.destroyed) { - this.domUtils.waitForImages(this.questionTextEl.nativeElement).then(() => { - // Create the instance. - this.questionInstance = new AddonQtypeDdMarkerQuestion(this.loggerProvider, this.domUtils, this.textUtils, - this.element, this.question, this.question.readOnly, this.dropZones); + // Download background image (3.6+ sites). + let promise = null; + const site = this.sitesProvider.getCurrentSite(); + if (this.imgSrc && site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(this.imgSrc)) { + promise = this.filepoolProvider.getSrcByUrl(site.id, this.imgSrc, this.component, this.componentId, 0, true, true); + } else { + promise = Promise.resolve(this.imgSrc); + } + + promise.then((imgSrc) => { + this.domUtils.waitForImages(this.questionTextEl.nativeElement).then(() => { + // Create the instance. + this.questionInstance = new AddonQtypeDdMarkerQuestion(this.loggerProvider, this.domUtils, this.textUtils, + this.element, this.question, this.question.readOnly, this.dropZones, imgSrc); + }); }); } } diff --git a/src/addon/qtype/ddmarker/providers/handler.ts b/src/addon/qtype/ddmarker/providers/handler.ts index 04b1f2926..aaf0c781f 100644 --- a/src/addon/qtype/ddmarker/providers/handler.ts +++ b/src/addon/qtype/ddmarker/providers/handler.ts @@ -16,6 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { CoreQuestionProvider } from '@core/question/providers/question'; import { CoreQuestionHandler } from '@core/question/providers/delegate'; +import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { AddonQtypeDdMarkerComponent } from '../component/ddmarker'; /** @@ -26,7 +27,7 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { name = 'AddonQtypeDdMarker'; type = 'qtype_ddmarker'; - constructor(private questionProvider: CoreQuestionProvider) { } + constructor(private questionProvider: CoreQuestionProvider, private questionHelper: CoreQuestionHelperProvider) { } /** * Return the name of the behaviour to use for the question. @@ -106,4 +107,21 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } + + /** + * Get the list of files that needs to be downloaded in addition to the files embedded in the HTML. + * + * @param {any} question Question. + * @return {string[]} List of URLs. + */ + getAdditionalDownloadableFiles(question: any): string[] { + this.questionHelper.extractQuestionScripts(question); + + if (question.amdArgs && typeof question.amdArgs[1] !== 'undefined') { + // Moodle 3.6+. + return [question.amdArgs[1]]; + } + + return []; + } } diff --git a/src/core/question/providers/delegate.ts b/src/core/question/providers/delegate.ts index 54637e90c..2d3451beb 100644 --- a/src/core/question/providers/delegate.ts +++ b/src/core/question/providers/delegate.ts @@ -107,6 +107,14 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * @return {boolean} Whether sequencecheck is valid. */ validateSequenceCheck?(question: any, offlineSequenceCheck: string): boolean; + + /** + * Get the list of files that needs to be downloaded in addition to the files embedded in the HTML. + * + * @param {any} question Question. + * @return {string[]} List of URLs. + */ + getAdditionalDownloadableFiles?(question: any): string[]; } /** @@ -261,4 +269,16 @@ export class CoreQuestionDelegate extends CoreDelegate { return this.executeFunctionOnEnabled(type, 'validateSequenceCheck', [question, offlineSequenceCheck]); } + + /** + * Get the list of files that needs to be downloaded in addition to the files embedded in the HTML. + * + * @param {any} question Question. + * @return {string[]} List of URLs. + */ + getAdditionalDownloadableFiles(question: any): string[] { + const type = this.getTypeName(question); + + return this.executeFunctionOnEnabled(type, 'getAdditionalDownloadableFiles', [question]) || []; + } } diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts index 106326f61..cb0fe8eb1 100644 --- a/src/core/question/providers/helper.ts +++ b/src/core/question/providers/helper.ts @@ -242,7 +242,8 @@ export class CoreQuestionHelperProvider { */ extractQuestionScripts(question: any): void { question.scriptsCode = ''; - question.initObjects = []; + question.initObjects = null; + question.amdArgs = null; if (question.html) { // Search the scripts. @@ -267,7 +268,15 @@ export class CoreQuestionHelperProvider { initMatch = initMatch.substr(0, initMatch.length - 2); // Try to convert it to an object and add it to the question. - question.initObjects = this.textUtils.parseJSON(initMatch); + question.initObjects = this.textUtils.parseJSON(initMatch, null); + } + + const amdRegExp = new RegExp('require\\(\\["qtype_' + question.type + '/question"\\], ' + + 'function\\(amd\\) \\{ amd\.init\\(("q' + question.slot + '".*?)\\); \\}\\);;', 'm'); + const amdMatch = match.match(amdRegExp); + if (amdMatch) { + // Try to convert the arguments to an array and add them to the question. + question.amdArgs = this.textUtils.parseJSON('[' + amdMatch[1] + ']', null); } }); } @@ -507,6 +516,8 @@ export class CoreQuestionHelperProvider { componentId = question.id; } + urls.push(...this.questionDelegate.getAdditionalDownloadableFiles(question)); + return this.sitesProvider.getSite(siteId).then((site) => { const promises = [];