forked from EVOgeek/Vmeda.Online
		
	MOBILE-2272 quiz: Support viewing attachments and inline files
This commit is contained in:
		
							parent
							
								
									b6ed831b25
								
							
						
					
					
						commit
						3f40127661
					
				| @ -1861,6 +1861,7 @@ | |||||||
|   "core.mainmenu.help": "moodle", |   "core.mainmenu.help": "moodle", | ||||||
|   "core.mainmenu.logout": "moodle", |   "core.mainmenu.logout": "moodle", | ||||||
|   "core.mainmenu.website": "local_moodlemobileapp", |   "core.mainmenu.website": "local_moodlemobileapp", | ||||||
|  |   "core.maxfilesize": "moodle", | ||||||
|   "core.maxsizeandattachments": "moodle", |   "core.maxsizeandattachments": "moodle", | ||||||
|   "core.min": "moodle", |   "core.min": "moodle", | ||||||
|   "core.mins": "moodle", |   "core.mins": "moodle", | ||||||
| @ -1944,8 +1945,8 @@ | |||||||
|   "core.question.certainty": "qbehaviour_deferredcbm", |   "core.question.certainty": "qbehaviour_deferredcbm", | ||||||
|   "core.question.complete": "question", |   "core.question.complete": "question", | ||||||
|   "core.question.correct": "question", |   "core.question.correct": "question", | ||||||
|   "core.question.errorattachmentsnotsupported": "local_moodlemobileapp", |   "core.question.errorattachmentsnotsupportedinsite": "local_moodlemobileapp", | ||||||
|   "core.question.errorinlinefilesnotsupported": "local_moodlemobileapp", |   "core.question.errorembeddedfilesnotsupportedinsite": "local_moodlemobileapp", | ||||||
|   "core.question.errorquestionnotsupported": "local_moodlemobileapp", |   "core.question.errorquestionnotsupported": "local_moodlemobileapp", | ||||||
|   "core.question.feedback": "question", |   "core.question.feedback": "question", | ||||||
|   "core.question.howtodraganddrop": "local_moodlemobileapp", |   "core.question.howtodraganddrop": "local_moodlemobileapp", | ||||||
|  | |||||||
| @ -411,7 +411,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid | |||||||
|                                 const params = this.urlUtils.extractUrlParams(response.data.reviewlesson.value); |                                 const params = this.urlUtils.extractUrlParams(response.data.reviewlesson.value); | ||||||
|                                 if (params && params.pageid) { |                                 if (params && params.pageid) { | ||||||
|                                     // The retake can be reviewed, mark it as finished. Don't block the user for this.
 |                                     // The retake can be reviewed, mark it as finished. Don't block the user for this.
 | ||||||
|                                     this.setRetakeFinishedInSync(lessonId, retake.retake, params.pageid, siteId); |                                     this.setRetakeFinishedInSync(lessonId, retake.retake, Number(params.pageid), siteId); | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  | |||||||
| @ -5,9 +5,10 @@ | |||||||
|     </ion-item> |     </ion-item> | ||||||
| 
 | 
 | ||||||
|     <!-- Textarea. --> |     <!-- Textarea. --> | ||||||
|     <ion-item *ngIf="question.textarea && !question.hasDraftFiles"> |     <ion-item *ngIf="question.textarea && (!question.hasDraftFiles || uploadFilesSupported)"> | ||||||
|         <!-- "Format" hidden input --> |         <!-- "Format" and draftid hidden inputs --> | ||||||
|         <input item-content *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value" > |         <input item-content *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value" > | ||||||
|  |         <input item-content *ngIf="question.answerDraftIdInput" type="hidden" [name]="question.answerDraftIdInput.name" [value]="question.answerDraftIdInput.value" > | ||||||
|         <!-- Plain text textarea. --> |         <!-- Plain text textarea. --> | ||||||
|         <ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true" [ngModel]="question.textarea.text"></ion-textarea> |         <ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true" [ngModel]="question.textarea.text"></ion-textarea> | ||||||
|         <!-- Rich text editor. --> |         <!-- Rich text editor. --> | ||||||
| @ -15,22 +16,29 @@ | |||||||
|     </ion-item> |     </ion-item> | ||||||
| 
 | 
 | ||||||
|     <!-- Draft files not supported. --> |     <!-- Draft files not supported. --> | ||||||
|     <ng-container *ngIf="question.textarea && question.hasDraftFiles"> |     <ng-container *ngIf="question.textarea && question.hasDraftFiles && !uploadFilesSupported"> | ||||||
|         <ion-item text-wrap class="core-danger-item"> |         <ion-item text-wrap class="core-danger-item"> | ||||||
|             <p class="core-question-warning">{{ 'core.question.errorinlinefilesnotsupported' | translate }}</p> |             <p class="core-question-warning">{{ 'core.question.errorinlinefilesnotsupportedinsite' | translate }}</p> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|         <ion-item text-wrap> |         <ion-item text-wrap> | ||||||
|             <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.textarea.text" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"></core-format-text></p> |             <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.textarea.text" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"></core-format-text></p> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|     </ng-container> |     </ng-container> | ||||||
| 
 | 
 | ||||||
|     <!-- Attachments not supported in the app yet. --> |     <!-- Attachments. --> | ||||||
|     <ion-item text-wrap *ngIf="question.allowsAttachments" class="core-danger-item"> |     <ng-container *ngIf="question.allowsAttachments"> | ||||||
|         <p class="core-question-warning">{{ 'core.question.errorattachmentsnotsupported' | translate }}</p> |         <core-attachments *ngIf="uploadFilesSupported" [files]="attachments" [component]="component" [componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles"></core-attachments> | ||||||
|     </ion-item> | 
 | ||||||
|  |         <input item-content *ngIf="uploadFilesSupported" type="hidden" [name]="question.attachmentsDraftIdInput.name" [value]="question.attachmentsDraftIdInput.value" > | ||||||
|  | 
 | ||||||
|  |         <!-- Attachments not supported in this site. --> | ||||||
|  |         <ion-item text-wrap *ngIf="!uploadFilesSupported" class="core-danger-item"> | ||||||
|  |             <p class="core-question-warning">{{ 'core.question.errorattachmentsnotsupportedinsite' | translate }}</p> | ||||||
|  |         </ion-item> | ||||||
|  |     </ng-container> | ||||||
| 
 | 
 | ||||||
|     <!-- Answer to the question and attachments (reviewing). --> |     <!-- Answer to the question and attachments (reviewing). --> | ||||||
|     <ion-item text-wrap *ngIf="!question.textarea && (question.answer || (!question.attachments.length && !question.allowsAttachments))"> |     <ion-item text-wrap *ngIf="!question.textarea && (question.answer || question.answer == '')"> | ||||||
|         <p><core-format-text [ngClass]='{"core-monospaced": question.isMonospaced}' [component]="component" [componentId]="componentId" [text]="question.answer" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"></core-format-text></p> |         <p><core-format-text [ngClass]='{"core-monospaced": question.isMonospaced}' [component]="component" [componentId]="componentId" [text]="question.answer" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"></core-format-text></p> | ||||||
|     </ion-item> |     </ion-item> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,8 +14,11 @@ | |||||||
| 
 | 
 | ||||||
| import { Component, OnInit, Injector } from '@angular/core'; | import { Component, OnInit, Injector } from '@angular/core'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
|  | import { CoreSites } from '@providers/sites'; | ||||||
|  | import { CoreWSExternalFile } from '@providers/ws'; | ||||||
| import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; | import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; | ||||||
| import { FormControl, FormBuilder } from '@angular/forms'; | import { FormControl, FormBuilder } from '@angular/forms'; | ||||||
|  | import { CoreFileSession } from '@providers/file-session'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component to render an essay question. |  * Component to render an essay question. | ||||||
| @ -28,6 +31,9 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen | |||||||
| 
 | 
 | ||||||
|     protected formControl: FormControl; |     protected formControl: FormControl; | ||||||
| 
 | 
 | ||||||
|  |     attachments: CoreWSExternalFile[]; | ||||||
|  |     uploadFilesSupported: boolean; | ||||||
|  | 
 | ||||||
|     constructor(logger: CoreLoggerProvider, injector: Injector, protected fb: FormBuilder) { |     constructor(logger: CoreLoggerProvider, injector: Injector, protected fb: FormBuilder) { | ||||||
|         super(logger, 'AddonQtypeEssayComponent', injector); |         super(logger, 'AddonQtypeEssayComponent', injector); | ||||||
|     } |     } | ||||||
| @ -36,8 +42,14 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen | |||||||
|      * Component being initialized. |      * Component being initialized. | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|  |         this.uploadFilesSupported = CoreSites.instance.getCurrentSite().isVersionGreaterEqualThan('3.10'); | ||||||
|         this.initEssayComponent(); |         this.initEssayComponent(); | ||||||
| 
 | 
 | ||||||
|         this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text); |         this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text); | ||||||
|  | 
 | ||||||
|  |         if (this.question.allowsAttachments && this.uploadFilesSupported) { | ||||||
|  |             this.attachments = Array.from(this.questionHelper.getResponseFileAreaFiles(this.question, 'attachments')); | ||||||
|  |             CoreFileSession.instance.setFiles(this.component, this.componentId + '_' + this.question.id, this.attachments); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Injectable, Injector } from '@angular/core'; | import { Injectable, Injector } from '@angular/core'; | ||||||
|  | import { CoreSites } from '@providers/sites'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| @ -68,11 +69,11 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { | |||||||
| 
 | 
 | ||||||
|         if (element.querySelector('div[id*=filemanager]')) { |         if (element.querySelector('div[id*=filemanager]')) { | ||||||
|             // The question allows attachments. Since the app cannot attach files yet we will prevent submitting the question.
 |             // The question allows attachments. Since the app cannot attach files yet we will prevent submitting the question.
 | ||||||
|             return 'core.question.errorattachmentsnotsupported'; |             return 'core.question.errorattachmentsnotsupportedinsite'; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.questionHelper.hasDraftFileUrls(element.innerHTML)) { |         if (this.questionHelper.hasDraftFileUrls(element.innerHTML)) { | ||||||
|             return 'core.question.errorinlinefilesnotsupported'; |             return 'core.question.errorinlinefilesnotsupportedinsite'; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -139,13 +140,21 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { | |||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Return a promise resolved when done if async, void if sync. |      * @return Return a promise resolved when done if async, void if sync. | ||||||
|      */ |      */ | ||||||
|     prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise<any> { |     async prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): Promise<void> { | ||||||
|         const element = this.domUtils.convertToElement(question.html); |         const element = this.domUtils.convertToElement(question.html); | ||||||
| 
 | 
 | ||||||
|         // Search the textarea to get its name.
 |         // Search the textarea to get its name.
 | ||||||
|         const textarea = <HTMLTextAreaElement> element.querySelector('textarea[name*=_answer]'); |         const textarea = <HTMLTextAreaElement> element.querySelector('textarea[name*=_answer]'); | ||||||
| 
 | 
 | ||||||
|         if (textarea && typeof answers[textarea.name] != 'undefined') { |         if (textarea && typeof answers[textarea.name] != 'undefined') { | ||||||
|  |             if (this.questionHelper.hasDraftFileUrls(question.html) && question.responsefileareas) { | ||||||
|  |                 // Restore draftfile URLs.
 | ||||||
|  |                 const site = await CoreSites.instance.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |                 answers[textarea.name] = this.textUtils.restoreDraftfileUrls(site.getURL(), answers[textarea.name], | ||||||
|  |                         question.html, this.questionHelper.getResponseFileAreaFiles(question, 'answer')); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             // Add some HTML to the text if needed.
 |             // Add some HTML to the text if needed.
 | ||||||
|             answers[textarea.name] = this.textUtils.formatHtmlLines(answers[textarea.name]); |             answers[textarea.name] = this.textUtils.formatHtmlLines(answers[textarea.name]); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -1945,8 +1945,8 @@ | |||||||
|     "core.question.certainty": "Certainty", |     "core.question.certainty": "Certainty", | ||||||
|     "core.question.complete": "Complete", |     "core.question.complete": "Complete", | ||||||
|     "core.question.correct": "Correct", |     "core.question.correct": "Correct", | ||||||
|     "core.question.errorattachmentsnotsupported": "The application doesn't support attaching files to answers yet.", |     "core.question.errorattachmentsnotsupportedinsite": "Your site doesn't support attaching files to answers yet.", | ||||||
|     "core.question.errorinlinefilesnotsupported": "The application doesn't support editing inline files yet.", |     "core.question.errorinlinefilesnotsupportedinsite": "Your site doesn't support editing inline files yet.", | ||||||
|     "core.question.errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.", |     "core.question.errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.", | ||||||
|     "core.question.feedback": "Feedback", |     "core.question.feedback": "Feedback", | ||||||
|     "core.question.howtodraganddrop": "Tap to select then tap to drop.", |     "core.question.howtodraganddrop": "Tap to select then tap to drop.", | ||||||
|  | |||||||
| @ -221,13 +221,16 @@ export class CoreSite { | |||||||
| 
 | 
 | ||||||
|     // Versions of Moodle releases.
 |     // Versions of Moodle releases.
 | ||||||
|     protected MOODLE_RELEASES = { |     protected MOODLE_RELEASES = { | ||||||
|         3.1: 2016052300, |         '3.1': 2016052300, | ||||||
|         3.2: 2016120500, |         '3.2': 2016120500, | ||||||
|         3.3: 2017051503, |         '3.3': 2017051503, | ||||||
|         3.4: 2017111300, |         '3.4': 2017111300, | ||||||
|         3.5: 2018051700, |         '3.5': 2018051700, | ||||||
|         3.6: 2018120300, |         '3.6': 2018120300, | ||||||
|         3.7: 2019052000 |         '3.7': 2019052000, | ||||||
|  |         '3.8': 2019111800, | ||||||
|  |         '3.9': 2020061500, | ||||||
|  |         '3.10': 2020092400, // @todo: Replace with the right value once 3.10 is released.
 | ||||||
|     }; |     }; | ||||||
|     static MINIMUM_MOODLE_VERSION = '3.1'; |     static MINIMUM_MOODLE_VERSION = '3.1'; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -56,6 +56,7 @@ ion-app.app-root core-rich-text-editor { | |||||||
|         img { |         img { | ||||||
|             @include padding(null, null, null, 2px); |             @include padding(null, null, null, 2px); | ||||||
|             max-width: 95%; |             max-width: 95%; | ||||||
|  |             width: auto; | ||||||
|         } |         } | ||||||
|         &:empty:before { |         &:empty:before { | ||||||
|             content: attr(data-placeholder-text); |             content: attr(data-placeholder-text); | ||||||
|  | |||||||
| @ -407,7 +407,7 @@ export class CoreGradesHelperProvider { | |||||||
|                     if (matches && matches.length) { |                     if (matches && matches.length) { | ||||||
|                         const hrefParams = this.urlUtils.extractUrlParams(matches[1]); |                         const hrefParams = this.urlUtils.extractUrlParams(matches[1]); | ||||||
| 
 | 
 | ||||||
|                         return hrefParams && hrefParams.id == moduleId; |                         return hrefParams && Number(hrefParams.id) == moduleId; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1152,7 +1152,7 @@ export class CoreLoginHelperProvider { | |||||||
|                     const providerToUse = identityProviders.find((provider) => { |                     const providerToUse = identityProviders.find((provider) => { | ||||||
|                         const params = this.urlUtils.extractUrlParams(provider.url); |                         const params = this.urlUtils.extractUrlParams(provider.url); | ||||||
| 
 | 
 | ||||||
|                         return params.id == currentSite.getOAuthId(); |                         return Number(params.id) == currentSite.getOAuthId(); | ||||||
|                     }); |                     }); | ||||||
| 
 | 
 | ||||||
|                     if (providerToUse) { |                     if (providerToUse) { | ||||||
|  | |||||||
| @ -14,8 +14,10 @@ | |||||||
| 
 | 
 | ||||||
| import { Input, Output, EventEmitter, Injector, ElementRef } from '@angular/core'; | import { Input, Output, EventEmitter, Injector, ElementRef } from '@angular/core'; | ||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
|  | import { CoreSites } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
|  | import { CoreUrlUtils } from '@providers/utils/url'; | ||||||
| import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; | import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -162,6 +164,8 @@ export class CoreQuestionBaseComponent { | |||||||
|             const input = questionEl.querySelector('input[type="text"][name*=answer]'); |             const input = questionEl.querySelector('input[type="text"][name*=answer]'); | ||||||
|             this.question.optionsFirst = |             this.question.optionsFirst = | ||||||
|                     questionEl.innerHTML.indexOf(input.outerHTML) > questionEl.innerHTML.indexOf(radios[0].outerHTML); |                     questionEl.innerHTML.indexOf(input.outerHTML) > questionEl.innerHTML.indexOf(radios[0].outerHTML); | ||||||
|  | 
 | ||||||
|  |             return questionEl; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -201,27 +205,38 @@ export class CoreQuestionBaseComponent { | |||||||
|         const questionEl = this.initComponent(); |         const questionEl = this.initComponent(); | ||||||
| 
 | 
 | ||||||
|         if (questionEl) { |         if (questionEl) { | ||||||
|             // First search the textarea.
 |  | ||||||
|             const textarea = <HTMLTextAreaElement> questionEl.querySelector('textarea[name*=_answer]'); |             const textarea = <HTMLTextAreaElement> questionEl.querySelector('textarea[name*=_answer]'); | ||||||
|  |             const answerDraftIdInput = <HTMLInputElement> questionEl.querySelector('input[name*="_answer:itemid"]'); | ||||||
|  | 
 | ||||||
|             this.question.allowsAttachments = !!questionEl.querySelector('div[id*=filemanager]'); |             this.question.allowsAttachments = !!questionEl.querySelector('div[id*=filemanager]'); | ||||||
|  |             this.question.allowsAnswerFiles = !!answerDraftIdInput; | ||||||
|             this.question.isMonospaced = !!questionEl.querySelector('.qtype_essay_monospaced'); |             this.question.isMonospaced = !!questionEl.querySelector('.qtype_essay_monospaced'); | ||||||
|             this.question.isPlainText = this.question.isMonospaced || !!questionEl.querySelector('.qtype_essay_plain'); |             this.question.isPlainText = this.question.isMonospaced || !!questionEl.querySelector('.qtype_essay_plain'); | ||||||
|             this.question.hasDraftFiles = this.questionHelper.hasDraftFileUrls(questionEl.innerHTML); |             this.question.hasDraftFiles = this.question.allowsAnswerFiles && | ||||||
|  |                     this.questionHelper.hasDraftFileUrls(questionEl.innerHTML); | ||||||
| 
 | 
 | ||||||
|             if (!textarea) { |             if (!textarea && !this.question.allowsAttachments) { | ||||||
|                 // Textarea not found, we might be in review. Search the answer and the attachments.
 |                 // Textarea and filemanager not found, we might be in review. Search the answer and the attachments.
 | ||||||
|                 this.question.answer = this.domUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); |                 this.question.answer = this.domUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); | ||||||
|                 this.question.attachments = this.questionHelper.getQuestionAttachmentsFromHtml( |                 this.question.attachments = this.questionHelper.getQuestionAttachmentsFromHtml( | ||||||
|                         this.domUtils.getContentsOfElement(questionEl, '.attachments')); |                         this.domUtils.getContentsOfElement(questionEl, '.attachments')); | ||||||
|             } else { | 
 | ||||||
|                 // Textarea found.
 |                 return questionEl; | ||||||
|                 const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=answerformat]'), |             } | ||||||
|                     content = textarea.innerHTML; | 
 | ||||||
|  |             if (textarea) { | ||||||
|  |                 const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=answerformat]'); | ||||||
|  |                 let content = this.textUtils.decodeHTML(textarea.innerHTML || ''); | ||||||
|  | 
 | ||||||
|  |                 if (this.question.hasDraftFiles && this.question.responsefileareas) { | ||||||
|  |                     content = this.textUtils.replaceDraftfileUrls(CoreSites.instance.getCurrentSite().getURL(), content, | ||||||
|  |                             this.questionHelper.getResponseFileAreaFiles(this.question, 'answer')).text; | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 this.question.textarea = { |                 this.question.textarea = { | ||||||
|                     id: textarea.id, |                     id: textarea.id, | ||||||
|                     name: textarea.name, |                     name: textarea.name, | ||||||
|                     text: content ? this.textUtils.decodeHTML(content) : '' |                     text: content, | ||||||
|                 }; |                 }; | ||||||
| 
 | 
 | ||||||
|                 if (input) { |                 if (input) { | ||||||
| @ -231,6 +246,38 @@ export class CoreQuestionBaseComponent { | |||||||
|                     }; |                     }; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             if (answerDraftIdInput) { | ||||||
|  |                 this.question.answerDraftIdInput = { | ||||||
|  |                     name: answerDraftIdInput.name, | ||||||
|  |                     value: Number(answerDraftIdInput.value), | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (this.question.allowsAttachments) { | ||||||
|  |                 const attachmentsInput = <HTMLInputElement> questionEl.querySelector('.attachments input[name*=_attachments]'); | ||||||
|  |                 const objectElement = <HTMLObjectElement> questionEl.querySelector('.attachments object'); | ||||||
|  |                 const fileManagerUrl = objectElement && objectElement.data; | ||||||
|  | 
 | ||||||
|  |                 if (attachmentsInput) { | ||||||
|  |                     this.question.attachmentsDraftIdInput = { | ||||||
|  |                         name: attachmentsInput.name, | ||||||
|  |                         value: Number(attachmentsInput.value), | ||||||
|  |                     }; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (fileManagerUrl) { | ||||||
|  |                     const params = CoreUrlUtils.instance.extractUrlParams(fileManagerUrl); | ||||||
|  |                     const maxBytes = Number(params.maxbytes); | ||||||
|  |                     const areaMaxBytes = Number(params.areamaxbytes); | ||||||
|  | 
 | ||||||
|  |                     this.question.attachmentsMaxFiles = Number(params.maxfiles); | ||||||
|  |                     this.question.attachmentsMaxBytes = maxBytes === -1 || areaMaxBytes === -1 ? | ||||||
|  |                             Math.max(maxBytes, areaMaxBytes) : Math.min(maxBytes, areaMaxBytes); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return questionEl; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -270,6 +317,8 @@ export class CoreQuestionBaseComponent { | |||||||
| 
 | 
 | ||||||
|         // Set the question text.
 |         // Set the question text.
 | ||||||
|         this.question.text = content.innerHTML; |         this.question.text = content.innerHTML; | ||||||
|  | 
 | ||||||
|  |         return element; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -5,8 +5,8 @@ | |||||||
|     "certainty": "Certainty", |     "certainty": "Certainty", | ||||||
|     "complete": "Complete", |     "complete": "Complete", | ||||||
|     "correct": "Correct", |     "correct": "Correct", | ||||||
|     "errorattachmentsnotsupported": "The application doesn't support attaching files to answers yet.", |     "errorattachmentsnotsupportedinsite": "Your site doesn't support attaching files to answers yet.", | ||||||
|     "errorinlinefilesnotsupported": "The application doesn't support editing inline files yet.", |     "errorinlinefilesnotsupportedinsite": "Your site doesn't support editing inline files yet.", | ||||||
|     "errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.", |     "errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.", | ||||||
|     "feedback": "Feedback", |     "feedback": "Feedback", | ||||||
|     "howtodraganddrop": "Tap to select then tap to drop.", |     "howtodraganddrop": "Tap to select then tap to drop.", | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ import { Injectable, EventEmitter } from '@angular/core'; | |||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
| import { CoreFilepoolProvider } from '@providers/filepool'; | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider } from '@providers/sites'; | ||||||
|  | import { CoreWSExternalFile } from '@providers/ws'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| import { CoreUrlUtilsProvider } from '@providers/utils/url'; | import { CoreUrlUtilsProvider } from '@providers/utils/url'; | ||||||
| @ -421,6 +422,25 @@ export class CoreQuestionHelperProvider { | |||||||
|         return state ? state.class : ''; |         return state ? state.class : ''; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the files of a certain response file area. | ||||||
|  |      * | ||||||
|  |      * @param question Question. | ||||||
|  |      * @param areaName Name of the area, e.g. 'attachments'. | ||||||
|  |      * @return List of files. | ||||||
|  |      */ | ||||||
|  |     getResponseFileAreaFiles(question: any, areaName: string): CoreWSExternalFile[] { | ||||||
|  |         if (!question.responsefileareas) { | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const area = question.responsefileareas.find((area) => { | ||||||
|  |             return area.area == areaName; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return area && area.files || []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Get the validation error message from a question HTML if it's there. |      * Get the validation error message from a question HTML if it's there. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ import { TranslateService } from '@ngx-translate/core'; | |||||||
| import { CoreLangProvider } from '../lang'; | import { CoreLangProvider } from '../lang'; | ||||||
| import { makeSingleton } from '@singletons/core.singletons'; | import { makeSingleton } from '@singletons/core.singletons'; | ||||||
| import { CoreApp } from '../app'; | import { CoreApp } from '../app'; | ||||||
|  | import { CoreWSExternalFile } from '../ws'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Different type of errors the app can treat. |  * Different type of errors the app can treat. | ||||||
| @ -699,6 +700,60 @@ export class CoreTextUtilsProvider { | |||||||
|         return text.replace(/(?:\r\n|\r|\n)/g, newValue); |         return text.replace(/(?:\r\n|\r|\n)/g, newValue); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Replace draftfile URLs with the equivalent pluginfile URL. | ||||||
|  |      * | ||||||
|  |      * @param siteUrl URL of the site. | ||||||
|  |      * @param text Text to treat, including draftfile URLs. | ||||||
|  |      * @param files List of files of the area, using pluginfile URLs. | ||||||
|  |      * @return Treated text and map with the replacements. | ||||||
|  |      */ | ||||||
|  |     replaceDraftfileUrls(siteUrl: string, text: string, files: CoreWSExternalFile[]) | ||||||
|  |             : {text: string, replaceMap?: {[url: string]: string}} { | ||||||
|  | 
 | ||||||
|  |         if (!text || !files || !files.length) { | ||||||
|  |             return {text}; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const draftfileUrl = this.concatenatePaths(siteUrl, 'draftfile.php'); | ||||||
|  |         const matches = text.match(new RegExp(this.escapeForRegex(draftfileUrl) + '[^\'" ]+', 'ig')); | ||||||
|  | 
 | ||||||
|  |         if (!matches || !matches.length) { | ||||||
|  |             return {text}; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Index the pluginfile URLs by file name.
 | ||||||
|  |         const pluginfileMap: {[name: string]: string} = {}; | ||||||
|  |         files.forEach((file) => { | ||||||
|  |             pluginfileMap[file.filename] = file.fileurl; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Replace each draftfile with the corresponding pluginfile URL.
 | ||||||
|  |         const replaceMap: {[url: string]: string} = {}; | ||||||
|  |         matches.forEach((url) => { | ||||||
|  |             if (replaceMap[url]) { | ||||||
|  |                 // URL already treated, same file embedded more than once.
 | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Get the filename from the URL.
 | ||||||
|  |             let filename = url.substr(url.lastIndexOf('/') + 1); | ||||||
|  |             if (filename.indexOf('?') != -1) { | ||||||
|  |                 filename = filename.substr(0, filename.indexOf('?')); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (pluginfileMap[filename]) { | ||||||
|  |                 replaceMap[url] = pluginfileMap[filename]; | ||||||
|  |                 text = text.replace(new RegExp(this.escapeForRegex(url), 'g'), pluginfileMap[filename]); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             text, | ||||||
|  |             replaceMap, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Replace @@PLUGINFILE@@ wildcards with the real URL in a text. |      * Replace @@PLUGINFILE@@ wildcards with the real URL in a text. | ||||||
|      * |      * | ||||||
| @ -717,6 +772,36 @@ export class CoreTextUtilsProvider { | |||||||
|         return text; |         return text; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Restore original draftfile URLs. | ||||||
|  |      * | ||||||
|  |      * @param text Text to treat, including pluginfile URLs. | ||||||
|  |      * @param replaceMap Map of the replacements that were done. | ||||||
|  |      * @return Treated text. | ||||||
|  |      */ | ||||||
|  |     restoreDraftfileUrls(siteUrl: string, treatedText: string, originalText: string, files: CoreWSExternalFile[]): string { | ||||||
|  |         if (!treatedText || !files || !files.length) { | ||||||
|  |             return treatedText; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const draftfileUrl = this.concatenatePaths(siteUrl, 'draftfile.php'); | ||||||
|  |         const draftfileUrlRegexPrefix = this.escapeForRegex(draftfileUrl) + '/[^/]+/[^/]+/[^/]+/[^/]+/'; | ||||||
|  | 
 | ||||||
|  |         files.forEach((file) => { | ||||||
|  |             // Search the draftfile URL in the original text.
 | ||||||
|  |             const matches = originalText.match(new RegExp( | ||||||
|  |                     draftfileUrlRegexPrefix + this.escapeForRegex(file.filename) + '[^\'" ]*', 'i')); | ||||||
|  | 
 | ||||||
|  |             if (!matches || !matches[0]) { | ||||||
|  |                 return; // Original URL not found, skip.
 | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             treatedText = treatedText.replace(new RegExp(this.escapeForRegex(file.fileurl), 'g'), matches[0]); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return treatedText; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Replace pluginfile URLs with @@PLUGINFILE@@ wildcards. |      * Replace pluginfile URLs with @@PLUGINFILE@@ wildcards. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -114,10 +114,10 @@ export class CoreUrlUtilsProvider { | |||||||
|      * @param url URL to treat. |      * @param url URL to treat. | ||||||
|      * @return Object with the params. |      * @return Object with the params. | ||||||
|      */ |      */ | ||||||
|     extractUrlParams(url: string): any { |     extractUrlParams(url: string): {[name: string]: string} { | ||||||
|         const regex = /[?&]+([^=&]+)=?([^&]*)?/gi, |         const regex = /[?&]+([^=&]+)=?([^&]*)?/gi, | ||||||
|             subParamsPlaceholder = '@@@SUBPARAMS@@@', |             subParamsPlaceholder = '@@@SUBPARAMS@@@', | ||||||
|             params: any = {}, |             params: {[name: string]: string} = {}, | ||||||
|             urlAndHash = url.split('#'), |             urlAndHash = url.split('#'), | ||||||
|             questionMarkSplit = urlAndHash[0].split('?'); |             questionMarkSplit = urlAndHash[0].split('?'); | ||||||
|         let subParams; |         let subParams; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user