MOBILE-2444 media: Prevent multiple requests and EXC_BAD_ACCESS

main
Dani Palou 2018-07-09 10:12:29 +02:00
parent d2b175ef0a
commit cbf7214cc3
14 changed files with 210 additions and 175 deletions

View File

@ -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 = <HTMLInputElement> this.div.querySelector('input[type="button"]');
const button = <HTMLInputElement> 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 = <HTMLInputElement[]> Array.from(this.div.querySelectorAll('input[type="hidden"]'));
const hiddenInputs = <HTMLInputElement[]> 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 = <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');
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 = <HTMLInputElement> this.div.querySelector('input[type="checkbox"][name*="answer"]');
let input = <HTMLInputElement> 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 = <HTMLInputElement> this.div.querySelector('input[type="number"],input[type="text"]');
input = <HTMLInputElement> 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 = <HTMLSelectElement> this.div.querySelector('select');
const select = <HTMLSelectElement> 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();
}
}

View File

@ -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"]');
}
}

View File

@ -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');
}
/**

View File

@ -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');
}
/**

View File

@ -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"]'));
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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 = <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) => {
this.question.text += inputEl.outerHTML;

View File

@ -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 = <HTMLInputElement> questionDiv.querySelector('input[type="hidden"][name*=seen]');
const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=seen]');
if (input) {
this.question.seenInput = {
name: input.name,

View File

@ -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<any>} Return a promise resolved when done if async, void if sync.
*/
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.
const textarea = <HTMLTextAreaElement> this.div.querySelector('textarea[name*=_answer]');
const textarea = <HTMLTextAreaElement> element.querySelector('textarea[name*=_answer]');
if (textarea && typeof answers[textarea.name] != 'undefined') {
// Add some HTML to the text if needed.

View File

@ -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 = <HTMLSelectElement> questionDiv.querySelector('select[name*=unit]'),
select = <HTMLSelectElement> 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 = <HTMLInputElement[]> Array.from(questionDiv.querySelectorAll('input[type="radio"]'));
const radios = <HTMLInputElement[]> 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 = <HTMLElement> questionDiv.querySelector('label[for="' + option.id + '"]');
label = <HTMLElement> 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 = <HTMLTextAreaElement> 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 = <HTMLTextAreaElement> 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 = <HTMLInputElement> questionDiv.querySelector('input[type="hidden"][name*=answerformat]'),
const input = <HTMLInputElement> 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 = <HTMLElement> div.querySelector(contentSelector);
const content = <HTMLElement> 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 = <HTMLInputElement> questionDiv.querySelector('input[type="text"][name*=answer]');
const input = <HTMLInputElement> 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 = <HTMLInputElement[]> Array.from(questionDiv.querySelectorAll('input[type="radio"]'));
let options = <HTMLInputElement[]> 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 = <HTMLInputElement[]> Array.from(questionDiv.querySelectorAll('input[type="checkbox"]'));
options = <HTMLInputElement[]> 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;
}
}

View File

@ -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 = <HTMLInputElement[]> Array.from(this.div.querySelectorAll(selector));
const buttons = <HTMLInputElement[]> 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 = <HTMLInputElement> this.div.querySelector('input[type="hidden"][name*=seen]');
const seenInput = <HTMLInputElement> 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 = <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.
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('<form>' + html + '</form>'),
form = <HTMLFormElement> 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 = <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') {
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('<form>' + question.html + '</form>'),
form = <HTMLFormElement> 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 = <HTMLInputElement> this.div.querySelector(selector);
const button = <HTMLInputElement> 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;
}

View File

@ -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;
}

View File

@ -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 ? ' ' : '<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.
*
@ -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;