Merge pull request #1406 from dpalou/MOBILE-2444

Mobile 2444
main
Juan Leyva 2018-07-09 16:23:59 +02:00 committed by GitHub
commit 6485ded480
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 212 additions and 176 deletions

View File

@ -27,8 +27,6 @@ import * as moment from 'moment';
@Injectable() @Injectable()
export class AddonModLessonHelperProvider { 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, constructor(private domUtils: CoreDomUtilsProvider, private fb: FormBuilder, private translate: TranslateService,
private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { } private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { }
@ -39,8 +37,9 @@ export class AddonModLessonHelperProvider {
* @return {{formatted: boolean, label: string, href: string}} Formatted data. * @return {{formatted: boolean, label: string, href: string}} Formatted data.
*/ */
formatActivityLink(activityLink: string): {formatted: boolean, label: string, href: string} { formatActivityLink(activityLink: string): {formatted: boolean, label: string, href: string} {
this.div.innerHTML = activityLink; const element = this.domUtils.convertToElement(activityLink),
const anchor = this.div.querySelector('a'); anchor = element.querySelector('a');
if (!anchor) { if (!anchor) {
// Anchor not found, return the original HTML. // Anchor not found, return the original HTML.
return { return {
@ -67,11 +66,11 @@ export class AddonModLessonHelperProvider {
const data = { const data = {
buttonText: '', buttonText: '',
content: '' content: ''
}; },
element = this.domUtils.convertToElement(html);
// Search the input button. // Search the input button.
this.div.innerHTML = html; const button = <HTMLInputElement> element.querySelector('input[type="button"]');
const button = <HTMLInputElement> this.div.querySelector('input[type="button"]');
if (button) { if (button) {
// Extract the button content and remove it from the HTML. // Extract the button content and remove it from the HTML.
@ -79,7 +78,7 @@ export class AddonModLessonHelperProvider {
button.remove(); button.remove();
} }
data.content = this.div.innerHTML.trim(); data.content = element.innerHTML.trim();
return data; return data;
} }
@ -91,19 +90,19 @@ export class AddonModLessonHelperProvider {
* @return {any[]} List of buttons. * @return {any[]} List of buttons.
*/ */
getPageButtonsFromHtml(html: string): any[] { getPageButtonsFromHtml(html: string): any[] {
const buttons = []; const buttons = [],
element = this.domUtils.convertToElement(html);
// Get the container of the buttons if it exists. // Get the container of the buttons if it exists.
this.div.innerHTML = html; let buttonsContainer = element.querySelector('.branchbuttoncontainer');
let buttonsContainer = this.div.querySelector('.branchbuttoncontainer');
if (!buttonsContainer) { if (!buttonsContainer) {
// Button container not found, might be a legacy lesson (from 1.9). // 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. // No buttons found.
return buttons; return buttons;
} }
buttonsContainer = this.div; buttonsContainer = element;
} }
const forms = Array.from(buttonsContainer.querySelectorAll('form')); const forms = Array.from(buttonsContainer.querySelectorAll('form'));
@ -144,8 +143,8 @@ export class AddonModLessonHelperProvider {
*/ */
getPageContentsFromPageData(data: any): string { getPageContentsFromPageData(data: any): string {
// Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered. // Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered.
this.div.innerHTML = data.pagecontent; const element = this.domUtils.convertToElement(data.pagecontent),
const contents = this.div.querySelector('.contents'); contents = element.querySelector('.contents');
if (contents) { if (contents) {
return contents.innerHTML.trim(); return contents.innerHTML.trim();
@ -163,20 +162,20 @@ export class AddonModLessonHelperProvider {
* @return {any} Question data. * @return {any} Question data.
*/ */
getQuestionFromPageData(questionForm: FormGroup, pageData: any): any { 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. // Get the container of the question answers if it exists.
this.div.innerHTML = pageData.pagecontent; const fieldContainer = element.querySelector('.fcontainer');
const fieldContainer = this.div.querySelector('.fcontainer');
// Get hidden inputs and add their data to the form group. // Get hidden inputs and add their data to the form group.
const hiddenInputs = <HTMLInputElement[]> Array.from(this.div.querySelectorAll('input[type="hidden"]')); const hiddenInputs = <HTMLInputElement[]> Array.from(element.querySelectorAll('input[type="hidden"]'));
hiddenInputs.forEach((input) => { hiddenInputs.forEach((input) => {
questionForm.addControl(input.name, this.fb.control(input.value)); questionForm.addControl(input.name, this.fb.control(input.value));
}); });
// Get the submit button and extract its value. // Get the submit button and extract its value.
const submitButton = <HTMLInputElement> this.div.querySelector('input[type="submit"]'); const submitButton = <HTMLInputElement> element.querySelector('input[type="submit"]');
question.submitLabel = submitButton ? submitButton.value : this.translate.instant('addon.mod_lesson.submit'); question.submitLabel = submitButton ? submitButton.value : this.translate.instant('addon.mod_lesson.submit');
if (!fieldContainer) { if (!fieldContainer) {
@ -358,28 +357,27 @@ export class AddonModLessonHelperProvider {
* the HTML. * the HTML.
*/ */
getQuestionPageAnswerDataFromHtml(html: string): any { getQuestionPageAnswerDataFromHtml(html: string): any {
const data: any = {}; const data: any = {},
element = this.domUtils.convertToElement(html);
this.div.innerHTML = html;
// Check if it has a checkbox. // Check if it has a checkbox.
let input = <HTMLInputElement> this.div.querySelector('input[type="checkbox"][name*="answer"]'); let input = <HTMLInputElement> element.querySelector('input[type="checkbox"][name*="answer"]');
if (input) { if (input) {
// Truefalse or multichoice. // Truefalse or multichoice.
data.isCheckbox = true; data.isCheckbox = true;
data.checked = !!input.checked; data.checked = !!input.checked;
data.name = input.name; data.name = input.name;
data.highlight = !!this.div.querySelector('.highlight'); data.highlight = !!element.querySelector('.highlight');
input.remove(); input.remove();
data.content = this.div.innerHTML.trim(); data.content = element.innerHTML.trim();
return data; return data;
} }
// Check if it has an input text or number. // Check if it has an input text or number.
input = <HTMLInputElement> this.div.querySelector('input[type="number"],input[type="text"]'); input = <HTMLInputElement> element.querySelector('input[type="number"],input[type="text"]');
if (input) { if (input) {
// Short answer or numeric. // Short answer or numeric.
data.isText = true; data.isText = true;
@ -389,7 +387,7 @@ export class AddonModLessonHelperProvider {
} }
// Check if it has a select. // Check if it has a select.
const select = <HTMLSelectElement> this.div.querySelector('select'); const select = <HTMLSelectElement> element.querySelector('select');
if (select && select.options) { if (select && select.options) {
// Matching. // Matching.
const selectedOption = select.options[select.selectedIndex]; const selectedOption = select.options[select.selectedIndex];
@ -402,7 +400,7 @@ export class AddonModLessonHelperProvider {
} }
select.remove(); select.remove();
data.content = this.div.innerHTML.trim(); data.content = element.innerHTML.trim();
return data; return data;
} }
@ -477,11 +475,11 @@ export class AddonModLessonHelperProvider {
* @return {string} Feedback without the question text. * @return {string} Feedback without the question text.
*/ */
removeQuestionFromFeedback(html: string): string { removeQuestionFromFeedback(html: string): string {
this.div.innerHTML = html; const element = this.domUtils.convertToElement(html);
// Remove the question text. // 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();
} }
} }

View File

@ -17,6 +17,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreGradesProvider } from '@core/grades/providers/grades'; import { CoreGradesProvider } from '@core/grades/providers/grades';
import { CoreSiteWSPreSets } from '@classes/site'; import { CoreSiteWSPreSets } from '@classes/site';
@ -170,10 +171,9 @@ export class AddonModLessonProvider {
protected ROOT_CACHE_KEY = 'mmaModLesson:'; protected ROOT_CACHE_KEY = 'mmaModLesson:';
protected logger; protected logger;
protected div = document.createElement('div'); // A div element to search in HTML code.
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, 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) { private lessonOfflineProvider: AddonModLessonOfflineProvider) {
this.logger = logger.getInstance('AddonModLessonProvider'); this.logger = logger.getInstance('AddonModLessonProvider');
@ -233,9 +233,9 @@ export class AddonModLessonProvider {
if (page.answerdata && !this.answerPageIsQuestion(page)) { 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. // 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]) { 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"]');
} }
} }

View File

@ -27,8 +27,6 @@ import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
@Injectable() @Injectable()
export class AddonModQuizHelperProvider { 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, constructor(private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
private accessRuleDelegate: AddonModQuizAccessRuleDelegate, private quizProvider: AddonModQuizProvider, private accessRuleDelegate: AddonModQuizAccessRuleDelegate, private quizProvider: AddonModQuizProvider,
private modalCtrl: ModalController, private quizOfflineProvider: AddonModQuizOfflineProvider) { } private modalCtrl: ModalController, private quizOfflineProvider: AddonModQuizOfflineProvider) { }
@ -153,9 +151,9 @@ export class AddonModQuizHelperProvider {
* @return {string} Question's mark. * @return {string} Question's mark.
*/ */
getQuestionMarkFromHtml(html: string): string { 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');
} }
/** /**

View File

@ -17,6 +17,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
@ -56,13 +57,13 @@ export class AddonModQuizProvider {
protected ROOT_CACHE_KEY = 'mmaModQuiz:'; protected ROOT_CACHE_KEY = 'mmaModQuiz:';
protected logger; protected logger;
protected div = document.createElement('div'); // A div element to search in HTML code.
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider,
private gradesHelper: CoreGradesHelperProvider, private questionDelegate: CoreQuestionDelegate, private gradesHelper: CoreGradesHelperProvider, private questionDelegate: CoreQuestionDelegate,
private filepoolProvider: CoreFilepoolProvider, private timeUtils: CoreTimeUtilsProvider, 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'); this.logger = logger.getInstance('AddonModQuizProvider');
} }
@ -1480,9 +1481,9 @@ export class AddonModQuizProvider {
* @return {boolean} Whether it's blocked. * @return {boolean} Whether it's blocked.
*/ */
isQuestionBlocked(question: any): boolean { 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');
} }
/** /**

View File

@ -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 { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHandler } from '@core/question/providers/delegate';
import { AddonQtypeNumericalHandler } from '@addon/qtype/numerical/providers/handler'; import { AddonQtypeNumericalHandler } from '@addon/qtype/numerical/providers/handler';
@ -27,7 +28,8 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
name = 'AddonQtypeCalculated'; name = 'AddonQtypeCalculated';
type = 'qtype_calculated'; 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. * 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. * @return {boolean} Whether the question requires units.
*/ */
requiresUnits(question: any): boolean { requiresUnits(question: any): boolean {
const div = document.createElement('div'); const element = this.domUtils.convertToElement(question.html);
div.innerHTML = question.html;
return !!(div.querySelector('select[name*=unit]') || div.querySelector('input[type="radio"]')); return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]'));
} }
} }

View File

@ -47,13 +47,12 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
return this.questionHelper.showComponentError(this.onAbort); return this.questionHelper.showComponentError(this.onAbort);
} }
const div = document.createElement('div'); const element = this.domUtils.convertToElement(this.question.html);
div.innerHTML = this.question.html;
// Get D&D area and question text. // 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') { if (!ddArea || typeof this.question.text == 'undefined') {
this.logger.warn('Aborting because of an error parsing question.', this.question.name); this.logger.warn('Aborting because of an error parsing question.', this.question.name);

View File

@ -47,14 +47,13 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
return this.questionHelper.showComponentError(this.onAbort); return this.questionHelper.showComponentError(this.onAbort);
} }
const div = document.createElement('div'); const element = this.domUtils.convertToElement(this.question.html);
div.innerHTML = this.question.html;
// Get D&D area, form and question text. // Get D&D area, form and question text.
const ddArea = div.querySelector('.ddarea'), const ddArea = element.querySelector('.ddarea'),
ddForm = div.querySelector('.ddform'); 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') { if (!ddArea || !ddForm || typeof this.question.text == 'undefined') {
this.logger.warn('Aborting because of an error parsing question.', this.question.name); 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. // Build the D&D area HTML.
this.question.ddArea = ddArea.outerHTML; this.question.ddArea = ddArea.outerHTML;
const wrongParts = div.querySelector('.wrongparts'); const wrongParts = element.querySelector('.wrongparts');
if (wrongParts) { if (wrongParts) {
this.question.ddArea += wrongParts.outerHTML; this.question.ddArea += wrongParts.outerHTML;
} }

View File

@ -47,17 +47,16 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
return this.questionHelper.showComponentError(this.onAbort); return this.questionHelper.showComponentError(this.onAbort);
} }
const div = document.createElement('div'); const element = this.domUtils.convertToElement(this.question.html);
div.innerHTML = this.question.html;
// Replace Moodle's correct/incorrect and feedback classes with our own. // Replace Moodle's correct/incorrect and feedback classes with our own.
this.questionHelper.replaceCorrectnessClasses(div); this.questionHelper.replaceCorrectnessClasses(element);
this.questionHelper.replaceFeedbackClasses(div); this.questionHelper.replaceFeedbackClasses(element);
// Treat the correct/incorrect icons. // 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) { if (!answerContainer) {
this.logger.warn('Aborting because of an error parsing question.', this.question.name); 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.readOnly = answerContainer.classList.contains('readonly');
this.question.answers = answerContainer.outerHTML; 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') { if (typeof this.question.text == 'undefined') {
this.logger.warn('Aborting because of an error parsing question.', this.question.name); 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. // Get the inputs where the answers will be stored and add them to the question text.
const inputEls = <HTMLElement[]> Array.from(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])')); const inputEls = <HTMLElement[]> Array.from(element.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])'));
inputEls.forEach((inputEl) => { inputEls.forEach((inputEl) => {
this.question.text += inputEl.outerHTML; this.question.text += inputEl.outerHTML;

View File

@ -33,10 +33,10 @@ export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent im
* Component being initialized. * Component being initialized.
*/ */
ngOnInit(): void { ngOnInit(): void {
const questionDiv = this.initComponent(); const questionEl = this.initComponent();
if (questionDiv) { if (questionEl) {
// Get the "seen" hidden input. // Get the "seen" hidden input.
const input = <HTMLInputElement> questionDiv.querySelector('input[type="hidden"][name*=seen]'); const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=seen]');
if (input) { if (input) {
this.question.seenInput = { this.question.seenInput = {
name: input.name, name: input.name,

View File

@ -15,6 +15,7 @@
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHandler } from '@core/question/providers/delegate';
import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
@ -28,10 +29,8 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
name = 'AddonQtypeEssay'; name = 'AddonQtypeEssay';
type = 'qtype_essay'; type = 'qtype_essay';
protected div = document.createElement('div'); // A div element to search in HTML code.
constructor(private utils: CoreUtilsProvider, private questionHelper: CoreQuestionHelperProvider, 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. * 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. * @return {string} Prevent submit message. Undefined or empty if can be submitted.
*/ */
getPreventSubmitMessage(question: any): string { 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. // The question allows attachments. Since the app cannot attach files yet we will prevent submitting the question.
return 'core.question.errorattachmentsnotsupported'; return 'core.question.errorattachmentsnotsupported';
} }
if (this.questionHelper.hasDraftFileUrls(this.div.innerHTML)) { if (this.questionHelper.hasDraftFileUrls(element.innerHTML)) {
return 'core.question.errorinlinefilesnotsupported'; 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. * @return {number} 1 if complete, 0 if not complete, -1 if cannot determine.
*/ */
isCompleteResponse(question: any, answers: any): number { isCompleteResponse(question: any, answers: any): number {
this.div.innerHTML = question.html; const element = this.domUtils.convertToElement(question.html);
const hasInlineText = answers['answer'] && answers['answer'] !== '', const hasInlineText = answers['answer'] && answers['answer'] !== '',
allowsAttachments = !!this.div.querySelector('div[id*=filemanager]'); allowsAttachments = !!element.querySelector('div[id*=filemanager]');
if (!allowsAttachments) { if (!allowsAttachments) {
return hasInlineText ? 1 : 0; return hasInlineText ? 1 : 0;
@ -141,10 +140,10 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
* @return {void|Promise<any>} Return a promise resolved when done if async, void if sync. * @return {void|Promise<any>} Return a promise resolved when done if async, void if sync.
*/ */
prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise<any> { prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise<any> {
this.div.innerHTML = 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> this.div.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') {
// Add some HTML to the text if needed. // Add some HTML to the text if needed.

View File

@ -824,6 +824,7 @@ body.keyboard-is-open core-ion-tabs .tabbar {
// Fix links and videos in ion-radio and ion-checkbox. // Fix links and videos in ion-radio and ion-checkbox.
.item.item-radio, .item.item-checkbox { .item.item-radio, .item.item-checkbox {
.input-wrapper { .input-wrapper {
position: relative;
z-index: 5; z-index: 5;
pointer-events: none; pointer-events: none;
} }

View File

@ -51,12 +51,12 @@ export class CoreQuestionBaseComponent {
*/ */
initCalculatedComponent(): void | HTMLElement { initCalculatedComponent(): void | HTMLElement {
// Treat the input text first. // Treat the input text first.
const questionDiv = this.initInputTextComponent(); const questionEl = this.initInputTextComponent();
if (questionDiv) { if (questionEl) {
// Check if the question has a select for units. // Check if the question has a select for units.
const selectModel: any = {}, const selectModel: any = {},
select = <HTMLSelectElement> questionDiv.querySelector('select[name*=unit]'), select = <HTMLSelectElement> questionEl.querySelector('select[name*=unit]'),
options = select && Array.from(select.querySelectorAll('option')); options = select && Array.from(select.querySelectorAll('option'));
if (select && options && options.length) { if (select && options && options.length) {
@ -94,24 +94,24 @@ export class CoreQuestionBaseComponent {
} }
// Get the accessibility label. // 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; selectModel.accessibilityLabel = accessibilityLabel && accessibilityLabel.innerHTML;
this.question.select = selectModel; this.question.select = selectModel;
// Check which one should be displayed first: the select or the input. // 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 = 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. // Check if the question has radio buttons for units.
const radios = <HTMLInputElement[]> Array.from(questionDiv.querySelectorAll('input[type="radio"]')); const radios = <HTMLInputElement[]> Array.from(questionEl.querySelectorAll('input[type="radio"]'));
if (!radios.length) { if (!radios.length) {
// No select and no radio buttons. The units need to be entered in the input text. // No select and no radio buttons. The units need to be entered in the input text.
return questionDiv; return questionEl;
} }
this.question.options = []; this.question.options = [];
@ -126,7 +126,7 @@ export class CoreQuestionBaseComponent {
disabled: radioEl.disabled disabled: radioEl.disabled
}, },
// Get the label with the question text. // Get the label with the question text.
label = <HTMLElement> questionDiv.querySelector('label[for="' + option.id + '"]'); label = <HTMLElement> questionEl.querySelector('label[for="' + option.id + '"]');
this.question.optionsName = option.name; this.question.optionsName = option.name;
@ -154,9 +154,9 @@ export class CoreQuestionBaseComponent {
} }
// Check which one should be displayed first: the options or the input. // 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 = 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); return this.questionHelper.showComponentError(this.onAbort);
} }
const div = document.createElement('div'); const element = this.domUtils.convertToElement(this.question.html);
div.innerHTML = this.question.html;
// Extract question text. // 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') { if (typeof this.question.text == 'undefined') {
this.logger.warn('Aborting because of an error parsing question.', this.question.name); this.logger.warn('Aborting because of an error parsing question.', this.question.name);
return this.questionHelper.showComponentError(this.onAbort); 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. * @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
*/ */
initEssayComponent(): void | HTMLElement { initEssayComponent(): void | HTMLElement {
const questionDiv = this.initComponent(); const questionEl = this.initComponent();
if (questionDiv) { if (questionEl) {
// First search the textarea. // First search the textarea.
const textarea = <HTMLTextAreaElement> questionDiv.querySelector('textarea[name*=_answer]'); const textarea = <HTMLTextAreaElement> questionEl.querySelector('textarea[name*=_answer]');
this.question.allowsAttachments = !!questionDiv.querySelector('div[id*=filemanager]'); this.question.allowsAttachments = !!questionEl.querySelector('div[id*=filemanager]');
this.question.isMonospaced = !!questionDiv.querySelector('.qtype_essay_monospaced'); this.question.isMonospaced = !!questionEl.querySelector('.qtype_essay_monospaced');
this.question.isPlainText = this.question.isMonospaced || !!questionDiv.querySelector('.qtype_essay_plain'); this.question.isPlainText = this.question.isMonospaced || !!questionEl.querySelector('.qtype_essay_plain');
this.question.hasDraftFiles = this.questionHelper.hasDraftFileUrls(questionDiv.innerHTML); this.question.hasDraftFiles = this.questionHelper.hasDraftFileUrls(questionEl.innerHTML);
if (!textarea) { if (!textarea) {
// Textarea not found, we might be in review. Search the answer and the attachments. // 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.question.attachments = this.questionHelper.getQuestionAttachmentsFromHtml(
this.domUtils.getContentsOfElement(questionDiv, '.attachments')); this.domUtils.getContentsOfElement(questionEl, '.attachments'));
} else { } else {
// Textarea found. // Textarea found.
const input = <HTMLInputElement> questionDiv.querySelector('input[type="hidden"][name*=answerformat]'), const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=answerformat]'),
content = textarea.innerHTML; content = textarea.innerHTML;
this.question.textarea = { this.question.textarea = {
@ -241,11 +240,10 @@ export class CoreQuestionBaseComponent {
return this.questionHelper.showComponentError(this.onAbort); return this.questionHelper.showComponentError(this.onAbort);
} }
const div = document.createElement('div'); const element = this.domUtils.convertToElement(this.question.html);
div.innerHTML = this.question.html;
// Get question content. // Get question content.
const content = <HTMLElement> div.querySelector(contentSelector); const content = <HTMLElement> element.querySelector(contentSelector);
if (!content) { if (!content) {
this.logger.warn('Aborting because of an error parsing question.', this.question.name); 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'); this.domUtils.removeElement(content, '.validationerror');
// Replace Moodle's correct/incorrect and feedback classes with our own. // Replace Moodle's correct/incorrect and feedback classes with our own.
this.questionHelper.replaceCorrectnessClasses(div); this.questionHelper.replaceCorrectnessClasses(element);
this.questionHelper.replaceFeedbackClasses(div); this.questionHelper.replaceFeedbackClasses(element);
// Treat the correct/incorrect icons. // Treat the correct/incorrect icons.
this.questionHelper.treatCorrectnessIcons(div); this.questionHelper.treatCorrectnessIcons(element);
// Set the question text. // Set the question text.
this.question.text = content.innerHTML; 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. * @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
*/ */
initInputTextComponent(): void | HTMLElement { initInputTextComponent(): void | HTMLElement {
const questionDiv = this.initComponent(); const questionEl = this.initComponent();
if (questionDiv) { if (questionEl) {
// Get the input element. // Get the input element.
const input = <HTMLInputElement> questionDiv.querySelector('input[type="text"][name*=answer]'); const input = <HTMLInputElement> questionEl.querySelector('input[type="text"][name*=answer]');
if (!input) { if (!input) {
this.logger.warn('Aborting because couldn\'t find input.', this.question.name); 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. * @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
*/ */
initMatchComponent(): void | HTMLElement { initMatchComponent(): void | HTMLElement {
const questionDiv = this.initComponent(); const questionEl = this.initComponent();
if (questionDiv) { if (questionEl) {
// Find rows. // Find rows.
const rows = Array.from(questionDiv.querySelectorAll('table.answer tr')); const rows = Array.from(questionEl.querySelectorAll('table.answer tr'));
if (!rows || !rows.length) { if (!rows || !rows.length) {
this.logger.warn('Aborting because couldn\'t find any row.', this.question.name); this.logger.warn('Aborting because couldn\'t find any row.', this.question.name);
@ -394,7 +392,7 @@ export class CoreQuestionBaseComponent {
this.question.loaded = true; 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. * @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
*/ */
initMultichoiceComponent(): void | HTMLElement { initMultichoiceComponent(): void | HTMLElement {
const questionDiv = this.initComponent(); const questionEl = this.initComponent();
if (questionDiv) { if (questionEl) {
// Get the prompt. // 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). // Search radio buttons first (single choice).
let options = <HTMLInputElement[]> Array.from(questionDiv.querySelectorAll('input[type="radio"]')); let options = <HTMLInputElement[]> Array.from(questionEl.querySelectorAll('input[type="radio"]'));
if (!options || !options.length) { if (!options || !options.length) {
// Radio buttons not found, it should be a multi answer. Search for checkbox. // Radio buttons not found, it should be a multi answer. Search for checkbox.
this.question.multi = true; this.question.multi = true;
options = <HTMLInputElement[]> Array.from(questionDiv.querySelectorAll('input[type="checkbox"]')); options = <HTMLInputElement[]> Array.from(questionEl.querySelectorAll('input[type="checkbox"]'));
if (!options || !options.length) { if (!options || !options.length) {
// No checkbox found either. Abort. // No checkbox found either. Abort.
@ -441,7 +439,7 @@ export class CoreQuestionBaseComponent {
this.question.optionsName = option.name; this.question.optionsName = option.name;
// Get the label with the question text. // 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) { if (label) {
option.text = label.innerHTML; option.text = label.innerHTML;
@ -483,6 +481,6 @@ export class CoreQuestionBaseComponent {
} }
} }
return questionDiv; return questionEl;
} }
} }

View File

@ -29,7 +29,6 @@ import { CoreQuestionDelegate } from './delegate';
@Injectable() @Injectable()
export class CoreQuestionHelperProvider { export class CoreQuestionHelperProvider {
protected lastErrorShown = 0; protected lastErrorShown = 0;
protected div = document.createElement('div'); // A div element to search in HTML code.
constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider,
private questionProvider: CoreQuestionProvider, private sitesProvider: CoreSitesProvider, private questionProvider: CoreQuestionProvider, private sitesProvider: CoreSitesProvider,
@ -70,15 +69,15 @@ export class CoreQuestionHelperProvider {
extractQbehaviourButtons(question: any, selector?: string): void { extractQbehaviourButtons(question: any, selector?: string): void {
selector = selector || '.im-controls input[type="submit"]'; selector = selector || '.im-controls input[type="submit"]';
this.div.innerHTML = question.html; const element = this.domUtils.convertToElement(question.html);
// Search the buttons. // Search the buttons.
const buttons = <HTMLInputElement[]> Array.from(this.div.querySelectorAll(selector)); const buttons = <HTMLInputElement[]> Array.from(element.querySelectorAll(selector));
buttons.forEach((button) => { buttons.forEach((button) => {
this.addBehaviourButton(question, 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. * @return {boolean} Wether the certainty is found.
*/ */
extractQbehaviourCBM(question: any): boolean { 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 = []; question.behaviourCertaintyOptions = [];
labels.forEach((label) => { labels.forEach((label) => {
@ -158,10 +157,10 @@ export class CoreQuestionHelperProvider {
* @return {boolean} Whether the seen input is found. * @return {boolean} Whether the seen input is found.
*/ */
extractQbehaviourSeenInput(question: any): boolean { extractQbehaviourSeenInput(question: any): boolean {
this.div.innerHTML = question.html; const element = this.domUtils.convertToElement(question.html);
// Search the "seen" input. // Search the "seen" input.
const seenInput = <HTMLInputElement> this.div.querySelector('input[type="hidden"][name*=seen]'); const seenInput = <HTMLInputElement> element.querySelector('input[type="hidden"][name*=seen]');
if (seenInput) { if (seenInput) {
// Get the data and remove the input. // Get the data and remove the input.
question.behaviourSeenInput = { question.behaviourSeenInput = {
@ -169,7 +168,7 @@ export class CoreQuestionHelperProvider {
value: seenInput.value value: seenInput.value
}; };
seenInput.parentElement.removeChild(seenInput); seenInput.parentElement.removeChild(seenInput);
question.html = this.div.innerHTML; question.html = element.innerHTML;
return true; return true;
} }
@ -214,9 +213,9 @@ export class CoreQuestionHelperProvider {
* @param {string} attrName Name of the attribute to store the HTML in. * @param {string} attrName Name of the attribute to store the HTML in.
*/ */
protected extractQuestionLastElementNotInContent(question: any, selector: string, attrName: string): void { protected extractQuestionLastElementNotInContent(question: any, selector: string, attrName: string): void {
this.div.innerHTML = question.html; const element = this.domUtils.convertToElement(question.html);
const matches = <HTMLElement[]> Array.from(this.div.querySelectorAll(selector)); const matches = <HTMLElement[]> Array.from(element.querySelectorAll(selector));
// Get the last element and check it's not in the question contents. // Get the last element and check it's not in the question contents.
let last = matches.pop(); 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. // Not in question contents. Add it to a separate attribute and remove it from the HTML.
question[attrName] = last.innerHTML; question[attrName] = last.innerHTML;
last.parentElement.removeChild(last); last.parentElement.removeChild(last);
question.html = this.div.innerHTML; question.html = element.innerHTML;
return; return;
} }
@ -283,11 +282,10 @@ export class CoreQuestionHelperProvider {
* @return {any} Object where the keys are the names. * @return {any} Object where the keys are the names.
*/ */
getAllInputNamesFromHtml(html: string): any { getAllInputNamesFromHtml(html: string): any {
const form = document.createElement('form'), const element = this.domUtils.convertToElement('<form>' + html + '</form>'),
form = <HTMLFormElement> element.children[0],
answers = {}; answers = {};
form.innerHTML = html;
// Search all input elements. // Search all input elements.
Array.from(form.elements).forEach((element: HTMLInputElement) => { Array.from(form.elements).forEach((element: HTMLInputElement) => {
const name = element.name || ''; const name = element.name || '';
@ -350,13 +348,13 @@ export class CoreQuestionHelperProvider {
* @return {Object[]} Attachments. * @return {Object[]} Attachments.
*/ */
getQuestionAttachmentsFromHtml(html: string): any[] { getQuestionAttachmentsFromHtml(html: string): any[] {
this.div.innerHTML = html; const element = this.domUtils.convertToElement(html);
// Remove the filemanager (area to attach files to a question). // 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. // Search the anchors.
const anchors = Array.from(this.div.querySelectorAll('a')), const anchors = Array.from(element.querySelectorAll('a')),
attachments = []; attachments = [];
anchors.forEach((anchor) => { anchors.forEach((anchor) => {
@ -383,10 +381,10 @@ export class CoreQuestionHelperProvider {
*/ */
getQuestionSequenceCheckFromHtml(html: string): {name: string, value: string} { getQuestionSequenceCheckFromHtml(html: string): {name: string, value: string} {
if (html) { if (html) {
this.div.innerHTML = html; const element = this.domUtils.convertToElement(html);
// Search the input holding the sequencecheck. // Search the input holding the sequencecheck.
const input = <HTMLInputElement> this.div.querySelector('input[name*=sequencecheck]'); const input = <HTMLInputElement> element.querySelector('input[name*=sequencecheck]');
if (input && typeof input.name != 'undefined' && typeof input.value != 'undefined') { if (input && typeof input.name != 'undefined' && typeof input.value != 'undefined') {
return { return {
name: input.name, name: input.name,
@ -415,9 +413,9 @@ export class CoreQuestionHelperProvider {
* @return {string} Validation error message if present. * @return {string} Validation error message if present.
*/ */
getValidationErrorFromHtml(html: string): string { 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. * @param {any} question Question.
*/ */
loadLocalAnswersInHtml(question: any): void { loadLocalAnswersInHtml(question: any): void {
const form = document.createElement('form'); const element = this.domUtils.convertToElement('<form>' + question.html + '</form>'),
form.innerHTML = question.html; form = <HTMLFormElement> element.children[0];
// Search all input elements. // Search all input elements.
Array.from(form.elements).forEach((element: HTMLInputElement | HTMLButtonElement) => { Array.from(form.elements).forEach((element: HTMLInputElement | HTMLButtonElement) => {
@ -574,9 +572,9 @@ export class CoreQuestionHelperProvider {
* @return {boolean} Whether the button is found. * @return {boolean} Whether the button is found.
*/ */
protected searchBehaviourButton(question: any, htmlProperty: string, selector: string): boolean { protected searchBehaviourButton(question: any, htmlProperty: string, selector: string): boolean {
this.div.innerHTML = question[htmlProperty]; const element = this.domUtils.convertToElement(question[htmlProperty]);
const button = <HTMLInputElement> this.div.querySelector(selector); const button = <HTMLInputElement> element.querySelector(selector);
if (button) { if (button) {
// Add a behaviour button to the question's "behaviourButtons" property. // Add a behaviour button to the question's "behaviourButtons" property.
this.addBehaviourButton(question, button); this.addBehaviourButton(question, button);
@ -585,7 +583,7 @@ export class CoreQuestionHelperProvider {
button.parentElement.removeChild(button); button.parentElement.removeChild(button);
// Update the question's html. // Update the question's html.
question[htmlProperty] = this.div.innerHTML; question[htmlProperty] = element.innerHTML;
return true; return true;
} }

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Moodle Desktop</title> <!-- Title is used in desktop, but should be ignored in mobile apps. --> <title>Moodle Desktop</title> <!-- Title is used in desktop, but should be ignored in mobile apps. -->
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src * filesystem: cdvfile: file: gap: https://ssl.gstatic.com; img-src * filesystem: gap: data: cdvfile: file: https://ssl.gstatic.com android-webview-video-poster: blob:; style-src 'self' 'unsafe-inline' filesystem: cdvfile: file:; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:* filesystem: cdvfile: file:; media-src * filesystem: cdvfile: file: gap: blob:"> <meta http-equiv="Content-Security-Policy" content="default-src * filesystem: cdvfile: file: data: gap: https://ssl.gstatic.com; img-src * filesystem: gap: data: cdvfile: file: https://ssl.gstatic.com android-webview-video-poster: blob:; style-src 'self' 'unsafe-inline' filesystem: cdvfile: file:; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:* filesystem: cdvfile: file:; media-src * filesystem: cdvfile: file: gap: blob:">
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no"> <meta name="msapplication-tap-highlight" content="no">

View File

@ -35,7 +35,8 @@ export class CoreDomUtilsProvider {
'search', 'tel', 'text', 'time', 'url', 'week']; 'search', 'tel', 'text', 'time', 'url', 'week'];
protected INSTANCE_ID_ATTR_NAME = 'core-instance-id'; 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 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 instances: {[id: string]: any} = {}; // Store component/directive instances by id.
protected lastInstanceId = 0; protected lastInstanceId = 0;
@ -124,6 +125,28 @@ export class CoreDomUtilsProvider {
return Promise.resolve(); 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. * Create a "cancelled" error. These errors won't display an error message in showErrorModal functions.
* *
@ -169,8 +192,8 @@ export class CoreDomUtilsProvider {
const urls = []; const urls = [];
let elements; let elements;
this.element.innerHTML = html; const element = this.convertToElement(html);
elements = this.element.querySelectorAll('a, img, audio, video, source, track'); elements = element.querySelectorAll('a, img, audio, video, source, track');
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
const element = elements[i]; const element = elements[i];
@ -599,21 +622,21 @@ export class CoreDomUtilsProvider {
removeElementFromHtml(html: string, selector: string, removeAll?: boolean): string { removeElementFromHtml(html: string, selector: string, removeAll?: boolean): string {
let selected; let selected;
this.element.innerHTML = html; const element = this.convertToElement(html);
if (removeAll) { if (removeAll) {
selected = this.element.querySelectorAll(selector); selected = element.querySelectorAll(selector);
for (let i = 0; i < selected.length; i++) { for (let i = 0; i < selected.length; i++) {
selected[i].remove(); selected[i].remove();
} }
} else { } else {
selected = this.element.querySelector(selector); selected = element.querySelector(selector);
if (selected) { if (selected) {
selected.remove(); selected.remove();
} }
} }
return this.element.innerHTML; return element.innerHTML;
} }
/** /**
@ -665,10 +688,10 @@ export class CoreDomUtilsProvider {
let media, let media,
anchors; anchors;
this.element.innerHTML = html; const element = this.convertToElement(html);
// Treat elements with src (img, audio, video, ...). // 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) => { media.forEach((media: HTMLElement) => {
let newSrc = paths[this.textUtils.decodeURIComponent(media.getAttribute('src'))]; let newSrc = paths[this.textUtils.decodeURIComponent(media.getAttribute('src'))];
@ -686,7 +709,7 @@ export class CoreDomUtilsProvider {
}); });
// Now treat links. // Now treat links.
anchors = this.element.querySelectorAll('a'); anchors = element.querySelectorAll('a');
anchors.forEach((anchor: HTMLElement) => { anchors.forEach((anchor: HTMLElement) => {
const href = this.textUtils.decodeURIComponent(anchor.getAttribute('href')), const href = this.textUtils.decodeURIComponent(anchor.getAttribute('href')),
newUrl = paths[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. * Converts HTML formatted text to DOM element(s).
*
* @param {string} text HTML text. * @param {string} text HTML text.
* @return {HTMLCollection} Same text converted to HTMLCollection. * @return {HTMLCollection} Same text converted to HTMLCollection.
*/ */
toDom(text: string): HTMLCollection { toDom(text: string): HTMLCollection {
const element = document.createElement('div'); const element = this.convertToElement(text);
element.innerHTML = text;
return element.children; return element.children;
} }

View File

@ -68,7 +68,7 @@ export class CoreTextUtilsProvider {
{old: /_mmaModWorkshop/g, new: '_AddonModWorkshop'}, {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) { } constructor(private translate: TranslateService, private langProvider: CoreLangProvider, private modalCtrl: ModalController) { }
@ -142,8 +142,8 @@ export class CoreTextUtilsProvider {
// First, we use a regexpr. // First, we use a regexpr.
text = text.replace(/(<([^>]+)>)/ig, ''); text = text.replace(/(<([^>]+)>)/ig, '');
// Then, we rely on the browser. We need to wrap the text to be sure is HTML. // Then, we rely on the browser. We need to wrap the text to be sure is HTML.
this.element.innerHTML = text; const element = this.convertToElement(text);
text = this.element.textContent; text = element.textContent;
// Recover or remove new lines. // Recover or remove new lines.
text = this.replaceNewLines(text, singleLine ? ' ' : '<br>'); text = this.replaceNewLines(text, singleLine ? ' ' : '<br>');
@ -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. * Count words in a text.
* *
@ -225,9 +248,8 @@ export class CoreTextUtilsProvider {
*/ */
decodeHTMLEntities(text: string): string { decodeHTMLEntities(text: string): string {
if (text) { if (text) {
this.element.innerHTML = text; const element = this.convertToElement(text);
text = this.element.textContent; text = element.textContent;
this.element.textContent = '';
} }
return text; return text;