From cbf7214cc3a84ca9de3ea57e5fd8b2cb31144fe6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 9 Jul 2018 10:12:29 +0200 Subject: [PATCH 1/3] MOBILE-2444 media: Prevent multiple requests and EXC_BAD_ACCESS --- src/addon/mod/lesson/providers/helper.ts | 62 ++++++------- src/addon/mod/lesson/providers/lesson.ts | 8 +- src/addon/mod/quiz/providers/helper.ts | 6 +- src/addon/mod/quiz/providers/quiz.ts | 9 +- .../qtype/calculated/providers/handler.ts | 9 +- .../ddimageortext/component/ddimageortext.ts | 7 +- .../qtype/ddmarker/component/ddmarker.ts | 11 +-- src/addon/qtype/ddwtos/component/ddwtos.ts | 15 ++- .../description/component/description.ts | 6 +- src/addon/qtype/essay/providers/handler.ts | 19 ++-- .../classes/base-question-component.ts | 92 +++++++++---------- src/core/question/providers/helper.ts | 52 +++++------ src/providers/utils/dom.ts | 55 +++++++---- src/providers/utils/text.ts | 34 +++++-- 14 files changed, 210 insertions(+), 175 deletions(-) diff --git a/src/addon/mod/lesson/providers/helper.ts b/src/addon/mod/lesson/providers/helper.ts index 2bd49988e..3f7be38d2 100644 --- a/src/addon/mod/lesson/providers/helper.ts +++ b/src/addon/mod/lesson/providers/helper.ts @@ -27,8 +27,6 @@ import * as moment from 'moment'; @Injectable() export class AddonModLessonHelperProvider { - protected div = document.createElement('div'); // A div element to search in HTML code. - constructor(private domUtils: CoreDomUtilsProvider, private fb: FormBuilder, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { } @@ -39,8 +37,9 @@ export class AddonModLessonHelperProvider { * @return {{formatted: boolean, label: string, href: string}} Formatted data. */ formatActivityLink(activityLink: string): {formatted: boolean, label: string, href: string} { - this.div.innerHTML = activityLink; - const anchor = this.div.querySelector('a'); + const element = this.domUtils.convertToElement(activityLink), + anchor = element.querySelector('a'); + if (!anchor) { // Anchor not found, return the original HTML. return { @@ -67,11 +66,11 @@ export class AddonModLessonHelperProvider { const data = { buttonText: '', content: '' - }; + }, + element = this.domUtils.convertToElement(html); // Search the input button. - this.div.innerHTML = html; - const button = this.div.querySelector('input[type="button"]'); + const button = element.querySelector('input[type="button"]'); if (button) { // Extract the button content and remove it from the HTML. @@ -79,7 +78,7 @@ export class AddonModLessonHelperProvider { button.remove(); } - data.content = this.div.innerHTML.trim(); + data.content = element.innerHTML.trim(); return data; } @@ -91,19 +90,19 @@ export class AddonModLessonHelperProvider { * @return {any[]} List of buttons. */ getPageButtonsFromHtml(html: string): any[] { - const buttons = []; + const buttons = [], + element = this.domUtils.convertToElement(html); // Get the container of the buttons if it exists. - this.div.innerHTML = html; - let buttonsContainer = this.div.querySelector('.branchbuttoncontainer'); + let buttonsContainer = element.querySelector('.branchbuttoncontainer'); if (!buttonsContainer) { // Button container not found, might be a legacy lesson (from 1.9). - if (!this.div.querySelector('form input[type="submit"]')) { + if (!element.querySelector('form input[type="submit"]')) { // No buttons found. return buttons; } - buttonsContainer = this.div; + buttonsContainer = element; } const forms = Array.from(buttonsContainer.querySelectorAll('form')); @@ -144,8 +143,8 @@ export class AddonModLessonHelperProvider { */ getPageContentsFromPageData(data: any): string { // Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered. - this.div.innerHTML = data.pagecontent; - const contents = this.div.querySelector('.contents'); + const element = this.domUtils.convertToElement(data.pagecontent), + contents = element.querySelector('.contents'); if (contents) { return contents.innerHTML.trim(); @@ -163,20 +162,20 @@ export class AddonModLessonHelperProvider { * @return {any} Question data. */ getQuestionFromPageData(questionForm: FormGroup, pageData: any): any { - const question: any = {}; + const question: any = {}, + element = this.domUtils.convertToElement(pageData.pagecontent); // Get the container of the question answers if it exists. - this.div.innerHTML = pageData.pagecontent; - const fieldContainer = this.div.querySelector('.fcontainer'); + const fieldContainer = element.querySelector('.fcontainer'); // Get hidden inputs and add their data to the form group. - const hiddenInputs = Array.from(this.div.querySelectorAll('input[type="hidden"]')); + const hiddenInputs = Array.from(element.querySelectorAll('input[type="hidden"]')); hiddenInputs.forEach((input) => { questionForm.addControl(input.name, this.fb.control(input.value)); }); // Get the submit button and extract its value. - const submitButton = this.div.querySelector('input[type="submit"]'); + const submitButton = element.querySelector('input[type="submit"]'); question.submitLabel = submitButton ? submitButton.value : this.translate.instant('addon.mod_lesson.submit'); if (!fieldContainer) { @@ -358,28 +357,27 @@ export class AddonModLessonHelperProvider { * the HTML. */ getQuestionPageAnswerDataFromHtml(html: string): any { - const data: any = {}; - - this.div.innerHTML = html; + const data: any = {}, + element = this.domUtils.convertToElement(html); // Check if it has a checkbox. - let input = this.div.querySelector('input[type="checkbox"][name*="answer"]'); + let input = element.querySelector('input[type="checkbox"][name*="answer"]'); if (input) { // Truefalse or multichoice. data.isCheckbox = true; data.checked = !!input.checked; data.name = input.name; - data.highlight = !!this.div.querySelector('.highlight'); + data.highlight = !!element.querySelector('.highlight'); input.remove(); - data.content = this.div.innerHTML.trim(); + data.content = element.innerHTML.trim(); return data; } // Check if it has an input text or number. - input = this.div.querySelector('input[type="number"],input[type="text"]'); + input = element.querySelector('input[type="number"],input[type="text"]'); if (input) { // Short answer or numeric. data.isText = true; @@ -389,7 +387,7 @@ export class AddonModLessonHelperProvider { } // Check if it has a select. - const select = this.div.querySelector('select'); + const select = element.querySelector('select'); if (select && select.options) { // Matching. const selectedOption = select.options[select.selectedIndex]; @@ -402,7 +400,7 @@ export class AddonModLessonHelperProvider { } select.remove(); - data.content = this.div.innerHTML.trim(); + data.content = element.innerHTML.trim(); return data; } @@ -477,11 +475,11 @@ export class AddonModLessonHelperProvider { * @return {string} Feedback without the question text. */ removeQuestionFromFeedback(html: string): string { - this.div.innerHTML = html; + const element = this.domUtils.convertToElement(html); // Remove the question text. - this.domUtils.removeElement(this.div, '.generalbox:not(.feedback):not(.correctanswer)'); + this.domUtils.removeElement(element, '.generalbox:not(.feedback):not(.correctanswer)'); - return this.div.innerHTML.trim(); + return element.innerHTML.trim(); } } diff --git a/src/addon/mod/lesson/providers/lesson.ts b/src/addon/mod/lesson/providers/lesson.ts index a3eac51c5..c9f22c3e9 100644 --- a/src/addon/mod/lesson/providers/lesson.ts +++ b/src/addon/mod/lesson/providers/lesson.ts @@ -17,6 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGradesProvider } from '@core/grades/providers/grades'; import { CoreSiteWSPreSets } from '@classes/site'; @@ -170,10 +171,9 @@ export class AddonModLessonProvider { protected ROOT_CACHE_KEY = 'mmaModLesson:'; protected logger; - protected div = document.createElement('div'); // A div element to search in HTML code. constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, - private translate: TranslateService, private textUtils: CoreTextUtilsProvider, + private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider, private lessonOfflineProvider: AddonModLessonOfflineProvider) { this.logger = logger.getInstance('AddonModLessonProvider'); @@ -233,9 +233,9 @@ export class AddonModLessonProvider { if (page.answerdata && !this.answerPageIsQuestion(page)) { // It isn't a question page, but it can be an end of branch, etc. Check if the first answer has a button. if (page.answerdata.answers && page.answerdata.answers[0]) { - this.div.innerHTML = page.answerdata.answers[0][0]; + const element = this.domUtils.convertToElement(page.answerdata.answers[0][0]); - return !!this.div.querySelector('input[type="button"]'); + return !!element.querySelector('input[type="button"]'); } } diff --git a/src/addon/mod/quiz/providers/helper.ts b/src/addon/mod/quiz/providers/helper.ts index 3093b3994..af9d466a8 100644 --- a/src/addon/mod/quiz/providers/helper.ts +++ b/src/addon/mod/quiz/providers/helper.ts @@ -27,8 +27,6 @@ import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate'; @Injectable() export class AddonModQuizHelperProvider { - protected div = document.createElement('div'); // A div element to search in HTML code. - constructor(private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider, private accessRuleDelegate: AddonModQuizAccessRuleDelegate, private quizProvider: AddonModQuizProvider, private modalCtrl: ModalController, private quizOfflineProvider: AddonModQuizOfflineProvider) { } @@ -153,9 +151,9 @@ export class AddonModQuizHelperProvider { * @return {string} Question's mark. */ getQuestionMarkFromHtml(html: string): string { - this.div.innerHTML = html; + const element = this.domUtils.convertToElement(html); - return this.domUtils.getContentsOfElement(this.div, '.grade'); + return this.domUtils.getContentsOfElement(element, '.grade'); } /** diff --git a/src/addon/mod/quiz/providers/quiz.ts b/src/addon/mod/quiz/providers/quiz.ts index 95161dc5e..a14ffedc3 100644 --- a/src/addon/mod/quiz/providers/quiz.ts +++ b/src/addon/mod/quiz/providers/quiz.ts @@ -17,6 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -56,13 +57,13 @@ export class AddonModQuizProvider { protected ROOT_CACHE_KEY = 'mmaModQuiz:'; protected logger; - protected div = document.createElement('div'); // A div element to search in HTML code. constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private gradesHelper: CoreGradesHelperProvider, private questionDelegate: CoreQuestionDelegate, private filepoolProvider: CoreFilepoolProvider, private timeUtils: CoreTimeUtilsProvider, - private accessRulesDelegate: AddonModQuizAccessRuleDelegate, private quizOfflineProvider: AddonModQuizOfflineProvider) { + private accessRulesDelegate: AddonModQuizAccessRuleDelegate, private quizOfflineProvider: AddonModQuizOfflineProvider, + private domUtils: CoreDomUtilsProvider) { this.logger = logger.getInstance('AddonModQuizProvider'); } @@ -1480,9 +1481,9 @@ export class AddonModQuizProvider { * @return {boolean} Whether it's blocked. */ isQuestionBlocked(question: any): boolean { - this.div.innerHTML = question.html; + const element = this.domUtils.convertToElement(question.html); - return !!this.div.querySelector('.mod_quiz-blocked_question_warning'); + return !!element.querySelector('.mod_quiz-blocked_question_warning'); } /** diff --git a/src/addon/qtype/calculated/providers/handler.ts b/src/addon/qtype/calculated/providers/handler.ts index da6f3d24c..441cecf65 100644 --- a/src/addon/qtype/calculated/providers/handler.ts +++ b/src/addon/qtype/calculated/providers/handler.ts @@ -14,6 +14,7 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { AddonQtypeNumericalHandler } from '@addon/qtype/numerical/providers/handler'; @@ -27,7 +28,8 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { name = 'AddonQtypeCalculated'; type = 'qtype_calculated'; - constructor(private utils: CoreUtilsProvider, private numericalHandler: AddonQtypeNumericalHandler) { } + constructor(private utils: CoreUtilsProvider, private numericalHandler: AddonQtypeNumericalHandler, + private domUtils: CoreDomUtilsProvider) { } /** * Return the Component to use to display the question. @@ -120,9 +122,8 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { * @return {boolean} Whether the question requires units. */ requiresUnits(question: any): boolean { - const div = document.createElement('div'); - div.innerHTML = question.html; + const element = this.domUtils.convertToElement(question.html); - return !!(div.querySelector('select[name*=unit]') || div.querySelector('input[type="radio"]')); + return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]')); } } diff --git a/src/addon/qtype/ddimageortext/component/ddimageortext.ts b/src/addon/qtype/ddimageortext/component/ddimageortext.ts index 907a5875b..2048842b5 100644 --- a/src/addon/qtype/ddimageortext/component/ddimageortext.ts +++ b/src/addon/qtype/ddimageortext/component/ddimageortext.ts @@ -47,13 +47,12 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent return this.questionHelper.showComponentError(this.onAbort); } - const div = document.createElement('div'); - div.innerHTML = this.question.html; + const element = this.domUtils.convertToElement(this.question.html); // Get D&D area and question text. - const ddArea = div.querySelector('.ddarea'); + const ddArea = element.querySelector('.ddarea'); - this.question.text = this.domUtils.getContentsOfElement(div, '.qtext'); + this.question.text = this.domUtils.getContentsOfElement(element, '.qtext'); if (!ddArea || typeof this.question.text == 'undefined') { this.logger.warn('Aborting because of an error parsing question.', this.question.name); diff --git a/src/addon/qtype/ddmarker/component/ddmarker.ts b/src/addon/qtype/ddmarker/component/ddmarker.ts index 95814762b..345887738 100644 --- a/src/addon/qtype/ddmarker/component/ddmarker.ts +++ b/src/addon/qtype/ddmarker/component/ddmarker.ts @@ -47,14 +47,13 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple return this.questionHelper.showComponentError(this.onAbort); } - const div = document.createElement('div'); - div.innerHTML = this.question.html; + const element = this.domUtils.convertToElement(this.question.html); // Get D&D area, form and question text. - const ddArea = div.querySelector('.ddarea'), - ddForm = div.querySelector('.ddform'); + const ddArea = element.querySelector('.ddarea'), + ddForm = element.querySelector('.ddform'); - this.question.text = this.domUtils.getContentsOfElement(div, '.qtext'); + this.question.text = this.domUtils.getContentsOfElement(element, '.qtext'); if (!ddArea || !ddForm || typeof this.question.text == 'undefined') { this.logger.warn('Aborting because of an error parsing question.', this.question.name); @@ -64,7 +63,7 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple // Build the D&D area HTML. this.question.ddArea = ddArea.outerHTML; - const wrongParts = div.querySelector('.wrongparts'); + const wrongParts = element.querySelector('.wrongparts'); if (wrongParts) { this.question.ddArea += wrongParts.outerHTML; } diff --git a/src/addon/qtype/ddwtos/component/ddwtos.ts b/src/addon/qtype/ddwtos/component/ddwtos.ts index 33ccd161d..8e39b0a3c 100644 --- a/src/addon/qtype/ddwtos/component/ddwtos.ts +++ b/src/addon/qtype/ddwtos/component/ddwtos.ts @@ -47,17 +47,16 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme return this.questionHelper.showComponentError(this.onAbort); } - const div = document.createElement('div'); - div.innerHTML = this.question.html; + const element = this.domUtils.convertToElement(this.question.html); // Replace Moodle's correct/incorrect and feedback classes with our own. - this.questionHelper.replaceCorrectnessClasses(div); - this.questionHelper.replaceFeedbackClasses(div); + this.questionHelper.replaceCorrectnessClasses(element); + this.questionHelper.replaceFeedbackClasses(element); // Treat the correct/incorrect icons. - this.questionHelper.treatCorrectnessIcons(div); + this.questionHelper.treatCorrectnessIcons(element); - const answerContainer = div.querySelector('.answercontainer'); + const answerContainer = element.querySelector('.answercontainer'); if (!answerContainer) { this.logger.warn('Aborting because of an error parsing question.', this.question.name); @@ -67,7 +66,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme this.question.readOnly = answerContainer.classList.contains('readonly'); this.question.answers = answerContainer.outerHTML; - this.question.text = this.domUtils.getContentsOfElement(div, '.qtext'); + this.question.text = this.domUtils.getContentsOfElement(element, '.qtext'); if (typeof this.question.text == 'undefined') { this.logger.warn('Aborting because of an error parsing question.', this.question.name); @@ -75,7 +74,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme } // 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])')); + const inputEls = Array.from(element.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])')); inputEls.forEach((inputEl) => { this.question.text += inputEl.outerHTML; diff --git a/src/addon/qtype/description/component/description.ts b/src/addon/qtype/description/component/description.ts index 3a9d8d223..55c323b0c 100644 --- a/src/addon/qtype/description/component/description.ts +++ b/src/addon/qtype/description/component/description.ts @@ -33,10 +33,10 @@ export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent im * Component being initialized. */ ngOnInit(): void { - const questionDiv = this.initComponent(); - if (questionDiv) { + const questionEl = this.initComponent(); + if (questionEl) { // Get the "seen" hidden input. - const input = questionDiv.querySelector('input[type="hidden"][name*=seen]'); + const input = questionEl.querySelector('input[type="hidden"][name*=seen]'); if (input) { this.question.seenInput = { name: input.name, diff --git a/src/addon/qtype/essay/providers/handler.ts b/src/addon/qtype/essay/providers/handler.ts index b75ad9c68..3babbd6e1 100644 --- a/src/addon/qtype/essay/providers/handler.ts +++ b/src/addon/qtype/essay/providers/handler.ts @@ -15,6 +15,7 @@ import { Injectable, Injector } from '@angular/core'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -28,10 +29,8 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { name = 'AddonQtypeEssay'; type = 'qtype_essay'; - protected div = document.createElement('div'); // A div element to search in HTML code. - constructor(private utils: CoreUtilsProvider, private questionHelper: CoreQuestionHelperProvider, - private textUtils: CoreTextUtilsProvider) { } + private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider) { } /** * Return the name of the behaviour to use for the question. @@ -65,14 +64,14 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { * @return {string} Prevent submit message. Undefined or empty if can be submitted. */ getPreventSubmitMessage(question: any): string { - this.div.innerHTML = question.html; + const element = this.domUtils.convertToElement(question.html); - if (this.div.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. return 'core.question.errorattachmentsnotsupported'; } - if (this.questionHelper.hasDraftFileUrls(this.div.innerHTML)) { + if (this.questionHelper.hasDraftFileUrls(element.innerHTML)) { return 'core.question.errorinlinefilesnotsupported'; } } @@ -85,10 +84,10 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { * @return {number} 1 if complete, 0 if not complete, -1 if cannot determine. */ isCompleteResponse(question: any, answers: any): number { - this.div.innerHTML = question.html; + const element = this.domUtils.convertToElement(question.html); const hasInlineText = answers['answer'] && answers['answer'] !== '', - allowsAttachments = !!this.div.querySelector('div[id*=filemanager]'); + allowsAttachments = !!element.querySelector('div[id*=filemanager]'); if (!allowsAttachments) { return hasInlineText ? 1 : 0; @@ -141,10 +140,10 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { * @return {void|Promise} Return a promise resolved when done if async, void if sync. */ prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { - this.div.innerHTML = question.html; + const element = this.domUtils.convertToElement(question.html); // Search the textarea to get its name. - const textarea = this.div.querySelector('textarea[name*=_answer]'); + const textarea = element.querySelector('textarea[name*=_answer]'); if (textarea && typeof answers[textarea.name] != 'undefined') { // Add some HTML to the text if needed. diff --git a/src/core/question/classes/base-question-component.ts b/src/core/question/classes/base-question-component.ts index c1a7d8b85..771502142 100644 --- a/src/core/question/classes/base-question-component.ts +++ b/src/core/question/classes/base-question-component.ts @@ -51,12 +51,12 @@ export class CoreQuestionBaseComponent { */ initCalculatedComponent(): void | HTMLElement { // Treat the input text first. - const questionDiv = this.initInputTextComponent(); - if (questionDiv) { + const questionEl = this.initInputTextComponent(); + if (questionEl) { // Check if the question has a select for units. const selectModel: any = {}, - select = questionDiv.querySelector('select[name*=unit]'), + select = questionEl.querySelector('select[name*=unit]'), options = select && Array.from(select.querySelectorAll('option')); if (select && options && options.length) { @@ -94,24 +94,24 @@ export class CoreQuestionBaseComponent { } // Get the accessibility label. - const accessibilityLabel = questionDiv.querySelector('label[for="' + select.id + '"]'); + const accessibilityLabel = questionEl.querySelector('label[for="' + select.id + '"]'); selectModel.accessibilityLabel = accessibilityLabel && accessibilityLabel.innerHTML; this.question.select = selectModel; // Check which one should be displayed first: the select or the input. - const input = questionDiv.querySelector('input[type="text"][name*=answer]'); + const input = questionEl.querySelector('input[type="text"][name*=answer]'); this.question.selectFirst = - questionDiv.innerHTML.indexOf(input.outerHTML) > questionDiv.innerHTML.indexOf(select.outerHTML); + questionEl.innerHTML.indexOf(input.outerHTML) > questionEl.innerHTML.indexOf(select.outerHTML); - return questionDiv; + return questionEl; } // Check if the question has radio buttons for units. - const radios = Array.from(questionDiv.querySelectorAll('input[type="radio"]')); + const radios = Array.from(questionEl.querySelectorAll('input[type="radio"]')); if (!radios.length) { // No select and no radio buttons. The units need to be entered in the input text. - return questionDiv; + return questionEl; } this.question.options = []; @@ -126,7 +126,7 @@ export class CoreQuestionBaseComponent { disabled: radioEl.disabled }, // Get the label with the question text. - label = questionDiv.querySelector('label[for="' + option.id + '"]'); + label = questionEl.querySelector('label[for="' + option.id + '"]'); this.question.optionsName = option.name; @@ -154,9 +154,9 @@ export class CoreQuestionBaseComponent { } // Check which one should be displayed first: the options or the input. - const input = questionDiv.querySelector('input[type="text"][name*=answer]'); + const input = questionEl.querySelector('input[type="text"][name*=answer]'); this.question.optionsFirst = - questionDiv.innerHTML.indexOf(input.outerHTML) > questionDiv.innerHTML.indexOf(radios[0].outerHTML); + questionEl.innerHTML.indexOf(input.outerHTML) > questionEl.innerHTML.indexOf(radios[0].outerHTML); } } @@ -172,18 +172,17 @@ export class CoreQuestionBaseComponent { return this.questionHelper.showComponentError(this.onAbort); } - const div = document.createElement('div'); - div.innerHTML = this.question.html; + const element = this.domUtils.convertToElement(this.question.html); // Extract question text. - this.question.text = this.domUtils.getContentsOfElement(div, '.qtext'); + this.question.text = this.domUtils.getContentsOfElement(element, '.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); } - return div; + return element; } /** @@ -192,24 +191,24 @@ export class CoreQuestionBaseComponent { * @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid. */ initEssayComponent(): void | HTMLElement { - const questionDiv = this.initComponent(); + const questionEl = this.initComponent(); - if (questionDiv) { + if (questionEl) { // First search the textarea. - const textarea = questionDiv.querySelector('textarea[name*=_answer]'); - this.question.allowsAttachments = !!questionDiv.querySelector('div[id*=filemanager]'); - this.question.isMonospaced = !!questionDiv.querySelector('.qtype_essay_monospaced'); - this.question.isPlainText = this.question.isMonospaced || !!questionDiv.querySelector('.qtype_essay_plain'); - this.question.hasDraftFiles = this.questionHelper.hasDraftFileUrls(questionDiv.innerHTML); + const textarea = questionEl.querySelector('textarea[name*=_answer]'); + this.question.allowsAttachments = !!questionEl.querySelector('div[id*=filemanager]'); + this.question.isMonospaced = !!questionEl.querySelector('.qtype_essay_monospaced'); + this.question.isPlainText = this.question.isMonospaced || !!questionEl.querySelector('.qtype_essay_plain'); + this.question.hasDraftFiles = this.questionHelper.hasDraftFileUrls(questionEl.innerHTML); if (!textarea) { // Textarea not found, we might be in review. Search the answer and the attachments. - this.question.answer = this.domUtils.getContentsOfElement(questionDiv, '.qtype_essay_response'); + this.question.answer = this.domUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); this.question.attachments = this.questionHelper.getQuestionAttachmentsFromHtml( - this.domUtils.getContentsOfElement(questionDiv, '.attachments')); + this.domUtils.getContentsOfElement(questionEl, '.attachments')); } else { // Textarea found. - const input = questionDiv.querySelector('input[type="hidden"][name*=answerformat]'), + const input = questionEl.querySelector('input[type="hidden"][name*=answerformat]'), content = textarea.innerHTML; this.question.textarea = { @@ -241,11 +240,10 @@ export class CoreQuestionBaseComponent { return this.questionHelper.showComponentError(this.onAbort); } - const div = document.createElement('div'); - div.innerHTML = this.question.html; + const element = this.domUtils.convertToElement(this.question.html); // Get question content. - const content = div.querySelector(contentSelector); + const content = element.querySelector(contentSelector); if (!content) { this.logger.warn('Aborting because of an error parsing question.', this.question.name); @@ -257,11 +255,11 @@ export class CoreQuestionBaseComponent { this.domUtils.removeElement(content, '.validationerror'); // Replace Moodle's correct/incorrect and feedback classes with our own. - this.questionHelper.replaceCorrectnessClasses(div); - this.questionHelper.replaceFeedbackClasses(div); + this.questionHelper.replaceCorrectnessClasses(element); + this.questionHelper.replaceFeedbackClasses(element); // Treat the correct/incorrect icons. - this.questionHelper.treatCorrectnessIcons(div); + this.questionHelper.treatCorrectnessIcons(element); // Set the question text. this.question.text = content.innerHTML; @@ -273,10 +271,10 @@ export class CoreQuestionBaseComponent { * @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid. */ initInputTextComponent(): void | HTMLElement { - const questionDiv = this.initComponent(); - if (questionDiv) { + const questionEl = this.initComponent(); + if (questionEl) { // Get the input element. - const input = questionDiv.querySelector('input[type="text"][name*=answer]'); + const input = questionEl.querySelector('input[type="text"][name*=answer]'); if (!input) { this.logger.warn('Aborting because couldn\'t find input.', this.question.name); @@ -302,7 +300,7 @@ export class CoreQuestionBaseComponent { } } - return questionDiv; + return questionEl; } /** @@ -311,11 +309,11 @@ export class CoreQuestionBaseComponent { * @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid. */ initMatchComponent(): void | HTMLElement { - const questionDiv = this.initComponent(); + const questionEl = this.initComponent(); - if (questionDiv) { + if (questionEl) { // Find rows. - const rows = Array.from(questionDiv.querySelectorAll('table.answer tr')); + const rows = Array.from(questionEl.querySelectorAll('table.answer tr')); if (!rows || !rows.length) { this.logger.warn('Aborting because couldn\'t find any row.', this.question.name); @@ -394,7 +392,7 @@ export class CoreQuestionBaseComponent { this.question.loaded = true; } - return questionDiv; + return questionEl; } /** @@ -403,19 +401,19 @@ export class CoreQuestionBaseComponent { * @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid. */ initMultichoiceComponent(): void | HTMLElement { - const questionDiv = this.initComponent(); + const questionEl = this.initComponent(); - if (questionDiv) { + if (questionEl) { // Get the prompt. - this.question.prompt = this.domUtils.getContentsOfElement(questionDiv, '.prompt'); + this.question.prompt = this.domUtils.getContentsOfElement(questionEl, '.prompt'); // Search radio buttons first (single choice). - let options = Array.from(questionDiv.querySelectorAll('input[type="radio"]')); + let options = Array.from(questionEl.querySelectorAll('input[type="radio"]')); if (!options || !options.length) { // Radio buttons not found, it should be a multi answer. Search for checkbox. this.question.multi = true; - options = Array.from(questionDiv.querySelectorAll('input[type="checkbox"]')); + options = Array.from(questionEl.querySelectorAll('input[type="checkbox"]')); if (!options || !options.length) { // No checkbox found either. Abort. @@ -441,7 +439,7 @@ export class CoreQuestionBaseComponent { this.question.optionsName = option.name; // Get the label with the question text. - const label = questionDiv.querySelector('label[for="' + option.id + '"]'); + const label = questionEl.querySelector('label[for="' + option.id + '"]'); if (label) { option.text = label.innerHTML; @@ -483,6 +481,6 @@ export class CoreQuestionBaseComponent { } } - return questionDiv; + return questionEl; } } diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts index b73dd9823..f1c9b5b3c 100644 --- a/src/core/question/providers/helper.ts +++ b/src/core/question/providers/helper.ts @@ -29,7 +29,6 @@ import { CoreQuestionDelegate } from './delegate'; @Injectable() export class CoreQuestionHelperProvider { protected lastErrorShown = 0; - protected div = document.createElement('div'); // A div element to search in HTML code. constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, private questionProvider: CoreQuestionProvider, private sitesProvider: CoreSitesProvider, @@ -70,15 +69,15 @@ export class CoreQuestionHelperProvider { extractQbehaviourButtons(question: any, selector?: string): void { selector = selector || '.im-controls input[type="submit"]'; - this.div.innerHTML = question.html; + const element = this.domUtils.convertToElement(question.html); // Search the buttons. - const buttons = Array.from(this.div.querySelectorAll(selector)); + const buttons = Array.from(element.querySelectorAll(selector)); buttons.forEach((button) => { this.addBehaviourButton(question, button); }); - question.html = this.div.innerHTML; + question.html = element.innerHTML; } /** @@ -91,9 +90,9 @@ export class CoreQuestionHelperProvider { * @return {boolean} Wether the certainty is found. */ extractQbehaviourCBM(question: any): boolean { - this.div.innerHTML = question.html; + const element = this.domUtils.convertToElement(question.html); - const labels = Array.from(this.div.querySelectorAll('.im-controls .certaintychoices label[for*="certainty"]')); + const labels = Array.from(element.querySelectorAll('.im-controls .certaintychoices label[for*="certainty"]')); question.behaviourCertaintyOptions = []; labels.forEach((label) => { @@ -158,10 +157,10 @@ export class CoreQuestionHelperProvider { * @return {boolean} Whether the seen input is found. */ extractQbehaviourSeenInput(question: any): boolean { - this.div.innerHTML = question.html; + const element = this.domUtils.convertToElement(question.html); // Search the "seen" input. - const seenInput = this.div.querySelector('input[type="hidden"][name*=seen]'); + const seenInput = element.querySelector('input[type="hidden"][name*=seen]'); if (seenInput) { // Get the data and remove the input. question.behaviourSeenInput = { @@ -169,7 +168,7 @@ export class CoreQuestionHelperProvider { value: seenInput.value }; seenInput.parentElement.removeChild(seenInput); - question.html = this.div.innerHTML; + question.html = element.innerHTML; return true; } @@ -214,9 +213,9 @@ export class CoreQuestionHelperProvider { * @param {string} attrName Name of the attribute to store the HTML in. */ protected extractQuestionLastElementNotInContent(question: any, selector: string, attrName: string): void { - this.div.innerHTML = question.html; + const element = this.domUtils.convertToElement(question.html); - const matches = Array.from(this.div.querySelectorAll(selector)); + const matches = Array.from(element.querySelectorAll(selector)); // Get the last element and check it's not in the question contents. let last = matches.pop(); @@ -225,7 +224,7 @@ export class CoreQuestionHelperProvider { // Not in question contents. Add it to a separate attribute and remove it from the HTML. question[attrName] = last.innerHTML; last.parentElement.removeChild(last); - question.html = this.div.innerHTML; + question.html = element.innerHTML; return; } @@ -283,11 +282,10 @@ export class CoreQuestionHelperProvider { * @return {any} Object where the keys are the names. */ getAllInputNamesFromHtml(html: string): any { - const form = document.createElement('form'), + const element = this.domUtils.convertToElement('
' + html + '
'), + form = element.children[0], answers = {}; - form.innerHTML = html; - // Search all input elements. Array.from(form.elements).forEach((element: HTMLInputElement) => { const name = element.name || ''; @@ -350,13 +348,13 @@ export class CoreQuestionHelperProvider { * @return {Object[]} Attachments. */ getQuestionAttachmentsFromHtml(html: string): any[] { - this.div.innerHTML = html; + const element = this.domUtils.convertToElement(html); // Remove the filemanager (area to attach files to a question). - this.domUtils.removeElement(this.div, 'div[id*=filemanager]'); + this.domUtils.removeElement(element, 'div[id*=filemanager]'); // Search the anchors. - const anchors = Array.from(this.div.querySelectorAll('a')), + const anchors = Array.from(element.querySelectorAll('a')), attachments = []; anchors.forEach((anchor) => { @@ -383,10 +381,10 @@ export class CoreQuestionHelperProvider { */ getQuestionSequenceCheckFromHtml(html: string): {name: string, value: string} { if (html) { - this.div.innerHTML = html; + const element = this.domUtils.convertToElement(html); // Search the input holding the sequencecheck. - const input = this.div.querySelector('input[name*=sequencecheck]'); + const input = element.querySelector('input[name*=sequencecheck]'); if (input && typeof input.name != 'undefined' && typeof input.value != 'undefined') { return { name: input.name, @@ -415,9 +413,9 @@ export class CoreQuestionHelperProvider { * @return {string} Validation error message if present. */ getValidationErrorFromHtml(html: string): string { - this.div.innerHTML = html; + const element = this.domUtils.convertToElement(html); - return this.domUtils.getContentsOfElement(this.div, '.validationerror'); + return this.domUtils.getContentsOfElement(element, '.validationerror'); } /** @@ -443,8 +441,8 @@ export class CoreQuestionHelperProvider { * @param {any} question Question. */ loadLocalAnswersInHtml(question: any): void { - const form = document.createElement('form'); - form.innerHTML = question.html; + const element = this.domUtils.convertToElement('
' + question.html + '
'), + form = element.children[0]; // Search all input elements. Array.from(form.elements).forEach((element: HTMLInputElement | HTMLButtonElement) => { @@ -574,9 +572,9 @@ export class CoreQuestionHelperProvider { * @return {boolean} Whether the button is found. */ protected searchBehaviourButton(question: any, htmlProperty: string, selector: string): boolean { - this.div.innerHTML = question[htmlProperty]; + const element = this.domUtils.convertToElement(question[htmlProperty]); - const button = this.div.querySelector(selector); + const button = element.querySelector(selector); if (button) { // Add a behaviour button to the question's "behaviourButtons" property. this.addBehaviourButton(question, button); @@ -585,7 +583,7 @@ export class CoreQuestionHelperProvider { button.parentElement.removeChild(button); // Update the question's html. - question[htmlProperty] = this.div.innerHTML; + question[htmlProperty] = element.innerHTML; return true; } diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 3ab14f2a9..99d6a0ccc 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -35,7 +35,8 @@ export class CoreDomUtilsProvider { 'search', 'tel', 'text', 'time', 'url', 'week']; protected INSTANCE_ID_ATTR_NAME = 'core-instance-id'; - protected element = document.createElement('div'); // Fake element to use in some functions, to prevent creating it each time. + protected parser = new DOMParser(); // Parser to treat HTML. + protected matchesFn: string; // Name of the "matches" function to use when simulating a closest call. protected instances: {[id: string]: any} = {}; // Store component/directive instances by id. protected lastInstanceId = 0; @@ -124,6 +125,28 @@ export class CoreDomUtilsProvider { return Promise.resolve(); } + /** + * Convert some HTML as text into an HTMLElement. This HTML is put inside a div or a body. + * + * @param {string} html Text to convert. + * @return {HTMLElement} Element. + */ + convertToElement(html: string): HTMLElement { + if (this.parser) { + const doc = this.parser.parseFromString(html, 'text/html'); + + // Verify that the doc is valid. In some OS like Android 4.4 only XML parsing is supported, so doc is null. + if (doc) { + return doc.body; + } + } + + const element = document.createElement('div'); + element.innerHTML = html; + + return element; + } + /** * Create a "cancelled" error. These errors won't display an error message in showErrorModal functions. * @@ -169,8 +192,8 @@ export class CoreDomUtilsProvider { const urls = []; let elements; - this.element.innerHTML = html; - elements = this.element.querySelectorAll('a, img, audio, video, source, track'); + const element = this.convertToElement(html); + elements = element.querySelectorAll('a, img, audio, video, source, track'); for (let i = 0; i < elements.length; i++) { const element = elements[i]; @@ -599,21 +622,21 @@ export class CoreDomUtilsProvider { removeElementFromHtml(html: string, selector: string, removeAll?: boolean): string { let selected; - this.element.innerHTML = html; + const element = this.convertToElement(html); if (removeAll) { - selected = this.element.querySelectorAll(selector); + selected = element.querySelectorAll(selector); for (let i = 0; i < selected.length; i++) { selected[i].remove(); } } else { - selected = this.element.querySelector(selector); + selected = element.querySelector(selector); if (selected) { selected.remove(); } } - return this.element.innerHTML; + return element.innerHTML; } /** @@ -665,10 +688,10 @@ export class CoreDomUtilsProvider { let media, anchors; - this.element.innerHTML = html; + const element = this.convertToElement(html); // Treat elements with src (img, audio, video, ...). - media = this.element.querySelectorAll('img, video, audio, source, track'); + media = element.querySelectorAll('img, video, audio, source, track'); media.forEach((media: HTMLElement) => { let newSrc = paths[this.textUtils.decodeURIComponent(media.getAttribute('src'))]; @@ -686,7 +709,7 @@ export class CoreDomUtilsProvider { }); // Now treat links. - anchors = this.element.querySelectorAll('a'); + anchors = element.querySelectorAll('a'); anchors.forEach((anchor: HTMLElement) => { const href = this.textUtils.decodeURIComponent(anchor.getAttribute('href')), newUrl = paths[href]; @@ -700,7 +723,7 @@ export class CoreDomUtilsProvider { } }); - return this.element.innerHTML; + return element.innerHTML; } /** @@ -1104,13 +1127,13 @@ export class CoreDomUtilsProvider { } /** - * Converts HTML formatted text to DOM element. - * @param {string} text HTML text. - * @return {HTMLCollection} Same text converted to HTMLCollection. + * Converts HTML formatted text to DOM element(s). + * + * @param {string} text HTML text. + * @return {HTMLCollection} Same text converted to HTMLCollection. */ toDom(text: string): HTMLCollection { - const element = document.createElement('div'); - element.innerHTML = text; + const element = this.convertToElement(text); return element.children; } diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts index 5d3fcbcf1..7add3d34b 100644 --- a/src/providers/utils/text.ts +++ b/src/providers/utils/text.ts @@ -68,7 +68,7 @@ export class CoreTextUtilsProvider { {old: /_mmaModWorkshop/g, new: '_AddonModWorkshop'}, ]; - protected element = document.createElement('div'); // Fake element to use in some functions, to prevent creating it each time. + protected parser = new DOMParser(); // Parser to treat HTML. constructor(private translate: TranslateService, private langProvider: CoreLangProvider, private modalCtrl: ModalController) { } @@ -142,8 +142,8 @@ export class CoreTextUtilsProvider { // First, we use a regexpr. text = text.replace(/(<([^>]+)>)/ig, ''); // Then, we rely on the browser. We need to wrap the text to be sure is HTML. - this.element.innerHTML = text; - text = this.element.textContent; + const element = this.convertToElement(text); + text = element.textContent; // Recover or remove new lines. text = this.replaceNewLines(text, singleLine ? ' ' : '
'); @@ -176,6 +176,29 @@ export class CoreTextUtilsProvider { } } + /** + * Convert some HTML as text into an HTMLElement. This HTML is put inside a div or a body. + * This function is the same as in DomUtils, but we cannot use that one because of circular dependencies. + * + * @param {string} html Text to convert. + * @return {HTMLElement} Element. + */ + protected convertToElement(html: string): HTMLElement { + if (this.parser) { + const doc = this.parser.parseFromString(html, 'text/html'); + + // Verify that the doc is valid. In some OS like Android 4.4 only XML parsing is supported, so doc is null. + if (doc) { + return doc.body; + } + } + + const element = document.createElement('div'); + element.innerHTML = html; + + return element; + } + /** * Count words in a text. * @@ -225,9 +248,8 @@ export class CoreTextUtilsProvider { */ decodeHTMLEntities(text: string): string { if (text) { - this.element.innerHTML = text; - text = this.element.textContent; - this.element.textContent = ''; + const element = this.convertToElement(text); + text = element.textContent; } return text; From 30c78bcee38a8f4994f99869a4fce095e214e594 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 9 Jul 2018 15:16:22 +0200 Subject: [PATCH 2/3] MOBILE-2444 core: Allow opening modules in dev mode in iOS 10 --- src/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.html b/src/index.html index afe3bcca7..066800c23 100644 --- a/src/index.html +++ b/src/index.html @@ -4,7 +4,7 @@ Moodle Desktop - + From b1aff5b87d8e2daa0c8c31db8be2c7f1c4b50b17 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 9 Jul 2018 15:19:52 +0200 Subject: [PATCH 3/3] MOBILE-2444 ios: Fix click links and media in quiz in iOS --- src/app/app.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/app.scss b/src/app/app.scss index d1f0e8b5e..727fdff6a 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -824,6 +824,7 @@ body.keyboard-is-open core-ion-tabs .tabbar { // Fix links and videos in ion-radio and ion-checkbox. .item.item-radio, .item.item-checkbox { .input-wrapper { + position: relative; z-index: 5; pointer-events: none; }