' + CoreText.replaceNewLines(
+ CoreText.escapeHTML(error.backtrace, false),
' ',
);
}
@@ -467,7 +468,7 @@ export class CoreDomUtilsProvider {
}
// We received an object instead of a string. Search for common properties.
- errorMessage = CoreTextUtils.getErrorMessageFromError(error);
+ errorMessage = CoreErrorHelper.getErrorMessageFromError(error);
CoreErrorLogs.addErrorLog({ message: JSON.stringify(error), type: errorMessage || '', time: new Date().getTime() });
if (!errorMessage) {
// No common properties found, just stringify it.
@@ -484,7 +485,7 @@ export class CoreDomUtilsProvider {
errorMessage = error;
}
- let message = CoreTextUtils.decodeHTML(needsTranslate ? Translate.instant(errorMessage) : errorMessage);
+ let message = CoreText.decodeHTML(needsTranslate ? Translate.instant(errorMessage) : errorMessage);
if (extraInfo) {
message += extraInfo;
@@ -705,7 +706,7 @@ export class CoreDomUtilsProvider {
const currentSrc = media.getAttribute('src');
const newSrc = currentSrc ?
paths[CoreUrl.removeUrlParts(
- CoreTextUtils.decodeURIComponent(currentSrc),
+ CoreUrl.decodeURIComponent(currentSrc),
[CoreUrlPartNames.Query, CoreUrlPartNames.Fragment],
)] :
undefined;
@@ -717,7 +718,7 @@ export class CoreDomUtilsProvider {
// Treat video posters.
const currentPoster = media.getAttribute('poster');
if (media.tagName == 'VIDEO' && currentPoster) {
- const newPoster = paths[CoreTextUtils.decodeURIComponent(currentPoster)];
+ const newPoster = paths[CoreUrl.decodeURIComponent(currentPoster)];
if (newPoster !== undefined) {
media.setAttribute('poster', newPoster);
}
@@ -730,7 +731,7 @@ export class CoreDomUtilsProvider {
const currentHref = anchor.getAttribute('href');
const newHref = currentHref ?
paths[CoreUrl.removeUrlParts(
- CoreTextUtils.decodeURIComponent(currentHref),
+ CoreUrl.decodeURIComponent(currentHref),
[CoreUrlPartNames.Query, CoreUrlPartNames.Fragment],
)] :
undefined;
@@ -838,7 +839,7 @@ export class CoreDomUtilsProvider {
? options.message
: options.message?.value || '';
- const hasHTMLTags = CoreTextUtils.hasHTMLTags(message);
+ const hasHTMLTags = CoreText.hasHTMLTags(message);
if (hasHTMLTags && !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) {
// Treat multilang.
@@ -1022,7 +1023,7 @@ export class CoreDomUtilsProvider {
* @returns Promise resolved with the alert modal.
*/
async showErrorModal(
- error: CoreError | CoreTextErrorObject | string,
+ error: CoreError | CoreErrorObject | string,
needsTranslate?: boolean,
autocloseTime?: number,
): Promise {
@@ -1117,7 +1118,7 @@ export class CoreDomUtilsProvider {
let errorMessage = error || undefined;
if (error && typeof error != 'string') {
- errorMessage = CoreTextUtils.getErrorMessageFromError(error);
+ errorMessage = CoreErrorHelper.getErrorMessageFromError(error);
}
return this.showErrorModal(
diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts
index 834394e74..78a3b5386 100644
--- a/src/core/services/utils/text.ts
+++ b/src/core/services/utils/text.ts
@@ -16,105 +16,33 @@ import { Injectable } from '@angular/core';
import { SafeUrl } from '@angular/platform-browser';
import { CoreAnyError, CoreError } from '@classes/errors/error';
-import { DomSanitizer, makeSingleton, Translate } from '@singletons';
+import { makeSingleton } from '@singletons';
import { CoreWSFile } from '@services/ws';
-import { Locutus } from '@singletons/locutus';
import { CoreFileHelper } from '@services/file-helper';
import { CoreUrl } from '@singletons/url';
-import { AlertButton } from '@ionic/angular';
-import { CorePath } from '@singletons/path';
-import { CorePlatform } from '@services/platform';
import { CoreDom } from '@singletons/dom';
import { CoreText } from '@singletons/text';
import { CoreViewer, CoreViewerTextOptions } from '@features/viewer/services/viewer';
+import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper';
/**
- * Different type of errors the app can treat.
- */
-export type CoreTextErrorObject = {
- message?: string;
- error?: string;
- content?: string;
- body?: string;
- debuginfo?: string;
- backtrace?: string;
- title?: string;
- buttons?: AlertButton[];
-};
-
-/*
* "Utils" service with helper functions for text.
-*/
+ *
+ * @deprecated since 4.5. Some of the functions have been moved to CoreText but not all of them, check function deprecation message.
+ */
@Injectable({ providedIn: 'root' })
export class CoreTextUtilsProvider {
- // List of regular expressions to convert the old nomenclature to new nomenclature for disabled features.
- protected readonly DISABLED_FEATURES_COMPAT_REGEXPS: { old: RegExp; new: string }[] = [
- { old: /\$mmLoginEmailSignup/g, new: 'CoreLoginEmailSignup' },
- { old: /\$mmSideMenuDelegate/g, new: 'CoreMainMenuDelegate' },
- { old: /\$mmCoursesDelegate/g, new: 'CoreCourseOptionsDelegate' },
- { old: /\$mmUserDelegate/g, new: 'CoreUserDelegate' },
- { old: /\$mmCourseDelegate/g, new: 'CoreCourseModuleDelegate' },
- { old: /_mmCourses/g, new: '_CoreCourses' },
- { old: /_mmaFrontpage/g, new: '_CoreSiteHome' },
- { old: /_mmaGrades/g, new: '_CoreGrades' },
- { old: /_mmaCompetency/g, new: '_AddonCompetency' },
- { old: /_mmaNotifications/g, new: '_AddonNotifications' },
- { old: /_mmaMessages/g, new: '_AddonMessages' },
- { old: /_mmaCalendar/g, new: '_AddonCalendar' },
- { old: /_mmaFiles/g, new: '_AddonPrivateFiles' },
- { old: /_mmaParticipants/g, new: '_CoreUserParticipants' },
- { old: /_mmaCourseCompletion/g, new: '_AddonCourseCompletion' },
- { old: /_mmaNotes/g, new: '_AddonNotes' },
- { old: /_mmaBadges/g, new: '_AddonBadges' },
- { old: /files_privatefiles/g, new: 'AddonPrivateFilesPrivateFiles' },
- { old: /files_sitefiles/g, new: 'AddonPrivateFilesSiteFiles' },
- { old: /files_upload/g, new: 'AddonPrivateFilesUpload' },
- { old: /_mmaModAssign/g, new: '_AddonModAssign' },
- { old: /_mmaModBigbluebuttonbn/g, new: '_AddonModBBB' },
- { old: /_mmaModBook/g, new: '_AddonModBook' },
- { old: /_mmaModChat/g, new: '_AddonModChat' },
- { old: /_mmaModChoice/g, new: '_AddonModChoice' },
- { old: /_mmaModData/g, new: '_AddonModData' },
- { old: /_mmaModFeedback/g, new: '_AddonModFeedback' },
- { old: /_mmaModFolder/g, new: '_AddonModFolder' },
- { old: /_mmaModForum/g, new: '_AddonModForum' },
- { old: /_mmaModGlossary/g, new: '_AddonModGlossary' },
- { old: /_mmaModH5pactivity/g, new: '_AddonModH5PActivity' },
- { old: /_mmaModImscp/g, new: '_AddonModImscp' },
- { old: /_mmaModLabel/g, new: '_AddonModLabel' },
- { old: /_mmaModLesson/g, new: '_AddonModLesson' },
- { old: /_mmaModLti/g, new: '_AddonModLti' },
- { old: /_mmaModPage/g, new: '_AddonModPage' },
- { old: /_mmaModQuiz/g, new: '_AddonModQuiz' },
- { old: /_mmaModResource/g, new: '_AddonModResource' },
- { old: /_mmaModScorm/g, new: '_AddonModScorm' },
- { old: /_mmaModSurvey/g, new: '_AddonModSurvey' },
- { old: /_mmaModUrl/g, new: '_AddonModUrl' },
- { old: /_mmaModWiki/g, new: '_AddonModWiki' },
- { old: /_mmaModWorkshop/g, new: '_AddonModWorkshop' },
- { old: /remoteAddOn_/g, new: 'sitePlugin_' },
- { old: /AddonNotes:addNote/g, new: 'AddonNotes:notes' },
- ];
-
- protected template: HTMLTemplateElement = document.createElement('template'); // A template element to convert HTML to element.
-
/**
* Add ending slash from a path or URL.
*
* @param text Text to treat.
* @returns Treated text.
+ *
+ * @deprecated since 4.5. Use CoreText.addEndingSlash instead.
*/
addEndingSlash(text: string): string {
- if (!text) {
- return '';
- }
-
- if (text.slice(-1) != '/') {
- return text + '/';
- }
-
- return text;
+ return CoreText.addEndingSlash(text);
}
/**
@@ -123,33 +51,14 @@ export class CoreTextUtilsProvider {
* @param error Error message or object.
* @param text Text to add.
* @returns Modified error.
+ *
+ * @deprecated since 4.5. Use CoreErrorHelper.addTextToError instead.
*/
- addTextToError(error: string | CoreError | CoreTextErrorObject | undefined | null, text: string): string | CoreTextErrorObject {
- if (typeof error == 'string') {
- return error + text;
- }
-
- if (error instanceof CoreError) {
- error.message += text;
-
- return error;
- }
-
- if (!error) {
- return text;
- }
-
- if (typeof error.message == 'string') {
- error.message += text;
- } else if (typeof error.error == 'string') {
- error.error += text;
- } else if (typeof error.content == 'string') {
- error.content += text;
- } else if (typeof error.body == 'string') {
- error.body += text;
- }
-
- return error;
+ addTextToError(
+ error: string | CoreError | CoreErrorObject | undefined | null,
+ text: string,
+ ): string | CoreErrorObject {
+ return CoreErrorHelper.addTextToError(error, text);
}
/**
@@ -158,19 +67,11 @@ export class CoreTextUtilsProvider {
* @param error Error message or object.
* @param title Title to add.
* @returns Modified error.
+ *
+ * @deprecated since 4.5. Use CoreErrorHelper.addTitleToError instead.
*/
- addTitleToError(error: string | CoreError | CoreTextErrorObject | undefined | null, title: string): CoreTextErrorObject {
- let improvedError: CoreTextErrorObject = {};
-
- if (typeof error === 'string') {
- improvedError.message = error;
- } else if (error && 'message' in error) {
- improvedError = error;
- }
-
- improvedError.title = improvedError.title || title;
-
- return improvedError;
+ addTitleToError(error: string | CoreError | CoreErrorObject | undefined | null, title: string): CoreErrorObject {
+ return CoreErrorHelper.addTitleToError(error, title);
}
/**
@@ -178,16 +79,11 @@ export class CoreTextUtilsProvider {
*
* @param address The address.
* @returns URL to view the address.
+ *
+ * @deprecated since 4.5. Use CoreUrl.buildAddressURL instead.
*/
buildAddressURL(address: string): SafeUrl {
- const parsedUrl = CoreUrl.parse(address);
- if (parsedUrl?.protocol) {
- // It's already a URL, don't convert it.
- return DomSanitizer.bypassSecurityTrustUrl(address);
- }
-
- return DomSanitizer.bypassSecurityTrustUrl((CorePlatform.isAndroid() ? 'geo:0,0?q=' : 'http://maps.google.com?q=') +
- encodeURIComponent(address));
+ return CoreUrl.buildAddressURL(address);
}
/**
@@ -195,17 +91,11 @@ export class CoreTextUtilsProvider {
*
* @param messages Messages to show.
* @returns Message with all the messages.
+ *
+ * @deprecated since 4.5. Use CoreText.buildMessage instead.
*/
buildMessage(messages: string[]): string {
- let result = '';
-
- messages.forEach((message) => {
- if (message) {
- result += `
${message}
`;
- }
- });
-
- return result;
+ return CoreText.buildMessage(messages);
}
/**
@@ -213,31 +103,11 @@ export class CoreTextUtilsProvider {
*
* @param paragraphs List of paragraphs.
* @returns Built message.
+ *
+ * @deprecated since 4.5. Use CoreErrorHelper.buildSeveralParagraphsMessage instead.
*/
- buildSeveralParagraphsMessage(paragraphs: (string | CoreTextErrorObject)[]): string {
- // Filter invalid messages, and convert them to messages in case they're errors.
- const messages: string[] = [];
-
- paragraphs.forEach(paragraph => {
- // If it's an error, get its message.
- const message = this.getErrorMessageFromError(paragraph);
-
- if (paragraph && message) {
- messages.push(message);
- }
- });
-
- if (messages.length < 2) {
- return messages[0] || '';
- }
-
- let builtMessage = messages[0];
-
- for (let i = 1; i < messages.length; i++) {
- builtMessage = Translate.instant('core.twoparagraphs', { p1: builtMessage, p2: messages[i] });
- }
-
- return builtMessage;
+ buildSeveralParagraphsMessage(paragraphs: (string | CoreErrorObject)[]): string {
+ return CoreErrorHelper.buildSeveralParagraphsMessage(paragraphs);
}
/**
@@ -246,30 +116,11 @@ export class CoreTextUtilsProvider {
* @param bytes Number of bytes to convert.
* @param precision Number of digits after the decimal separator.
* @returns Size in human readable format.
+ *
+ * @deprecated since 4.5. Use CoreText.bytesToSize instead.
*/
bytesToSize(bytes: number, precision: number = 2): string {
- if (bytes === undefined || bytes === null || bytes < 0) {
- return Translate.instant('core.notapplicable');
- }
-
- if (precision < 0) {
- precision = 2;
- }
-
- const keys = ['core.sizeb', 'core.sizekb', 'core.sizemb', 'core.sizegb', 'core.sizetb'];
- const units = Translate.instant(keys);
- let pos = 0;
-
- if (bytes >= 1024) {
- while (bytes >= 1024) {
- pos++;
- bytes = bytes / 1024;
- }
- // Round to "precision" decimals if needed.
- bytes = Number(Math.round(parseFloat(bytes + 'e+' + precision)) + 'e-' + precision);
- }
-
- return Translate.instant('core.humanreadablesize', { size: bytes, unit: units[keys[pos]] });
+ return CoreText.bytesToSize(bytes, precision);
}
/**
@@ -278,13 +129,11 @@ export class CoreTextUtilsProvider {
* @param text HTML string.
* @param process Method to process the HTML.
* @returns Processed HTML string.
+ *
+ * @deprecated since 4.5. Use CoreText.processHTML instead.
*/
processHTML(text: string, process: (element: HTMLElement) => unknown): string {
- const element = this.convertToElement(text);
-
- process(element);
-
- return element.innerHTML;
+ return CoreText.processHTML(text, process);
}
/**
@@ -295,36 +144,11 @@ export class CoreTextUtilsProvider {
* @param options.singleLine True if new lines should be removed (all the text in a single line).
* @param options.trim True if text should be trimmed.
* @returns Clean text.
+ *
+ * @deprecated since 4.5. Use CoreText.cleanTags instead.
*/
cleanTags(text: string | undefined, options: { singleLine?: boolean; trim?: boolean } = {}): string {
- if (!text) {
- return '';
- }
-
- // 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.
- text = this.convertToElement(text).textContent || '';
- // Trim text
- text = options.trim ? text.trim() : text;
- // Recover or remove new lines.
- text = this.replaceNewLines(text, options.singleLine ? ' ' : ' ');
-
- return text;
- }
-
- /**
- * 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 html Text to convert.
- * @returns Element.
- */
- protected convertToElement(html: string): HTMLElement {
- // Add a div to hold the content, that's the element that will be returned.
- this.template.innerHTML = '
' + html + '
';
-
- return this.template.content.children[0];
+ return CoreText.cleanTags(text, options);
}
/**
@@ -333,37 +157,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to count.
* @returns Number of words.
+ *
+ * @deprecated since 4.5. Use CoreText.countWords instead.
*/
countWords(text?: string | null): number {
- if (!text || typeof text != 'string') {
- return 0;
- }
-
- // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
- // Also, br is a special case because it definitely delimits a word, but has no close tag.
- text = text.replace(/(<\/(?!a>|b>|del>|em>|i>|ins>|s>|small>|span>|strong>|sub>|sup>|u>)\w+>| | )/ig, '$1 ');
-
- // Now remove HTML tags.
- text = text.replace(/(<([^>]+)>)/ig, '');
- // Decode HTML entities.
- text = this.decodeHTMLEntities(text);
-
- // Now, the word count is the number of blocks of characters separated
- // by any sort of space. That seems to be the definition used by all other systems.
- // To be precise about what is considered to separate words:
- // * Anything that Unicode considers a 'Separator'
- // * Anything that Unicode considers a 'Control character'
- // * An em- or en- dash.
- let words: string[];
- try {
- words = text.split(/[\p{Z}\p{Cc}—–]+/u);
- } catch {
- // Unicode-aware flag not supported.
- words = text.split(/\s+/);
- }
-
- // Filter empty words.
- return words.filter(word => word).length;
+ return CoreText.countWords(text);
}
/**
@@ -371,21 +169,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to decode.
* @returns Decoded text.
+ *
+ * @deprecated since 4.5. Use CoreText.decodeHTML instead.
*/
decodeHTML(text: string | number): string {
- if (text === undefined || text === null || (typeof text == 'number' && isNaN(text))) {
- return '';
- } else if (typeof text != 'string') {
- return '' + text;
- }
-
- return text
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, '\'')
- .replace(/ /g, ' ');
+ return CoreText.decodeHTML(text);
}
/**
@@ -393,13 +181,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to decode.
* @returns Decoded text.
+ *
+ * @deprecated since 4.5. Use CoreText.decodeHTMLEntities instead.
*/
decodeHTMLEntities(text: string): string {
- if (text) {
- text = this.convertToElement(text).textContent || '';
- }
-
- return text;
+ return CoreText.decodeHTMLEntities(text);
}
/**
@@ -407,15 +193,11 @@ export class CoreTextUtilsProvider {
*
* @param uri URI to decode.
* @returns Decoded URI, or original URI if an exception is thrown.
+ *
+ * @deprecated since 4.5. Use CoreUrl.decodeURI instead.
*/
decodeURI(uri: string): string {
- try {
- return decodeURI(uri);
- } catch (ex) {
- // Error, use the original URI.
- }
-
- return uri;
+ return CoreUrl.decodeURI(uri);
}
/**
@@ -423,15 +205,11 @@ export class CoreTextUtilsProvider {
*
* @param uri URI to decode.
* @returns Decoded URI, or original URI if an exception is thrown.
+ *
+ * @deprecated since 4.5. Use CoreUrl.decodeURIComponent instead.
*/
decodeURIComponent(uri: string): string {
- try {
- return decodeURIComponent(uri);
- } catch (ex) {
- // Error, use the original URI.
- }
-
- return uri;
+ return CoreUrl.decodeURIComponent(uri);
}
/**
@@ -439,13 +217,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to escape.
* @returns Escaped text.
+ *
+ * @deprecated since 4.5. Use CoreText.escapeForRegex instead.
*/
escapeForRegex(text: string): string {
- if (!text || typeof text != 'string') {
- return '';
- }
-
- return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+ return CoreText.escapeForRegex(text);
}
/**
@@ -454,25 +230,11 @@ export class CoreTextUtilsProvider {
* @param text Text to escape.
* @param doubleEncode If false, it will not convert existing html entities. Defaults to true.
* @returns Escaped text.
+ *
+ * @deprecated since 4.5. Use CoreText.escapeHTML instead.
*/
- escapeHTML(text?: string | number | null, doubleEncode: boolean = true): string {
- if (text === undefined || text === null || (typeof text == 'number' && isNaN(text))) {
- return '';
- } else if (typeof text != 'string') {
- return '' + text;
- }
-
- if (doubleEncode) {
- text = text.replace(/&/g, '&');
- } else {
- text = text.replace(/&(?!amp;)(?!lt;)(?!gt;)(?!quot;)(?!#039;)/g, '&');
- }
-
- return text
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
+ escapeHTML(text?: string | number | null, doubleEncode = true): string {
+ return CoreText.escapeHTML(text, doubleEncode);
}
/**
@@ -480,20 +242,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to format.
* @returns Formatted text.
+ *
+ * @deprecated since 4.5. Use CoreText.formatHtmlLines instead.
*/
formatHtmlLines(text: string): string {
- const hasHTMLTags = this.hasHTMLTags(text);
- if (text.indexOf('
') == -1) {
- // Wrap the text in
tags.
- text = '
' + text + '
';
- }
-
- if (!hasHTMLTags) {
- // The text doesn't have HTML, replace new lines for .
- return this.replaceNewLines(text, ' ');
- }
-
- return text;
+ return CoreText.formatHtmlLines(text);
}
/**
@@ -501,46 +254,11 @@ export class CoreTextUtilsProvider {
*
* @param error Error.
* @returns Error message, undefined if not found.
+ *
+ * @deprecated since 4.5. Use CoreErrorHelper.getErrorMessageFromError instead.
*/
getErrorMessageFromError(error?: CoreAnyError): string | undefined {
- if (typeof error === 'string') {
- return error;
- }
-
- if (error instanceof CoreError) {
- return error.message;
- }
-
- if (!error) {
- return undefined;
- }
-
- if (error.message || error.error || error.content) {
- return error.message || error.error || error.content;
- }
-
- if (error.body) {
- return this.getErrorMessageFromHTML(error.body);
- }
-
- return undefined;
- }
-
- /**
- * Get the error message from an HTML error page.
- *
- * @param body HTML content.
- * @returns Error message or empty string if not found.
- */
- getErrorMessageFromHTML(body: string): string {
- // THe parser does not throw errors and scripts are not executed.
- const parser = new DOMParser();
- const doc = parser.parseFromString(body, 'text/html');
-
- // Errors are rendered using the "errorbox" and "errormessage" classes since Moodle 2.0.
- const element = doc.body.querySelector('.errorbox .errormessage');
-
- return element?.innerText.trim() ?? '';
+ return CoreErrorHelper.getErrorMessageFromError(error);
}
/**
@@ -548,11 +266,11 @@ export class CoreTextUtilsProvider {
*
* @param html HTML text.
* @returns Body HTML.
+ *
+ * @deprecated since 4.5. Use CoreDom.getHTMLBodyContent instead.
*/
getHTMLBodyContent(html: string): string {
- const matches = html.match(/([\s\S]*)<\/body>/im);
-
- return matches?.[1] ?? html;
+ return CoreDom.getHTMLBodyContent(html);
}
/**
@@ -560,16 +278,11 @@ export class CoreTextUtilsProvider {
*
* @param files Files to extract the URL from. They need to have the URL in a 'url' or 'fileurl' attribute.
* @returns Pluginfile URL, undefined if no files found.
+ *
+ * @deprecated since 4.5. Use CoreFileHelper.getTextPluginfileUrl instead.
*/
getTextPluginfileUrl(files: CoreWSFile[]): string | undefined {
- if (files?.length) {
- const url = CoreFileHelper.getFileUrl(files[0]);
-
- // Remove text after last slash (encoded or not).
- return url?.substring(0, Math.max(url.lastIndexOf('/'), url.lastIndexOf('%2F')));
- }
-
- return undefined;
+ return CoreFileHelper.getTextPluginfileUrl(files);
}
/**
@@ -577,9 +290,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to check.
* @returns Whether it has HTML tags.
+ *
+ * @deprecated since 4.5. Use CoreText.hasHTMLTags instead.
*/
hasHTMLTags(text: string): boolean {
- return /<[a-z][\s\S]*>/i.test(text);
+ return CoreText.hasHTMLTags(text);
}
/**
@@ -588,17 +303,11 @@ export class CoreTextUtilsProvider {
* @param text Full text.
* @param searchText Text to search and highlight.
* @returns Highlighted text.
+ *
+ * @deprecated since 4.5. Use CoreText.highlightText instead.
*/
highlightText(text: string, searchText: string): string {
- if (!text || typeof text != 'string') {
- return '';
- } else if (!searchText) {
- return text;
- }
-
- const regex = new RegExp('(' + searchText + ')', 'gi');
-
- return text.replace(regex, '$1');
+ return CoreText.highlightText(text, searchText);
}
/**
@@ -606,15 +315,11 @@ export class CoreTextUtilsProvider {
*
* @param content HTML content.
* @returns True if the string does not contain actual content: text, images, etc.
+ *
+ * @deprecated since 4.5. Use CoreDom.htmlIsBlank instead.
*/
htmlIsBlank(content: string): boolean {
- if (!content) {
- return true;
- }
-
- this.template.innerHTML = content;
-
- return !CoreDom.elementHasContent(this.template.content);
+ return CoreDom.htmlIsBlank(content);
}
/**
@@ -623,15 +328,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to check.
* @returns True if has Unicode chars, false otherwise.
+ *
+ * @deprecated since 4.5. Use CoreText.hasUnicode instead.
*/
hasUnicode(text: string): boolean {
- for (let x = 0; x < text.length; x++) {
- if (text.charCodeAt(x) > 55295) {
- return true;
- }
- }
-
- return false;
+ return CoreText.hasUnicode(text);
}
/**
@@ -639,23 +340,11 @@ export class CoreTextUtilsProvider {
*
* @param data Object to be checked.
* @returns If the data has any long Unicode char on it.
+ *
+ * @deprecated since 4.5. Use CoreText.hasUnicodeData instead.
*/
hasUnicodeData(data: Record): boolean {
- for (const el in data) {
- if (typeof data[el] == 'object') {
- if (this.hasUnicodeData(data[el] as Record)) {
- return true;
- }
-
- continue;
- }
-
- if (typeof data[el] == 'string' && this.hasUnicode(data[el] as string)) {
- return true;
- }
- }
-
- return false;
+ return CoreText.hasUnicodeData(data);
}
/**
@@ -664,21 +353,11 @@ export class CoreTextUtilsProvider {
* @param text Text to match against.
* @param pattern Glob pattern.
* @returns Whether the pattern matches.
+ *
+ * @deprecated since 4.5. Use CoreText.matchesGlob instead.
*/
matchesGlob(text: string, pattern: string): boolean {
- pattern = pattern
- .replace(/\*\*/g, '%RECURSIVE_MATCH%')
- .replace(/\*/g, '%LOCAL_MATCH%')
- .replace(/\?/g, '%CHARACTER_MATCH%');
-
- pattern = this.escapeForRegex(pattern);
-
- pattern = pattern
- .replace(/%RECURSIVE_MATCH%/g, '.*')
- .replace(/%LOCAL_MATCH%/g, '[^/]*')
- .replace(/%CHARACTER_MATCH%/g, '[^/]');
-
- return new RegExp(`^${pattern}$`).test(text);
+ return CoreText.matchesGlob(text, pattern);
}
/**
@@ -688,23 +367,11 @@ export class CoreTextUtilsProvider {
* @param defaultValue Default value to return if the parse fails. Defaults to the original value.
* @param logErrorFn An error to call with the exception to log the error. If not supplied, no error.
* @returns JSON parsed as object or what it gets.
+ *
+ * @deprecated since 4.5. Use CoreText.parseJSON instead.
*/
parseJSON(json: string, defaultValue?: T, logErrorFn?: (error?: Error) => void): T {
- try {
- return JSON.parse(json);
- } catch (error) {
- // Error, log the error if needed.
- if (logErrorFn) {
- logErrorFn(error);
- }
- }
-
- // Error parsing, return the default value or the original value.
- if (defaultValue !== undefined) {
- return defaultValue;
- }
-
- throw new CoreError('JSON cannot be parsed and not default value has been provided') ;
+ return CoreText.parseJSON(json, defaultValue, logErrorFn);
}
/**
@@ -712,13 +379,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to treat.
* @returns Treated text.
+ *
+ * @deprecated since 4.5. Use CoreText.removeSpecialCharactersForFiles instead.
*/
removeSpecialCharactersForFiles(text: string): string {
- if (!text || typeof text != 'string') {
- return '';
- }
-
- return text.replace(/[#:/?\\]+/g, '_');
+ return CoreText.removeSpecialCharactersForFiles(text);
}
/**
@@ -728,19 +393,11 @@ export class CoreTextUtilsProvider {
* @param replacements Argument values.
* @param encoding Encoding to use in values.
* @returns Treated text.
+ *
+ * @deprecated since 4.5. Use CoreText.replaceArguments instead.
*/
replaceArguments(text: string, replacements: Record = {}, encoding?: 'uri'): string {
- let match: RegExpMatchArray | null = null;
-
- while ((match = text.match(/\{\{([^}]+)\}\}/))) {
- const argument = match[1].trim();
- const value = replacements[argument] ?? '';
- const encodedValue = encoding ? encodeURIComponent(value) : value;
-
- text = text.replace(`{{${argument}}}`, encodedValue);
- }
-
- return text;
+ return CoreText.replaceArguments(text, replacements, encoding);
}
/**
@@ -749,13 +406,11 @@ export class CoreTextUtilsProvider {
* @param text The text to be treated.
* @param newValue Text to use instead of new lines.
* @returns Treated text.
+ *
+ * @deprecated since 4.5. Use CoreText.replaceNewLines instead.
*/
replaceNewLines(text: string, newValue: string): string {
- if (!text || typeof text != 'string') {
- return '';
- }
-
- return text.replace(/(?:\r\n|\r|\n)/g, newValue);
+ return CoreText.replaceNewLines(text, newValue);
}
/**
@@ -765,57 +420,15 @@ export class CoreTextUtilsProvider {
* @param text Text to treat, including draftfile URLs.
* @param files List of files of the area, using pluginfile URLs.
* @returns Treated text and map with the replacements.
+ *
+ * @deprecated since 4.5. Use CoreFileHelper.replaceDraftfileUrls instead.
*/
replaceDraftfileUrls(
siteUrl: string,
text: string,
files: CoreWSFile[],
): { text: string; replaceMap?: {[url: string]: string} } {
-
- if (!text || !files || !files.length) {
- return { text };
- }
-
- const draftfileUrl = CorePath.concatenatePaths(siteUrl, 'draftfile.php');
- const matches = text.match(new RegExp(this.escapeForRegex(draftfileUrl) + '[^\'" ]+', 'ig'));
-
- if (!matches || !matches.length) {
- return { text };
- }
-
- // Index the pluginfile URLs by file name.
- const pluginfileMap: {[name: string]: string} = {};
- files.forEach((file) => {
- if (!file.filename) {
- return;
- }
- pluginfileMap[file.filename] = CoreFileHelper.getFileUrl(file);
- });
-
- // Replace each draftfile with the corresponding pluginfile URL.
- const replaceMap: {[url: string]: string} = {};
- matches.forEach((url) => {
- if (replaceMap[url]) {
- // URL already treated, same file embedded more than once.
- return;
- }
-
- // Get the filename from the URL.
- let filename = url.substring(url.lastIndexOf('/') + 1);
- if (filename.indexOf('?') != -1) {
- filename = filename.substring(0, filename.indexOf('?'));
- }
-
- if (pluginfileMap[filename]) {
- replaceMap[url] = pluginfileMap[filename];
- text = text.replace(new RegExp(this.escapeForRegex(url), 'g'), pluginfileMap[filename]);
- }
- });
-
- return {
- text,
- replaceMap,
- };
+ return CoreFileHelper.replaceDraftfileUrls(siteUrl, text, files);
}
/**
@@ -824,16 +437,11 @@ export class CoreTextUtilsProvider {
* @param text to treat.
* @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute.
* @returns Treated text.
+ *
+ * @deprecated since 4.5. Use CoreFileHelper.replacePluginfileUrls instead.
*/
replacePluginfileUrls(text: string, files: CoreWSFile[]): string {
- if (text && typeof text == 'string') {
- const fileURL = this.getTextPluginfileUrl(files);
- if (fileURL) {
- return text.replace(/@@PLUGINFILE@@/g, fileURL);
- }
- }
-
- return text;
+ return CoreFileHelper.replacePluginfileUrls(text, files);
}
/**
@@ -844,33 +452,11 @@ export class CoreTextUtilsProvider {
* @param originalText Original text.
* @param files List of files to search and replace.
* @returns Treated text.
+ *
+ * @deprecated since 4.5. Use CoreFileHelper.restoreDraftfileUrls instead.
*/
restoreDraftfileUrls(siteUrl: string, treatedText: string, originalText: string, files: CoreWSFile[]): string {
- if (!treatedText || !files || !files.length) {
- return treatedText;
- }
-
- const draftfileUrl = CorePath.concatenatePaths(siteUrl, 'draftfile.php');
- const draftfileUrlRegexPrefix = this.escapeForRegex(draftfileUrl) + '/[^/]+/[^/]+/[^/]+/[^/]+/';
-
- files.forEach((file) => {
- if (!file.filename) {
- return;
- }
-
- // Search the draftfile URL in the original text.
- const matches = originalText.match(
- new RegExp(draftfileUrlRegexPrefix + this.escapeForRegex(file.filename) + '[^\'" ]*', 'i'),
- );
-
- if (!matches || !matches[0]) {
- return; // Original URL not found, skip.
- }
-
- treatedText = treatedText.replace(new RegExp(this.escapeForRegex(CoreFileHelper.getFileUrl(file)), 'g'), matches[0]);
- });
-
- return treatedText;
+ return CoreFileHelper.restoreDraftfileUrls(siteUrl, treatedText, originalText, files);
}
/**
@@ -879,16 +465,11 @@ export class CoreTextUtilsProvider {
* @param text Text to treat.
* @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute.
* @returns Treated text.
+ *
+ * @deprecated since 4.5. Use CoreFileHelper.restorePluginfileUrls instead.
*/
restorePluginfileUrls(text: string, files: CoreWSFile[]): string {
- if (text && typeof text == 'string') {
- const fileURL = this.getTextPluginfileUrl(files);
- if (fileURL) {
- return text.replace(new RegExp(this.escapeForRegex(fileURL), 'g'), '@@PLUGINFILE@@');
- }
- }
-
- return text;
+ return CoreFileHelper.restorePluginfileUrls(text, files);
}
/**
@@ -900,11 +481,11 @@ export class CoreTextUtilsProvider {
* @param num Number to round.
* @param decimals Number of decimals. By default, 2.
* @returns Rounded number.
+ *
+ * @deprecated since 4.5. Use CoreText.roundToDecimals instead.
*/
roundToDecimals(num: number, decimals: number = 2): number {
- const multiplier = Math.pow(10, decimals);
-
- return Math.round(num * multiplier) / multiplier;
+ return CoreText.roundToDecimals(num, decimals);
}
/**
@@ -915,13 +496,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to treat.
* @returns Treated text.
+ *
+ * @deprecated since 4.5. Use CoreText.s instead.
*/
s(text: string): string {
- if (!text) {
- return '';
- }
-
- return this.escapeHTML(text).replace(/&#(\d+|x[0-9a-f]+);/i, '$1;');
+ return CoreText.s(text);
}
/**
@@ -930,20 +509,11 @@ export class CoreTextUtilsProvider {
* @param text The text to be shortened.
* @param length The desired length.
* @returns Shortened text.
+ *
+ * @deprecated since 4.5. Use CoreText.shortenText instead.
*/
shortenText(text: string, length: number): string {
- if (text.length > length) {
- text = text.substring(0, length);
-
- // Now, truncate at the last word boundary (if exists).
- const lastWordPos = text.lastIndexOf(' ');
- if (lastWordPos > 0) {
- text = text.substring(0, lastWordPos);
- }
- text += '…';
- }
-
- return text;
+ return CoreText.shortenText(text, length);
}
/**
@@ -952,16 +522,11 @@ export class CoreTextUtilsProvider {
*
* @param text Text to check.
* @returns Without the Unicode chars.
+ *
+ * @deprecated since 4.5. Use CoreText.stripUnicode instead.
*/
stripUnicode(text: string): string {
- let stripped = '';
- for (let x = 0; x < text.length; x++) {
- if (text.charCodeAt(x) <= 55295) {
- stripped += text.charAt(x);
- }
- }
-
- return stripped;
+ return CoreText.stripUnicode(text);
}
/**
@@ -973,9 +538,11 @@ export class CoreTextUtilsProvider {
* @param length Length of the portion of string which is to be replaced. If negative, it represents the number of characters
* from the end of string at which to stop replacing. If not provided, replace until the end of the string.
* @returns Treated string.
+ *
+ * @deprecated since 4.5. Use CoreText.substrReplace instead.
*/
substrReplace(str: string, replace: string, start: number, length?: number): string {
- return Locutus.substrReplace(str, replace, start, length);
+ return CoreText.substrReplace(str, replace, start, length);
}
/**
@@ -983,18 +550,10 @@ export class CoreTextUtilsProvider {
*
* @param features List of disabled features.
* @returns Treated list.
+ *
+ * @deprecated since 4.5. Shoudn't be used since disabled features are not treated by this function anymore.
*/
treatDisabledFeatures(features: string): string {
- if (!features) {
- return '';
- }
-
- for (let i = 0; i < this.DISABLED_FEATURES_COMPAT_REGEXPS.length; i++) {
- const entry = this.DISABLED_FEATURES_COMPAT_REGEXPS[i];
-
- features = features.replace(entry.old, entry.new);
- }
-
return features;
}
@@ -1004,12 +563,11 @@ export class CoreTextUtilsProvider {
* @param text Text to treat.
* @param character Character to remove.
* @returns Treated text.
+ *
+ * @deprecated since 4.5. Use CoreText.trimCharacter instead.
*/
trimCharacter(text: string, character: string): string {
- const escaped = this.escapeForRegex(character);
- const regExp = new RegExp(`^${escaped}+|${escaped}+$`, 'g');
-
- return text.replace(regExp, '');
+ return CoreText.trimCharacter(text, character);
}
/**
@@ -1017,13 +575,11 @@ export class CoreTextUtilsProvider {
*
* @param num Number to convert.
* @returns Number with leading zeros.
+ *
+ * @deprecated since 4.5. Use CoreText.twoDigits instead.
*/
twoDigits(num: string | number): string {
- if (Number(num) < 10) {
- return '0' + num;
- } else {
- return '' + num; // Convert to string for coherence.
- }
+ return CoreText.twoDigits(num);
}
/**
@@ -1042,9 +598,11 @@ export class CoreTextUtilsProvider {
*
* @param data String to unserialize.
* @returns Unserialized data.
+ *
+ * @deprecated since 4.5. Use CoreText.unserialize instead.
*/
unserialize(data: string): T {
- return Locutus.unserialize(data);
+ return CoreText.unserialize(data);
}
/**
@@ -1061,24 +619,5 @@ export class CoreTextUtilsProvider {
}
}
+// eslint-disable-next-line deprecation/deprecation
export const CoreTextUtils = makeSingleton(CoreTextUtilsProvider);
-
-/**
- * Options for viewText.
- *
- * @deprecated since 4.5. Use CoreViewerTextOptions instead.
- */
-export type CoreTextUtilsViewTextOptions = CoreViewerTextOptions;
-
-/**
- * Define text formatting types.
- */
-export enum CoreTextFormat {
- FORMAT_MOODLE = 0, // Does all sorts of transformations and filtering.
- FORMAT_HTML = 1, // Plain HTML (with some tags stripped). Use it by default.
- FORMAT_PLAIN = 2, // Plain text (even tags are printed in full).
- // FORMAT_WIKI is deprecated since 2005...
- FORMAT_MARKDOWN = 4, // Markdown-formatted text http://daringfireball.net/projects/markdown/
-}
-
-export const defaultTextFormat = CoreTextFormat.FORMAT_HTML;
diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts
index 3bb3ba823..05fe91cec 100644
--- a/src/core/services/utils/utils.ts
+++ b/src/core/services/utils/utils.ts
@@ -20,7 +20,6 @@ import { CoreFile } from '@services/file';
import { CoreLang, CoreLangFormat } from '@services/lang';
import { CoreWS } from '@services/ws';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
-import { CoreTextUtils } from '@services/utils/text';
import { makeSingleton, InAppBrowser, FileOpener, WebIntent, Translate, NgZone } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreFileEntry } from '@services/file-helper';
@@ -38,6 +37,7 @@ import { CoreArray } from '@singletons/array';
import { CoreText } from '@singletons/text';
import { CoreWait, CoreWaitOptions } from '@singletons/wait';
import { CoreQRScan } from '@services/qrscan';
+import { CoreErrorHelper } from '@services/error-helper';
export type TreeNode = T & { children: TreeNode[] };
@@ -65,7 +65,7 @@ export class CoreUtilsProvider {
* @returns New error message.
*/
addDataNotDownloadedError(error: Error | string, defaultError?: string): string {
- const errorMessage = CoreTextUtils.getErrorMessageFromError(error) || defaultError || '';
+ const errorMessage = CoreErrorHelper.getErrorMessageFromError(error) || defaultError || '';
if (this.isWebServiceError(error)) {
return errorMessage;
diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts
index 004da122c..3b9ce5954 100644
--- a/src/core/services/ws.ts
+++ b/src/core/services/ws.ts
@@ -24,7 +24,7 @@ import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http
import { CoreNetwork } from '@services/network';
import { CoreFile, CoreFileFormat } from '@services/file';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
-import { CoreTextErrorObject, CoreTextUtils } from '@services/utils/text';
+import { CoreText } from '@singletons/text';
import { CoreConstants } from '@/core/constants';
import { CoreError } from '@classes/errors/error';
import { CoreInterceptor } from '@classes/interceptor';
@@ -43,6 +43,8 @@ import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest
import { CoreSites } from '@services/sites';
import { CoreLang, CoreLangFormat } from './lang';
import { CoreErrorLogs } from '@singletons/error-logs';
+import { CoreErrorHelper, CoreErrorObject } from './error-helper';
+import { CoreDom } from '@singletons/dom';
/**
* This service allows performing WS calls and download/upload files.
@@ -174,9 +176,9 @@ export class CoreWSProvider {
if (value == null) {
return null;
}
- } else if (typeof value == 'string') {
+ } else if (typeof value === 'string') {
if (stripUnicode) {
- const stripped = CoreTextUtils.stripUnicode(value);
+ const stripped = CoreText.stripUnicode(value);
if (stripped != value && stripped.trim().length == 0) {
return null;
}
@@ -532,45 +534,45 @@ export class CoreWSProvider {
options.debug = {
code: 'invalidcertificate',
details: Translate.instant('core.certificaterror', {
- details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Invalid certificate',
+ details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Invalid certificate',
}),
};
break;
case NativeHttp.ErrorCode.SERVER_NOT_FOUND:
options.debug = {
code: 'servernotfound',
- details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Server could not be found',
+ details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Server could not be found',
};
break;
case NativeHttp.ErrorCode.TIMEOUT:
options.debug = {
code: 'requesttimeout',
- details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request timed out',
+ details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Request timed out',
};
break;
case NativeHttp.ErrorCode.UNSUPPORTED_URL:
options.debug = {
code: 'unsupportedurl',
- details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Url not supported',
+ details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Url not supported',
};
break;
case NativeHttp.ErrorCode.NOT_CONNECTED:
options.debug = {
code: 'connectionerror',
- details: CoreTextUtils.getErrorMessageFromError(data.error)
+ details: CoreErrorHelper.getErrorMessageFromError(data.error)
?? 'Connection error, is network available?',
};
break;
case NativeHttp.ErrorCode.ABORTED:
options.debug = {
code: 'requestaborted',
- details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request aborted',
+ details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Request aborted',
};
break;
case NativeHttp.ErrorCode.POST_PROCESSING_FAILED:
options.debug = {
code: 'requestprocessingfailed',
- details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request processing failed',
+ details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Request processing failed',
};
break;
}
@@ -587,7 +589,7 @@ export class CoreWSProvider {
};
break;
default: {
- const details = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Unknown error';
+ const details = CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Unknown error';
options.debug = {
code: 'serverconnectionajax',
@@ -836,7 +838,7 @@ export class CoreWSProvider {
debug: {
code: 'invalidcertificate',
details: Translate.instant('core.certificaterror', {
- details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error',
+ details: CoreErrorHelper.getErrorMessageFromError(error) ?? 'Unknown error',
}),
},
});
@@ -845,7 +847,7 @@ export class CoreWSProvider {
}
throw new CoreError(Translate.instant('core.serverconnection', {
- details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error',
+ details: CoreErrorHelper.getErrorMessageFromError(error) ?? 'Unknown error',
}));
}).catch(err => {
CoreErrorLogs.addErrorLog({
@@ -965,7 +967,7 @@ export class CoreWSProvider {
}
// Treat response.
- data = CoreTextUtils.parseJSON(data);
+ data = CoreText.parseJSON(data);
// Some moodle web services return null.
// If the responseExpected value is set then so long as no data is returned, we create a blank object.
@@ -1057,7 +1059,7 @@ export class CoreWSProvider {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const data = CoreTextUtils.parseJSON(
+ const data = CoreText.parseJSON(
success.response,
null,
error => this.logger.error('Error parsing response from upload', success.response, error),
@@ -1119,12 +1121,12 @@ export class CoreWSProvider {
* @param status Status code (if any).
* @returns CoreHttpError.
*/
- protected createHttpError(error: CoreTextErrorObject, status: number): CoreHttpError {
- const message = CoreTextUtils.buildSeveralParagraphsMessage([
+ protected createHttpError(error: CoreErrorObject, status: number): CoreHttpError {
+ const message = CoreErrorHelper.buildSeveralParagraphsMessage([
CoreSites.isLoggedIn()
? Translate.instant('core.siteunavailablehelp', { site: CoreSites.getCurrentSite()?.siteUrl })
: Translate.instant('core.sitenotfoundhelp'),
- CoreTextUtils.getHTMLBodyContent(CoreTextUtils.getErrorMessageFromError(error) || ''),
+ CoreDom.getHTMLBodyContent(CoreErrorHelper.getErrorMessageFromError(error) || ''),
]);
return new CoreHttpError(message, status);
diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts
index 8f962cc87..a8050115e 100644
--- a/src/core/singletons/dom.ts
+++ b/src/core/singletons/dom.ts
@@ -18,6 +18,9 @@ import { CoreEventObserver } from '@singletons/events';
import { CorePlatform } from '@services/platform';
import { CoreWait } from './wait';
+// A template element to convert HTML to element.
+export const CoreTemplateElement: HTMLTemplateElement = document.createElement('template');
+
/**
* Singleton with helper functions for dom.
*/
@@ -74,6 +77,19 @@ export class CoreDom {
).length > 0;
}
+ /**
+ * Given some HTML code, return the HTML code inside tags. If there are no body tags, return the whole HTML.
+ *
+ * @param html HTML text.
+ * @returns Body HTML.
+ */
+ static getHTMLBodyContent(html: string): string {
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ const bodyContent = doc.body.innerHTML;
+
+ return bodyContent ?? html;
+ }
+
/**
* Retrieve the position of a element relative to another element.
*
@@ -93,6 +109,22 @@ export class CoreDom {
};
}
+ /**
+ * Check if HTML content is blank.
+ *
+ * @param content HTML content.
+ * @returns True if the string does not contain actual content: text, images, etc.
+ */
+ static htmlIsBlank(content: string): boolean {
+ if (!content) {
+ return true;
+ }
+
+ CoreTemplateElement.innerHTML = content;
+
+ return !CoreDom.elementHasContent(CoreTemplateElement.content);
+ }
+
/**
* Check whether an element has been added to the DOM.
*
diff --git a/src/core/singletons/tests/text.test.ts b/src/core/singletons/tests/text.test.ts
index b50f0db7d..f66221be5 100644
--- a/src/core/singletons/tests/text.test.ts
+++ b/src/core/singletons/tests/text.test.ts
@@ -16,6 +16,20 @@ import { CoreText } from '@singletons/text';
describe('CoreText singleton', () => {
+ it('adds ending slashes', () => {
+ const originalUrl = 'https://moodle.org';
+ const url = CoreText.addEndingSlash(originalUrl);
+
+ expect(url).toEqual('https://moodle.org/');
+ });
+
+ it('doesn\'t add duplicated ending slashes', () => {
+ const originalUrl = 'https://moodle.org/';
+ const url = CoreText.addEndingSlash(originalUrl);
+
+ expect(url).toEqual('https://moodle.org/');
+ });
+
it('adds a starting slash if needed', () => {
expect(CoreText.addStartingSlash('')).toEqual('/');
expect(CoreText.addStartingSlash('foo')).toEqual('/foo');
@@ -36,4 +50,71 @@ describe('CoreText singleton', () => {
expect(CoreText.removeStartingSlash('//foo')).toEqual('/foo');
});
+ it('matches glob patterns', () => {
+ expect(CoreText.matchesGlob('/foo/bar', '/foo/bar')).toBe(true);
+ expect(CoreText.matchesGlob('/foo/bar', '/foo/bar/')).toBe(false);
+ expect(CoreText.matchesGlob('/foo', '/foo/*')).toBe(false);
+ expect(CoreText.matchesGlob('/foo/', '/foo/*')).toBe(true);
+ expect(CoreText.matchesGlob('/foo/bar', '/foo/*')).toBe(true);
+ expect(CoreText.matchesGlob('/foo/bar/', '/foo/*')).toBe(false);
+ expect(CoreText.matchesGlob('/foo/bar/baz', '/foo/*')).toBe(false);
+ expect(CoreText.matchesGlob('/foo/bar/baz', '/foo/**')).toBe(true);
+ expect(CoreText.matchesGlob('/foo/bar/baz/', '/foo/**')).toBe(true);
+ expect(CoreText.matchesGlob('/foo/bar/baz', '**/baz')).toBe(true);
+ expect(CoreText.matchesGlob('/foo/bar/baz', '**/bar')).toBe(false);
+ expect(CoreText.matchesGlob('/foo/bar/baz', '/foo/ba?/ba?')).toBe(true);
+ });
+
+ it('replaces arguments', () => {
+ // Arrange
+ const url = 'http://campus.edu?device={{device}}&version={{version}}';
+ const replacements = {
+ device: 'iPhone or iPad',
+ version: '1.2.3',
+ };
+
+ // Act
+ const replaced = CoreText.replaceArguments(url, replacements, 'uri');
+
+ // Assert
+ expect(replaced).toEqual('http://campus.edu?device=iPhone%20or%20iPad&version=1.2.3');
+ });
+
+ it('counts words', () => {
+ expect(CoreText.countWords('')).toEqual(0);
+ expect(CoreText.countWords('one two three four')).toEqual(4);
+ expect(CoreText.countWords('a\'b')).toEqual(1);
+ expect(CoreText.countWords('1+1=2')).toEqual(1);
+ expect(CoreText.countWords(' one-sided ')).toEqual(1);
+ expect(CoreText.countWords('one two')).toEqual(2);
+ expect(CoreText.countWords('email@example.com')).toEqual(1);
+ expect(CoreText.countWords('first\\part second/part')).toEqual(2);
+ expect(CoreText.countWords('
one two three four
')).toEqual(4);
+ expect(CoreText.countWords('
one two three four
')).toEqual(4);
+ expect(CoreText.countWords('
one two three four
')).toEqual(4);
+ expect(CoreText.countWords(' one ... three ')).toEqual(3);
+ expect(CoreText.countWords('just...one')).toEqual(1);
+ expect(CoreText.countWords(' one & three ')).toEqual(3);
+ expect(CoreText.countWords('just&one')).toEqual(1);
+ expect(CoreText.countWords('em—dash')).toEqual(2);
+ expect(CoreText.countWords('en–dash')).toEqual(2);
+ expect(CoreText.countWords('1³ £2 €3.45 $6,789')).toEqual(4);
+ expect(CoreText.countWords('ブルース カンベッル')).toEqual(2);
+ expect(CoreText.countWords('
one two
three four
')).toEqual(4);
+ expect(CoreText.countWords('
one two
three four
')).toEqual(4);
+ expect(CoreText.countWords('
one
two
three
four.
')).toEqual(4);
+ expect(CoreText.countWords('
emphasis.
')).toEqual(1);
+ expect(CoreText.countWords('
emphasis.
')).toEqual(1);
+ expect(CoreText.countWords('
emphasis.
')).toEqual(1);
+ expect(CoreText.countWords('
emphasis.
')).toEqual(1);
+ expect(CoreText.countWords('one\ntwo')).toEqual(2);
+ expect(CoreText.countWords('one\rtwo')).toEqual(2);
+ expect(CoreText.countWords('one\ttwo')).toEqual(2);
+ expect(CoreText.countWords('one\vtwo')).toEqual(2);
+ expect(CoreText.countWords('one\ftwo')).toEqual(2);
+ expect(CoreText.countWords('SO42-')).toEqual(1);
+ expect(CoreText.countWords('4+4=8 i.e. O(1) a,b,c,d I’m black&blue_really')).toEqual(6);
+ expect(CoreText.countWords('ab')).toEqual(1);
+ });
+
});
diff --git a/src/core/singletons/tests/url.test.ts b/src/core/singletons/tests/url.test.ts
index 0e4980e05..7903de55f 100644
--- a/src/core/singletons/tests/url.test.ts
+++ b/src/core/singletons/tests/url.test.ts
@@ -12,12 +12,63 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { mock } from '@/testing/utils';
+import { mock, mockSingleton } from '@/testing/utils';
import { CoreSite } from '@classes/sites/site';
import { CoreUrl, CoreUrlPartNames } from '@singletons/url';
+import { CorePlatform } from '@services/platform';
+import { DomSanitizer } from '@singletons';
describe('CoreUrl singleton', () => {
+ const config = { platform: 'android' };
+
+ beforeEach(() => {
+ mockSingleton(CorePlatform, [], { isAndroid: () => config.platform === 'android' });
+ mockSingleton(DomSanitizer, [], { bypassSecurityTrustUrl: url => url });
+ });
+
+ it('builds address URL for Android platforms', () => {
+ // Arrange
+ const address = 'Moodle Spain HQ';
+
+ config.platform = 'android';
+
+ // Act
+ const url = CoreUrl.buildAddressURL(address);
+
+ // Assert
+ expect(url).toEqual('geo:0,0?q=Moodle%20Spain%20HQ');
+
+ expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled();
+ expect(CorePlatform.isAndroid).toHaveBeenCalled();
+ });
+
+ it('builds address URL for non-Android platforms', () => {
+ // Arrange
+ const address = 'Moodle Spain HQ';
+
+ config.platform = 'ios';
+
+ // Act
+ const url = CoreUrl.buildAddressURL(address);
+
+ // Assert
+ expect(url).toEqual('http://maps.google.com?q=Moodle%20Spain%20HQ');
+
+ expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled();
+ expect(CorePlatform.isAndroid).toHaveBeenCalled();
+ });
+
+ it('doesn\'t build address if it\'s already a URL', () => {
+ const address = 'https://moodle.org';
+
+ const url = CoreUrl.buildAddressURL(address);
+
+ expect(url).toEqual(address);
+
+ expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled();
+ });
+
it('adds www if missing', () => {
const originalUrl = 'https://moodle.org';
const url = CoreUrl.addOrRemoveWWW(originalUrl);
diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts
index 0a60c5a48..58c066655 100644
--- a/src/core/singletons/text.ts
+++ b/src/core/singletons/text.ts
@@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Clipboard } from '@singletons';
+import { Clipboard, Translate } from '@singletons';
import { CoreToasts } from '@services/toasts';
+import { Locutus } from './locutus';
+import { CoreError } from '@classes/errors/error';
+import { CoreTemplateElement } from './dom';
/**
* Singleton with helper functions for text manipulation.
@@ -25,6 +28,24 @@ export class CoreText {
// Nothing to do.
}
+ /**
+ * Add ending slash from a path or URL.
+ *
+ * @param text Text to treat.
+ * @returns Treated text.
+ */
+ static addEndingSlash(text: string): string {
+ if (!text) {
+ return '';
+ }
+
+ if (text.slice(-1) != '/') {
+ return text + '/';
+ }
+
+ return text;
+ }
+
/**
* Add starting slash to a string if needed.
*
@@ -39,6 +60,405 @@ export class CoreText {
return '/' + text;
}
+ /**
+ * Given a list of sentences, build a message with all of them wrapped in
.
+ *
+ * @param messages Messages to show.
+ * @returns Message with all the messages.
+ */
+ static buildMessage(messages: string[]): string {
+ let result = '';
+
+ messages.forEach((message) => {
+ if (message) {
+ result += `
${message}
`;
+ }
+ });
+
+ return result;
+ }
+
+ /**
+ * Convert size in bytes into human readable format
+ *
+ * @param bytes Number of bytes to convert.
+ * @param precision Number of digits after the decimal separator.
+ * @returns Size in human readable format.
+ */
+ static bytesToSize(bytes: number, precision: number = 2): string {
+ if (bytes === undefined || bytes === null || bytes < 0) {
+ return Translate.instant('core.notapplicable');
+ }
+
+ if (precision < 0) {
+ precision = 2;
+ }
+
+ const keys = ['core.sizeb', 'core.sizekb', 'core.sizemb', 'core.sizegb', 'core.sizetb'];
+ const units = Translate.instant(keys);
+ let pos = 0;
+
+ if (bytes >= 1024) {
+ while (bytes >= 1024) {
+ pos++;
+ bytes = bytes / 1024;
+ }
+ // Round to "precision" decimals if needed.
+ bytes = Number(Math.round(parseFloat(bytes + 'e+' + precision)) + 'e-' + precision);
+ }
+
+ return Translate.instant('core.humanreadablesize', { size: bytes, unit: units[keys[pos]] });
+ }
+
+ /**
+ * Copies a text to clipboard and shows a toast message.
+ *
+ * @param text Text to be copied
+ */
+ static async copyToClipboard(text: string): Promise {
+ try {
+ await Clipboard.copy(text);
+ } catch {
+ // Use HTML Copy command.
+ const virtualInput = document.createElement('textarea');
+ virtualInput.innerHTML = text;
+ virtualInput.select();
+ virtualInput.setSelectionRange(0, 99999);
+ document.execCommand('copy'); // eslint-disable-line deprecation/deprecation
+ }
+
+ // Show toast using ionicLoading.
+ CoreToasts.show({
+ message: 'core.copiedtoclipboard',
+ translateMessage: true,
+ });
+ }
+
+ /**
+ * Count words in a text.
+ * This function is based on Moodle's count_words.
+ *
+ * @param text Text to count.
+ * @returns Number of words.
+ */
+ static countWords(text?: string | null): number {
+ if (!text || typeof text != 'string') {
+ return 0;
+ }
+
+ // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
+ // Also, br is a special case because it definitely delimits a word, but has no close tag.
+ text = text.replace(/(<\/(?!a>|b>|del>|em>|i>|ins>|s>|small>|span>|strong>|sub>|sup>|u>)\w+>| | )/ig, '$1 ');
+
+ // Now remove HTML tags.
+ text = text.replace(/(<([^>]+)>)/ig, '');
+ // Decode HTML entities.
+ text = CoreText.decodeHTMLEntities(text);
+
+ // Now, the word count is the number of blocks of characters separated
+ // by any sort of space. That seems to be the definition used by all other systems.
+ // To be precise about what is considered to separate words:
+ // * Anything that Unicode considers a 'Separator'
+ // * Anything that Unicode considers a 'Control character'
+ // * An em- or en- dash.
+ let words: string[];
+ try {
+ words = text.split(/[\p{Z}\p{Cc}—–]+/u);
+ } catch {
+ // Unicode-aware flag not supported.
+ words = text.split(/\s+/);
+ }
+
+ // Filter empty words.
+ return words.filter(word => word).length;
+ }
+
+ /**
+ * Clean HTML tags.
+ *
+ * @param text The text to be cleaned.
+ * @param options Processing options.
+ * @param options.singleLine True if new lines should be removed (all the text in a single line).
+ * @param options.trim True if text should be trimmed.
+ * @returns Clean text.
+ */
+ static cleanTags(text: string | undefined, options: { singleLine?: boolean; trim?: boolean } = {}): string {
+ if (!text) {
+ return '';
+ }
+
+ // 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.
+ text = CoreText.convertToElement(text).textContent || '';
+ // Trim text
+ text = options.trim ? text.trim() : text;
+ // Recover or remove new lines.
+ text = CoreText.replaceNewLines(text, options.singleLine ? ' ' : ' ');
+
+ return text;
+ }
+
+ /**
+ * 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 html Text to convert.
+ * @returns Element.
+ */
+ protected static convertToElement(html: string): HTMLElement {
+ // Add a div to hold the content, that's the element that will be returned.
+ CoreTemplateElement.innerHTML = '
' + html + '
';
+
+ return CoreTemplateElement.content.children[0];
+ }
+
+ /**
+ * Decode an escaped HTML text. This implementation is based on PHP's htmlspecialchars_decode.
+ *
+ * @param text Text to decode.
+ * @returns Decoded text.
+ */
+ static decodeHTML(text: string | number): string {
+ if (text === undefined || text === null || (typeof text === 'number' && isNaN(text))) {
+ return '';
+ } else if (typeof text != 'string') {
+ return '' + text;
+ }
+
+ return text
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, '\'')
+ .replace(/ /g, ' ');
+ }
+
+ /**
+ * Decode HTML entities in a text. Equivalent to PHP html_entity_decode.
+ *
+ * @param text Text to decode.
+ * @returns Decoded text.
+ */
+ static decodeHTMLEntities(text: string): string {
+ if (text) {
+ text = CoreText.convertToElement(text).textContent || '';
+ }
+
+ return text;
+ }
+
+ /**
+ * Escapes some characters in a string to be used as a regular expression.
+ *
+ * @param text Text to escape.
+ * @returns Escaped text.
+ */
+ static escapeForRegex(text: string): string {
+ if (!text || typeof text !== 'string') {
+ return '';
+ }
+
+ return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+ }
+
+ /**
+ * Escape an HTML text. This implementation is based on PHP's htmlspecialchars.
+ *
+ * @param text Text to escape.
+ * @param doubleEncode If false, it will not convert existing html entities. Defaults to true.
+ * @returns Escaped text.
+ */
+ static escapeHTML(text?: string | number | null, doubleEncode = true): string {
+ if (text === undefined || text === null || (typeof text === 'number' && isNaN(text))) {
+ return '';
+ } else if (typeof text !== 'string') {
+ return '' + text;
+ }
+
+ if (doubleEncode) {
+ text = text.replace(/&/g, '&');
+ } else {
+ text = text.replace(/&(?!amp;)(?!lt;)(?!gt;)(?!quot;)(?!#039;)/g, '&');
+ }
+
+ return text
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ /**
+ * Formats a text, in HTML replacing new lines by correct html new lines.
+ *
+ * @param text Text to format.
+ * @returns Formatted text.
+ */
+ static formatHtmlLines(text: string): string {
+ const hasHTMLTags = CoreText.hasHTMLTags(text);
+ if (text.indexOf('
') == -1) {
+ // Wrap the text in
tags.
+ text = '
' + text + '
';
+ }
+
+ if (!hasHTMLTags) {
+ // The text doesn't have HTML, replace new lines for .
+ return CoreText.replaceNewLines(text, ' ');
+ }
+
+ return text;
+ }
+
+ /**
+ * Check if a text contains HTML tags.
+ *
+ * @param text Text to check.
+ * @returns Whether it has HTML tags.
+ */
+ static hasHTMLTags(text: string): boolean {
+ return /<[a-z][\s\S]*>/i.test(text);
+ }
+
+ /**
+ * Check if a text contains Unicode long chars.
+ * Using as threshold Hex value D800
+ *
+ * @param text Text to check.
+ * @returns True if has Unicode chars, false otherwise.
+ */
+ static hasUnicode(text: string): boolean {
+ for (let x = 0; x < text.length; x++) {
+ if (text.charCodeAt(x) > 55295) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if an object has any long Unicode char.
+ *
+ * @param data Object to be checked.
+ * @returns If the data has any long Unicode char on it.
+ */
+ static hasUnicodeData(data: Record): boolean {
+ for (const el in data) {
+ if (typeof data[el] === 'object') {
+ if (CoreText.hasUnicodeData(data[el] as Record)) {
+ return true;
+ }
+
+ continue;
+ }
+
+ if (typeof data[el] === 'string' && CoreText.hasUnicode(data[el] as string)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Highlight all occurrences of a certain text inside another text. It will add some HTML code to highlight it.
+ *
+ * @param text Full text.
+ * @param searchText Text to search and highlight.
+ * @returns Highlighted text.
+ */
+ static highlightText(text: string, searchText: string): string {
+ if (!text || typeof text !== 'string') {
+ return '';
+ } else if (!searchText) {
+ return text;
+ }
+
+ const regex = new RegExp('(' + searchText + ')', 'gi');
+
+ return text.replace(regex, '$1');
+ }
+
+ /**
+ * Check whether the given text matches a glob pattern.
+ *
+ * @param text Text to match against.
+ * @param pattern Glob pattern.
+ * @returns Whether the pattern matches.
+ */
+ static matchesGlob(text: string, pattern: string): boolean {
+ pattern = pattern
+ .replace(/\*\*/g, '%RECURSIVE_MATCH%')
+ .replace(/\*/g, '%LOCAL_MATCH%')
+ .replace(/\?/g, '%CHARACTER_MATCH%');
+
+ pattern = CoreText.escapeForRegex(pattern);
+
+ pattern = pattern
+ .replace(/%RECURSIVE_MATCH%/g, '.*')
+ .replace(/%LOCAL_MATCH%/g, '[^/]*')
+ .replace(/%CHARACTER_MATCH%/g, '[^/]');
+
+ return new RegExp(`^${pattern}$`).test(text);
+ }
+
+ /**
+ * Same as Javascript's JSON.parse, but it will handle errors.
+ *
+ * @param json JSON text.
+ * @param defaultValue Default value to return if the parse fails. Defaults to the original value.
+ * @param logErrorFn An error to call with the exception to log the error. If not supplied, no error.
+ * @returns JSON parsed as object or what it gets.
+ */
+ static parseJSON(json: string, defaultValue?: T, logErrorFn?: (error?: Error) => void): T {
+ try {
+ return JSON.parse(json);
+ } catch (error) {
+ // Error, log the error if needed.
+ if (logErrorFn) {
+ logErrorFn(error);
+ }
+ }
+
+ // Error parsing, return the default value or the original value.
+ if (defaultValue !== undefined) {
+ return defaultValue;
+ }
+
+ throw new CoreError('JSON cannot be parsed and not default value has been provided');
+ }
+
+ /**
+ * Process HTML string.
+ *
+ * @param text HTML string.
+ * @param process Method to process the HTML.
+ * @returns Processed HTML string.
+ */
+ static processHTML(text: string, process: (element: HTMLElement) => unknown): string {
+ const element = CoreText.convertToElement(text);
+
+ process(element);
+
+ return element.innerHTML;
+ }
+
+ /**
+ * Replace all characters that cause problems with files in Android and iOS.
+ *
+ * @param text Text to treat.
+ * @returns Treated text.
+ */
+ static removeSpecialCharactersForFiles(text: string): string {
+ if (!text || typeof text !== 'string') {
+ return '';
+ }
+
+ return text.replace(/[#:/?\\]+/g, '_');
+ }
+
/**
* Remove ending slash from a path or URL.
*
@@ -72,27 +492,155 @@ export class CoreText {
}
/**
- * Copies a text to clipboard and shows a toast message.
+ * Replace {{ARGUMENT}} arguments in the text.
*
- * @param text Text to be copied
+ * @param text Text to treat.
+ * @param replacements Argument values.
+ * @param encoding Encoding to use in values.
+ * @returns Treated text.
*/
- static async copyToClipboard(text: string): Promise {
- try {
- await Clipboard.copy(text);
- } catch {
- // Use HTML Copy command.
- const virtualInput = document.createElement('textarea');
- virtualInput.innerHTML = text;
- virtualInput.select();
- virtualInput.setSelectionRange(0, 99999);
- document.execCommand('copy'); // eslint-disable-line deprecation/deprecation
+ static replaceArguments(text: string, replacements: Record = {}, encoding?: 'uri'): string {
+ let match: RegExpMatchArray | null = null;
+
+ while ((match = text.match(/\{\{([^}]+)\}\}/))) {
+ const argument = match[1].trim();
+ const value = replacements[argument] ?? '';
+ const encodedValue = encoding ? encodeURIComponent(value) : value;
+
+ text = text.replace(`{{${argument}}}`, encodedValue);
}
- // Show toast using ionicLoading.
- CoreToasts.show({
- message: 'core.copiedtoclipboard',
- translateMessage: true,
- });
+ return text;
+ }
+
+ /**
+ * Replace all the new lines on a certain text.
+ *
+ * @param text The text to be treated.
+ * @param newValue Text to use instead of new lines.
+ * @returns Treated text.
+ */
+ static replaceNewLines(text: string, newValue: string): string {
+ if (!text || typeof text !== 'string') {
+ return '';
+ }
+
+ return text.replace(/(?:\r\n|\r|\n)/g, newValue);
+ }
+
+ /**
+ * Rounds a number to use a certain amout of decimals or less.
+ * Difference between this function and float's toFixed:
+ * 7.toFixed(2) -> 7.00
+ * roundToDecimals(7, 2) -> 7
+ *
+ * @param num Number to round.
+ * @param decimals Number of decimals. By default, 2.
+ * @returns Rounded number.
+ */
+ static roundToDecimals(num: number, decimals: number = 2): number {
+ const multiplier = Math.pow(10, decimals);
+
+ return Math.round(num * multiplier) / multiplier;
+ }
+
+ /**
+ * Add quotes to HTML characters.
+ *
+ * Returns text with HTML characters (like "<", ">", etc.) properly quoted.
+ * Based on Moodle's s() function.
+ *
+ * @param text Text to treat.
+ * @returns Treated text.
+ */
+ static s(text: string): string {
+ if (!text) {
+ return '';
+ }
+
+ return CoreText.escapeHTML(text).replace(/&#(\d+|x[0-9a-f]+);/i, '$1;');
+ }
+
+ /**
+ * Shortens a text to length and adds an ellipsis.
+ *
+ * @param text The text to be shortened.
+ * @param length The desired length.
+ * @returns Shortened text.
+ */
+ static shortenText(text: string, length: number): string {
+ if (text.length > length) {
+ text = text.substring(0, length);
+
+ // Now, truncate at the last word boundary (if exists).
+ const lastWordPos = text.lastIndexOf(' ');
+ if (lastWordPos > 0) {
+ text = text.substring(0, lastWordPos);
+ }
+ text += '…';
+ }
+
+ return text;
+ }
+
+ /**
+ * Strip Unicode long char of a given text.
+ * Using as threshold Hex value D800
+ *
+ * @param text Text to check.
+ * @returns Without the Unicode chars.
+ */
+ static stripUnicode(text: string): string {
+ let stripped = '';
+ for (let x = 0; x < text.length; x++) {
+ if (text.charCodeAt(x) <= 55295) {
+ stripped += text.charAt(x);
+ }
+ }
+
+ return stripped;
+ }
+
+ /**
+ * Replace text within a portion of a string. Equivalent to PHP's substr_replace.
+ *
+ * @param str The string to treat.
+ * @param replace The value to put inside the string.
+ * @param start The index where to start putting the new string. If negative, it will count from the end of the string.
+ * @param length Length of the portion of string which is to be replaced. If negative, it represents the number of characters
+ * from the end of string at which to stop replacing. If not provided, replace until the end of the string.
+ * @returns Treated string.
+ */
+ static substrReplace(str: string, replace: string, start: number, length?: number): string {
+ return Locutus.substrReplace(str, replace, start, length);
+ }
+
+ /**
+ * Remove all ocurrences of a certain character from the start and end of a string.
+ *
+ * @param text Text to treat.
+ * @param character Character to remove.
+ * @returns Treated text.
+ */
+ static trimCharacter(text: string, character: string): string {
+ const escaped = CoreText.escapeForRegex(character);
+ const regExp = new RegExp(`^${escaped}+|${escaped}+$`, 'g');
+
+ return text.replace(regExp, '');
+ }
+
+ /**
+ * If a number has only 1 digit, add a leading zero to it.
+ *
+ * @param num Number to convert.
+ * @returns Number with leading zeros.
+ */
+ static twoDigits(num: string | number): string {
+ if (Number(num) < 10) {
+ return '0' + num;
+ } else {
+ return '' + num; // Convert to string for coherence.
+ }
}
/**
@@ -105,4 +653,27 @@ export class CoreText {
return text.charAt(0).toUpperCase() + text.slice(1);
}
+ /**
+ * Unserialize Array from PHP.
+ *
+ * @param data String to unserialize.
+ * @returns Unserialized data.
+ */
+ static unserialize(data: string): T {
+ return Locutus.unserialize(data);
+ }
+
}
+
+/**
+ * Define text formatting types.
+ */
+export enum CoreTextFormat {
+ FORMAT_MOODLE = 0, // Does all sorts of transformations and filtering.
+ FORMAT_HTML = 1, // Plain HTML (with some tags stripped). Use it by default.
+ FORMAT_PLAIN = 2, // Plain text (even tags are printed in full).
+ // FORMAT_WIKI is deprecated since 2005...
+ FORMAT_MARKDOWN = 4, // Markdown-formatted text http://daringfireball.net/projects/markdown/
+}
+
+export const defaultTextFormat = CoreTextFormat.FORMAT_HTML;
diff --git a/src/core/singletons/url.ts b/src/core/singletons/url.ts
index e9080b1e3..dafbe1c3b 100644
--- a/src/core/singletons/url.ts
+++ b/src/core/singletons/url.ts
@@ -17,10 +17,11 @@ import { CorePath } from './path';
import { CoreText } from './text';
import { CorePlatform } from '@services/platform';
-import { CoreTextUtils } from '@services/utils/text';
import { CoreConstants } from '../constants';
import { CoreMedia } from './media';
import { CoreLang, CoreLangFormat } from '@services/lang';
+import { DomSanitizer } from '@singletons';
+import { SafeUrl } from '@angular/platform-browser';
/**
* Parts contained within a url.
@@ -91,6 +92,23 @@ export class CoreUrl {
// Nothing to do.
}
+ /**
+ * Given an address as a string, return a URL to open the address in maps.
+ *
+ * @param address The address.
+ * @returns URL to view the address.
+ */
+ static buildAddressURL(address: string): SafeUrl {
+ const parsedUrl = CoreUrl.parse(address);
+ if (parsedUrl?.protocol) {
+ // It's already a URL, don't convert it.
+ return DomSanitizer.bypassSecurityTrustUrl(address);
+ }
+
+ return DomSanitizer.bypassSecurityTrustUrl((CorePlatform.isAndroid() ? 'geo:0,0?q=' : 'http://maps.google.com?q=') +
+ encodeURIComponent(address));
+ }
+
/**
* Parse parts of a url, using an implicit protocol if it is missing from the url.
*
@@ -455,6 +473,38 @@ export class CoreUrl {
!CoreMedia.sourceUsesJavascriptPlayer({ src: url });
}
+ /**
+ * Same as Javascript's decodeURI, but if an exception is thrown it will return the original URI.
+ *
+ * @param uri URI to decode.
+ * @returns Decoded URI, or original URI if an exception is thrown.
+ */
+ static decodeURI(uri: string): string {
+ try {
+ return decodeURI(uri);
+ } catch {
+ // Error, use the original URI.
+ }
+
+ return uri;
+ }
+
+ /**
+ * Same as Javascript's decodeURIComponent, but if an exception is thrown it will return the original URI.
+ *
+ * @param uri URI to decode.
+ * @returns Decoded URI, or original URI if an exception is thrown.
+ */
+ static decodeURIComponent(uri: string): string {
+ try {
+ return decodeURIComponent(uri);
+ } catch {
+ // Error, use the original URI.
+ }
+
+ return uri;
+ }
+
/**
* Extracts the parameters from a URL and stores them in an object.
*
@@ -479,7 +529,7 @@ export class CoreUrl {
}
urlAndHash[0].replace(regex, (match: string, key: string, value: string): string => {
- params[key] = value !== undefined ? CoreTextUtils.decodeURIComponent(value) : '';
+ params[key] = value !== undefined ? CoreUrl.decodeURIComponent(value) : '';
if (subParams) {
params[key] = params[key].replace(subParamsPlaceholder, subParams);
@@ -525,7 +575,7 @@ export class CoreUrl {
}
// Check if is a valid URL (contains the pluginfile endpoint) and belongs to the site.
- if (!CoreUrl.isPluginFileUrl(url) || url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) {
+ if (!CoreUrl.isPluginFileUrl(url) || url.indexOf(CoreText.addEndingSlash(siteUrl)) !== 0) {
return url;
}
@@ -584,7 +634,7 @@ export class CoreUrl {
let videoId = '';
const params: CoreUrlParams = {};
- url = CoreTextUtils.decodeHTML(url);
+ url = CoreText.decodeHTML(url);
// Get the video ID.
let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/);
@@ -925,7 +975,7 @@ export class CoreUrl {
url = url.replace(/&/g, '&');
// It site URL is supplied, check if the URL belongs to the site.
- if (siteUrl && url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) {
+ if (siteUrl && url.indexOf(CoreText.addEndingSlash(siteUrl)) !== 0) {
return url;
}
diff --git a/src/testing/utils.ts b/src/testing/utils.ts
index 1cbe0ea39..633f6f6b2 100644
--- a/src/testing/utils.ts
+++ b/src/testing/utils.ts
@@ -20,7 +20,7 @@ import { sep } from 'path';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { ApplicationInit, CoreSingletonProxy, Translate } from '@singletons';
-import { CoreTextUtilsProvider } from '@services/utils/text';
+import { CoreText } from '@singletons/text';
import { CoreExternalContentDirectiveStub } from './stubs/directives/core-external-content';
import { CoreNetwork } from '@services/network';
@@ -44,7 +44,6 @@ abstract class WrapperComponent {
type ServiceInjectionToken = AbstractType | Type | string;
let testBedInitialized = false;
-const textUtils = new CoreTextUtilsProvider();
const DEFAULT_SERVICE_SINGLETON_MOCKS: [CoreSingletonProxy, unknown][] = [
[Translate, mock({
instant: key => key,
@@ -479,7 +478,7 @@ export async function renderWrapperComponent(
): Promise> {
const inputAttributes = Object
.entries(inputs)
- .map(([name, value]) => `[${name}]="${textUtils.escapeHTML(JSON.stringify(value)).replace(/\//g, '\\/')}"`)
+ .map(([name, value]) => `[${name}]="${CoreText.escapeHTML(JSON.stringify(value)).replace(/\//g, '\\/')}"`)
.join(' ');
return renderTemplate(component, `<${tag} ${inputAttributes}>${tag}>`, config);
From 323ccc8c760ad87bdbe2e87cf315b5f45aa2285f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?=
Date: Fri, 19 Jul 2024 15:37:32 +0200
Subject: [PATCH 2/4] MOBILE-4616 chore: Isolate convertHTMLToHTMLElement
---
.../services/handlers/displayh5p.ts | 13 +++++----
.../services/handlers/mediaplugin.ts | 9 +++---
.../bigbluebuttonbn/components/index/index.ts | 5 ++--
.../mod/folder/services/handlers/module.ts | 4 +--
.../mod/lesson/services/lesson-helper.ts | 15 +++++-----
src/addons/mod/lesson/services/lesson.ts | 4 +--
src/addons/mod/quiz/services/quiz-helper.ts | 3 +-
src/addons/mod/quiz/services/quiz.ts | 4 +--
.../services/handlers/calculated.ts | 4 +--
.../qtype/essay/services/handlers/essay.ts | 12 ++++----
.../components/show-password/show-password.ts | 4 +--
.../services/handlers/course-tag-area.ts | 4 +--
.../features/grades/services/grades-helper.ts | 3 +-
.../classes/base-question-component.ts | 5 ++--
.../question/services/question-helper.ts | 21 +++++++-------
src/core/features/tag/services/tag-helper.ts | 4 +--
.../user/services/handlers/tag-area.ts | 4 +--
src/core/services/filepool.ts | 3 +-
src/core/services/utils/dom.ts | 24 +++++++--------
src/core/singletons/dom.ts | 4 +--
src/core/singletons/text.ts | 22 +++-----------
src/core/utils/create-html-element.ts | 29 +++++++++++++++++++
22 files changed, 109 insertions(+), 91 deletions(-)
create mode 100644 src/core/utils/create-html-element.ts
diff --git a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts
index e71a429fa..0ef26c726 100644
--- a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts
+++ b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts
@@ -20,6 +20,7 @@ import { makeSingleton } from '@singletons';
import { CoreH5PPlayerComponent } from '@features/h5p/components/h5p-player/h5p-player';
import { CoreUrl } from '@singletons/url';
import { CoreH5PHelper } from '@features/h5p/classes/helper';
+import { CoreTemplateElement } from '@/core/utils/create-html-element';
/**
* Handler to support the Display H5P filter.
@@ -30,17 +31,15 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle
name = 'AddonFilterDisplayH5PHandler';
filterName = 'displayh5p';
- protected template = document.createElement('template'); // A template element to convert HTML to element.
-
/**
* @inheritdoc
*/
filter(
text: string,
): string | Promise {
- this.template.innerHTML = text;
+ CoreTemplateElement.innerHTML = text;
- const h5pIframes = Array.from(this.template.content.querySelectorAll('iframe.h5p-iframe'));
+ const h5pIframes = Array.from(CoreTemplateElement.content.querySelectorAll('iframe.h5p-iframe'));
// Replace all iframes with an empty div that will be treated in handleHtml.
h5pIframes.forEach((iframe) => {
@@ -53,7 +52,9 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle
});
// Handle H5P iframes embedded using the embed HTML code.
- const embeddedH5PIframes = Array.from(this.template.content.querySelectorAll('iframe.h5p-player'));
+ const embeddedH5PIframes = Array.from(
+ CoreTemplateElement.content.querySelectorAll('iframe.h5p-player'),
+ );
embeddedH5PIframes.forEach((iframe) => {
// Add the preventredirect param to allow authenticating if auto-login fails.
@@ -70,7 +71,7 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle
}
});
- return this.template.innerHTML;
+ return CoreTemplateElement.innerHTML;
}
/**
diff --git a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts
index 5c5cea576..6d111dca4 100644
--- a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts
+++ b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import { CoreTemplateElement } from '@/core/utils/create-html-element';
import { AddonFilterMediaPluginVideoJS } from '@addons/filter/mediaplugin/services/videojs';
import { Injectable } from '@angular/core';
@@ -28,21 +29,19 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
name = 'AddonFilterMediaPluginHandler';
filterName = 'mediaplugin';
- protected template = document.createElement('template'); // A template element to convert HTML to element.
-
/**
* @inheritdoc
*/
filter(text: string): string | Promise {
- this.template.innerHTML = text;
+ CoreTemplateElement.innerHTML = text;
- const videos = Array.from(this.template.content.querySelectorAll('video'));
+ const videos = Array.from(CoreTemplateElement.content.querySelectorAll('video'));
videos.forEach((video) => {
AddonFilterMediaPluginVideoJS.treatYoutubeVideos(video);
});
- return this.template.innerHTML;
+ return CoreTemplateElement.innerHTML;
}
/**
diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts
index 089ade130..29614ff71 100644
--- a/src/addons/mod/bigbluebuttonbn/components/index/index.ts
+++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts
@@ -33,6 +33,7 @@ import {
} from '../../services/bigbluebuttonbn';
import { ADDON_MOD_BBB_COMPONENT } from '../../constants';
import { CoreLoadings } from '@services/loadings';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
/**
* Component that displays a Big Blue Button activity.
@@ -147,7 +148,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
this.recordings = recordingsTable.parsedData.map(recordingData => {
const details: RecordingDetail[] = [];
- const playbacksEl = CoreDomUtils.convertToElement(String(recordingData.playback));
+ const playbacksEl = convertHTMLToHTMLElement(String(recordingData.playback));
const playbacks: RecordingPlayback[] = Array.from(playbacksEl.querySelectorAll('a')).map(playbackAnchor => ({
name: playbackAnchor.textContent ?? '',
url: playbackAnchor.href,
@@ -164,7 +165,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
value = CoreTimeUtils.userDate(Number(value), 'core.strftimedaydate');
} else if (columnData.allowHTML && typeof value === 'string') {
// If the HTML is empty, don't display it.
- const valueElement = CoreDomUtils.convertToElement(value);
+ const valueElement = convertHTMLToHTMLElement(value);
if (!valueElement.querySelector('img') && (valueElement.textContent ?? '').trim() === '') {
return;
}
diff --git a/src/addons/mod/folder/services/handlers/module.ts b/src/addons/mod/folder/services/handlers/module.ts
index aefa39df7..4470e98f5 100644
--- a/src/addons/mod/folder/services/handlers/module.ts
+++ b/src/addons/mod/folder/services/handlers/module.ts
@@ -18,7 +18,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand
import { CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreNavigator } from '@services/navigator';
-import { CoreDomUtils } from '@services/utils/dom';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
import { makeSingleton } from '@singletons';
import { ADDON_MOD_FOLDER_PAGE_NAME } from '../../constants';
@@ -58,7 +58,7 @@ export class AddonModFolderModuleHandlerService extends CoreModuleHandlerBase im
if (module.description) {
// Module description can contain the folder contents if it's inline, remove it.
- const descriptionElement = CoreDomUtils.convertToElement(module.description);
+ const descriptionElement = convertHTMLToHTMLElement(module.description);
Array.from(descriptionElement.querySelectorAll('.foldertree, .folderbuttons, .tertiary-navigation'))
.forEach(element => element.remove());
diff --git a/src/addons/mod/lesson/services/lesson-helper.ts b/src/addons/mod/lesson/services/lesson-helper.ts
index d54343733..25e014a1a 100644
--- a/src/addons/mod/lesson/services/lesson-helper.ts
+++ b/src/addons/mod/lesson/services/lesson-helper.ts
@@ -28,6 +28,7 @@ import {
import { CoreTime } from '@singletons/time';
import { CoreUtils } from '@services/utils/utils';
import { AddonModLessonPageSubtype } from '../constants';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
/**
* Helper service that provides some features for quiz.
@@ -46,7 +47,7 @@ export class AddonModLessonHelperProvider {
* @returns Formatted data.
*/
formatActivityLink(activityLink: string): AddonModLessonActivityLink {
- const element = CoreDomUtils.convertToElement(activityLink);
+ const element = convertHTMLToHTMLElement(activityLink);
const anchor = element.querySelector('a');
if (!anchor) {
@@ -76,7 +77,7 @@ export class AddonModLessonHelperProvider {
buttonText: '',
content: '',
};
- const element = CoreDomUtils.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
// Search the input button.
const button = element.querySelector('input[type="button"]');
@@ -100,7 +101,7 @@ export class AddonModLessonHelperProvider {
*/
getPageButtonsFromHtml(html: string): AddonModLessonPageButton[] {
const buttons: AddonModLessonPageButton[] = [];
- const element = CoreDomUtils.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
// Get the container of the buttons if it exists.
let buttonsContainer = element.querySelector('.branchbuttoncontainer');
@@ -152,7 +153,7 @@ export class AddonModLessonHelperProvider {
*/
getPageContentsFromPageData(data: AddonModLessonGetPageDataWSResponse): string {
// Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered.
- const element = CoreDomUtils.convertToElement(data.pagecontent || '');
+ const element = convertHTMLToHTMLElement(data.pagecontent || '');
const contents = element.querySelector('.contents');
if (contents) {
@@ -178,7 +179,7 @@ export class AddonModLessonHelperProvider {
* @returns Question data.
*/
getQuestionFromPageData(questionForm: FormGroup, pageData: AddonModLessonGetPageDataWSResponse): AddonModLessonQuestion {
- const element = CoreDomUtils.convertToElement(pageData.pagecontent || '');
+ const element = convertHTMLToHTMLElement(pageData.pagecontent || '');
// Get the container of the question answers if it exists.
const fieldContainer = element.querySelector('.fcontainer');
@@ -463,7 +464,7 @@ export class AddonModLessonHelperProvider {
* @returns Object with the data to render the answer. If the answer doesn't require any parsing, return a string with the HTML.
*/
getQuestionPageAnswerDataFromHtml(html: string): AddonModLessonAnswerData {
- const element = CoreDomUtils.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
// Check if it has a checkbox.
let input = element.querySelector('input[type="checkbox"][name*="answer"]');
@@ -588,7 +589,7 @@ export class AddonModLessonHelperProvider {
* @returns Feedback without the question text.
*/
removeQuestionFromFeedback(html: string): string {
- const element = CoreDomUtils.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
// Remove the question text.
CoreDomUtils.removeElement(element, '.generalbox:not(.feedback):not(.correctanswer)');
diff --git a/src/addons/mod/lesson/services/lesson.ts b/src/addons/mod/lesson/services/lesson.ts
index a6f2df32c..dd07e4ae8 100644
--- a/src/addons/mod/lesson/services/lesson.ts
+++ b/src/addons/mod/lesson/services/lesson.ts
@@ -18,7 +18,7 @@ import { CoreSite } from '@classes/sites/site';
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
-import { CoreDomUtils } from '@services/utils/dom';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
import { CoreText } from '@singletons/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
@@ -164,7 +164,7 @@ 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]) {
- const element = CoreDomUtils.convertToElement(page.answerdata.answers[0][0]);
+ const element = convertHTMLToHTMLElement(page.answerdata.answers[0][0]);
return !!element.querySelector('input[type="button"]');
}
diff --git a/src/addons/mod/quiz/services/quiz-helper.ts b/src/addons/mod/quiz/services/quiz-helper.ts
index 4026938fb..1113eb95a 100644
--- a/src/addons/mod/quiz/services/quiz-helper.ts
+++ b/src/addons/mod/quiz/services/quiz-helper.ts
@@ -42,6 +42,7 @@ import { CoreGroups } from '@services/groups';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreModals } from '@services/modals';
import { CoreLoadings } from '@services/loadings';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
/**
* Helper service that provides some features for quiz.
@@ -301,7 +302,7 @@ export class AddonModQuizHelperProvider {
* @returns Question's mark.
*/
getQuestionMarkFromHtml(html: string): string | undefined {
- const element = CoreDomUtils.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
return CoreDomUtils.getContentsOfElement(element, '.grade');
}
diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts
index 07604d170..80cdc81ee 100644
--- a/src/addons/mod/quiz/services/quiz.ts
+++ b/src/addons/mod/quiz/services/quiz.ts
@@ -29,7 +29,7 @@ import {
} from '@features/question/services/question';
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
-import { CoreDomUtils } from '@services/utils/dom';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
@@ -1588,7 +1588,7 @@ export class AddonModQuizProvider {
* @returns Whether it's blocked.
*/
isQuestionBlocked(question: CoreQuestionQuestionParsed): boolean {
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
return !!element.querySelector('.mod_quiz-blocked_question_warning');
}
diff --git a/src/addons/qtype/calculated/services/handlers/calculated.ts b/src/addons/qtype/calculated/services/handlers/calculated.ts
index 58f6ac24a..5c42adc04 100644
--- a/src/addons/qtype/calculated/services/handlers/calculated.ts
+++ b/src/addons/qtype/calculated/services/handlers/calculated.ts
@@ -16,7 +16,7 @@ import { Injectable, Type } from '@angular/core';
import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
import { CoreQuestionHandler } from '@features/question/services/question-delegate';
-import { CoreDomUtils } from '@services/utils/dom';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { AddonQtypeCalculatedComponent } from '../../component/calculated';
@@ -53,7 +53,7 @@ export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler {
*/
hasSeparateUnitField(question: CoreQuestionQuestionParsed): boolean {
if (!question.parsedSettings) {
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]'));
}
diff --git a/src/addons/qtype/essay/services/handlers/essay.ts b/src/addons/qtype/essay/services/handlers/essay.ts
index 33d05bd09..517317035 100644
--- a/src/addons/qtype/essay/services/handlers/essay.ts
+++ b/src/addons/qtype/essay/services/handlers/essay.ts
@@ -22,7 +22,7 @@ import { CoreQuestionHandler } from '@features/question/services/question-delega
import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { CoreFileSession } from '@services/file-session';
import { CoreSites } from '@services/sites';
-import { CoreDomUtils } from '@services/utils/dom';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
import { CoreText } from '@singletons/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSFile } from '@services/ws';
@@ -90,7 +90,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
};
}
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
return {
text: !!element.querySelector('textarea[name*=_answer]'),
@@ -116,7 +116,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
* @inheritdoc
*/
getPreventSubmitMessage(question: CoreQuestionQuestionParsed): string | undefined {
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
const uploadFilesSupported = question.responsefileareas !== undefined;
if (!uploadFilesSupported && element.querySelector('div[id*=filemanager]')) {
@@ -293,7 +293,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
siteId?: string,
): Promise {
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]');
// Search the textarea to get its name.
@@ -375,7 +375,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
siteId?: string,
): Promise {
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]');
if (attachmentsInput) {
@@ -454,7 +454,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
isPlainText = question.parsedSettings.responseformat == 'monospaced' ||
question.parsedSettings.responseformat == 'plain';
} else {
- const questionEl = CoreDomUtils.convertToElement(question.html);
+ const questionEl = convertHTMLToHTMLElement(question.html);
isPlainText = !!questionEl.querySelector('.qtype_essay_monospaced') || !!questionEl.querySelector('.qtype_essay_plain');
}
diff --git a/src/core/components/show-password/show-password.ts b/src/core/components/show-password/show-password.ts
index 88bdcc81f..ad01b6273 100644
--- a/src/core/components/show-password/show-password.ts
+++ b/src/core/components/show-password/show-password.ts
@@ -14,7 +14,7 @@
import { Component, AfterViewInit, Input, ContentChild, ViewEncapsulation } from '@angular/core';
import { IonInput } from '@ionic/angular';
-import { CoreDomUtils } from '@services/utils/dom';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from '@singletons/logger';
@@ -84,7 +84,7 @@ export class CoreShowPasswordComponent implements AfterViewInit {
return;
}
- const toggle = CoreDomUtils.convertToElement('');
+ const toggle = convertHTMLToHTMLElement('');
input.parentElement?.appendChild(toggle.children[0]);
}
diff --git a/src/core/features/course/services/handlers/course-tag-area.ts b/src/core/features/course/services/handlers/course-tag-area.ts
index 9c52f9b9a..93cb1dc5c 100644
--- a/src/core/features/course/services/handlers/course-tag-area.ts
+++ b/src/core/features/course/services/handlers/course-tag-area.ts
@@ -14,7 +14,7 @@
import { Injectable, Type } from '@angular/core';
-import { CoreDomUtils } from '@services/utils/dom';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
import { CoreCourseTagAreaComponent } from '../../components/tag-area/tag-area';
import { makeSingleton } from '@singletons';
@@ -45,7 +45,7 @@ export class CoreCourseTagAreaHandlerService implements CoreTagAreaHandler {
*/
parseContent(content: string): CoreCouseTagItems[] {
const items: CoreCouseTagItems[] = [];
- const element = CoreDomUtils.convertToElement(content);
+ const element = convertHTMLToHTMLElement(content);
Array.from(element.querySelectorAll('div.coursebox')).forEach((coursebox) => {
const courseId = parseInt(coursebox.getAttribute('data-courseid') || '', 10);
diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts
index 3b76d57f1..be49fb3fd 100644
--- a/src/core/features/grades/services/grades-helper.ts
+++ b/src/core/features/grades/services/grades-helper.ts
@@ -44,6 +44,7 @@ import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseAccess } from '@features/course/services/course-options-delegate';
import { CoreLoadings } from '@services/loadings';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
export const GRADES_PAGE_NAME = 'grades';
export const GRADES_PARTICIPANTS_PAGE_NAME = 'participant-grades';
@@ -573,7 +574,7 @@ export class CoreGradesHelperProvider {
const modname = module?.[1];
if (modname !== undefined) {
- const modicon = CoreDomUtils.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined;
+ const modicon = convertHTMLToHTMLElement(text).querySelector('img')?.getAttribute('src') ?? undefined;
row.itemtype = 'mod';
row.itemmodule = modname;
diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts
index f54cd05dd..34085904c 100644
--- a/src/core/features/question/classes/base-question-component.ts
+++ b/src/core/features/question/classes/base-question-component.ts
@@ -25,6 +25,7 @@ import { CoreLogger } from '@singletons/logger';
import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion } from '../services/question-helper';
import { ContextLevel } from '@/core/constants';
import { toBoolean } from '@/core/transforms/boolean';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
/**
* Base class for components to render a question.
@@ -87,7 +88,7 @@ export class CoreQuestionBaseComponent(contentSelector);
diff --git a/src/core/features/question/services/question-helper.ts b/src/core/features/question/services/question-helper.ts
index ca4e4cbb7..be8afac1e 100644
--- a/src/core/features/question/services/question-helper.ts
+++ b/src/core/features/question/services/question-helper.ts
@@ -31,6 +31,7 @@ import { CoreUrl } from '@singletons/url';
import { ContextLevel } from '@/core/constants';
import { CoreIonicColorNames } from '@singletons/colors';
import { CoreViewer } from '@features/viewer/services/viewer';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
/**
* Service with some common functions to handle questions.
@@ -131,7 +132,7 @@ export class CoreQuestionHelperProvider {
selector = selector || '.im-controls [type="submit"]';
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
// Search the buttons.
const buttons = Array.from(element.querySelectorAll(selector));
@@ -150,7 +151,7 @@ export class CoreQuestionHelperProvider {
* @returns Wether the certainty is found.
*/
extractQbehaviourCBM(question: CoreQuestionQuestion): boolean {
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
const labels = Array.from(element.querySelectorAll('.im-controls .certaintychoices label[for*="certainty"]'));
question.behaviourCertaintyOptions = [];
@@ -217,7 +218,7 @@ export class CoreQuestionHelperProvider {
* @returns Whether the seen input is found.
*/
extractQbehaviourSeenInput(question: CoreQuestionQuestion): boolean {
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
// Search the "seen" input.
const seenInput = element.querySelector('input[type="hidden"][name*=seen]');
@@ -273,7 +274,7 @@ export class CoreQuestionHelperProvider {
* @param attrName Name of the attribute to store the HTML in.
*/
protected extractQuestionLastElementNotInContent(question: CoreQuestionQuestion, selector: string, attrName: string): void {
- const element = CoreDomUtils.convertToElement(question.html);
+ const element = convertHTMLToHTMLElement(question.html);
const matches = Array.from(element.querySelectorAll(selector));
// Get the last element and check it's not in the question contents.
@@ -358,7 +359,7 @@ export class CoreQuestionHelperProvider {
* @returns Object where the keys are the names.
*/
getAllInputNamesFromHtml(html: string): Record {
- const element = CoreDomUtils.convertToElement('');
+ const element = convertHTMLToHTMLElement('');
const form = element.children[0];
const answers: Record = {};
@@ -424,7 +425,7 @@ export class CoreQuestionHelperProvider {
* @returns Attachments.
*/
getQuestionAttachmentsFromHtml(html: string): CoreWSFile[] {
- const element = CoreDomUtils.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
// Remove the filemanager (area to attach files to a question).
CoreDomUtils.removeElement(element, 'div[id*=filemanager]');
@@ -461,7 +462,7 @@ export class CoreQuestionHelperProvider {
}
// Search the input holding the sequencecheck.
- const element = CoreDomUtils.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
const input = element.querySelector('input[name*=sequencecheck]');
if (!input || input.name === undefined || input.value === undefined) {
@@ -531,7 +532,7 @@ export class CoreQuestionHelperProvider {
* @returns Validation error message if present.
*/
getValidationErrorFromHtml(html: string): string | undefined {
- const element = CoreDomUtils.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
return CoreDomUtils.getContentsOfElement(element, '.validationerror');
}
@@ -583,7 +584,7 @@ export class CoreQuestionHelperProvider {
* @param question Question.
*/
loadLocalAnswersInHtml(question: CoreQuestionQuestion): void {
- const element = CoreDomUtils.convertToElement('');
+ const element = convertHTMLToHTMLElement('');
const form = element.children[0];
// Search all input elements.
@@ -758,7 +759,7 @@ export class CoreQuestionHelperProvider {
* @returns Whether the button is found.
*/
protected searchBehaviourButton(question: CoreQuestionQuestion, htmlProperty: string, selector: string): boolean {
- const element = CoreDomUtils.convertToElement(question[htmlProperty]);
+ const element = convertHTMLToHTMLElement(question[htmlProperty]);
const button = element.querySelector(selector);
if (!button) {
diff --git a/src/core/features/tag/services/tag-helper.ts b/src/core/features/tag/services/tag-helper.ts
index becd52a95..da1d45ddf 100644
--- a/src/core/features/tag/services/tag-helper.ts
+++ b/src/core/features/tag/services/tag-helper.ts
@@ -14,7 +14,7 @@
import { makeSingleton } from '@singletons';
import { Injectable } from '@angular/core';
-import { CoreDomUtils } from '@services/utils/dom';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
/**
* Service with helper functions for tags.
@@ -30,7 +30,7 @@ export class CoreTagHelperProvider {
*/
parseFeedContent(content: string): CoreTagFeedElement[] {
const items: CoreTagFeedElement[] = [];
- const element = CoreDomUtils.convertToElement(content);
+ const element = convertHTMLToHTMLElement(content);
Array.from(element.querySelectorAll('ul.tag_feed > li')).forEach((itemElement) => {
const item: CoreTagFeedElement = { details: [] };
diff --git a/src/core/features/user/services/handlers/tag-area.ts b/src/core/features/user/services/handlers/tag-area.ts
index 37111a061..9fae87eaf 100644
--- a/src/core/features/user/services/handlers/tag-area.ts
+++ b/src/core/features/user/services/handlers/tag-area.ts
@@ -14,7 +14,7 @@
import { Injectable, Type } from '@angular/core';
-import { CoreDomUtils } from '@services/utils/dom';
+import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
import { CoreUserTagAreaComponent } from '@features/user/components/tag-area/tag-area';
import { CoreTagFeedElement } from '@features/tag/services/tag-helper';
@@ -47,7 +47,7 @@ export class CoreUserTagAreaHandlerService implements CoreTagAreaHandler {
*/
parseContent(content: string): CoreUserTagFeedElement[] {
const items: CoreUserTagFeedElement[] = [];
- const element = CoreDomUtils.convertToElement(content);
+ const element = convertHTMLToHTMLElement(content);
Array.from(element.querySelectorAll('div.user-box')).forEach((userbox: HTMLElement) => {
const avatarLink = userbox.querySelector('a:first-child');
diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts
index 863c1523a..77201627a 100644
--- a/src/core/services/filepool.ts
+++ b/src/core/services/filepool.ts
@@ -58,6 +58,7 @@ import { asyncInstance, AsyncInstance } from '../utils/async-instance';
import { CorePath } from '@singletons/path';
import { CorePromisedValue } from '@classes/promised-value';
import { CoreAnalytics, CoreAnalyticsEventType } from './analytics';
+import { convertHTMLToHTMLElement } from '../utils/create-html-element';
/*
* Factory for handling downloading files and retrieve downloaded files.
@@ -1147,7 +1148,7 @@ export class CoreFilepoolProvider {
extractDownloadableFilesFromHtml(html: string): string[] {
let urls: string[] = [];
- const element = CoreDomUtils.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
const elements: AnchorOrMediaElement[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track'));
for (let i = 0; i < elements.length; i++) {
diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts
index b677d06b5..2f677a978 100644
--- a/src/core/services/utils/dom.ts
+++ b/src/core/services/utils/dom.ts
@@ -55,6 +55,7 @@ import { CorePopovers, OpenPopoverOptions } from '@services/popovers';
import { CoreViewer } from '@features/viewer/services/viewer';
import { CoreLoadings } from '@services/loadings';
import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper';
+import { convertHTMLToHTMLElement, CoreTemplateElement } from '@/core/utils/create-html-element';
/*
* "Utils" service with helper functions for UI, DOM elements and HTML code.
@@ -68,8 +69,6 @@ export class CoreDomUtilsProvider {
protected readonly INPUT_SUPPORT_KEYBOARD: string[] = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number',
'password', 'search', 'tel', 'text', 'time', 'url', 'week'];
- protected template: HTMLTemplateElement = document.createElement('template'); // A template element to convert HTML to element.
-
protected matchesFunctionName?: string; // Name of the "matches" function to use when simulating a closest call.
protected debugDisplay = false; // Whether to display debug messages. Store it in a variable to make it synchronous.
protected displayedAlerts: Record = {}; // To prevent duplicated alerts.
@@ -196,12 +195,11 @@ export class CoreDomUtilsProvider {
*
* @param html Text to convert.
* @returns Element.
+ *
+ * @deprecated since 4.5. Use convertToElement directly instead.
*/
convertToElement(html: string): HTMLElement {
- // Add a div to hold the content, that's the element that will be returned.
- this.template.innerHTML = '
' + html + '
';
-
- return this.template.content.children[0];
+ return convertHTMLToHTMLElement(html);
}
/**
@@ -263,7 +261,7 @@ export class CoreDomUtilsProvider {
* @returns Fixed HTML text.
*/
fixHtml(html: string): string {
- this.template.innerHTML = html;
+ CoreTemplateElement.innerHTML = html;
// eslint-disable-next-line no-control-regex
const attrNameRegExp = /[^\x00-\x20\x7F-\x9F"'>/=]+/;
@@ -278,9 +276,9 @@ export class CoreDomUtilsProvider {
Array.from(element.children).forEach(fixElement);
};
- Array.from(this.template.content.children).forEach(fixElement);
+ Array.from(CoreTemplateElement.content.children).forEach(fixElement);
- return this.template.innerHTML;
+ return CoreTemplateElement.innerHTML;
}
/**
@@ -389,7 +387,7 @@ export class CoreDomUtilsProvider {
* @returns Attribute value.
*/
getHTMLElementAttribute(html: string, attribute: string): string | null {
- return this.convertToElement(html).children[0].getAttribute(attribute);
+ return convertHTMLToHTMLElement(html).children[0].getAttribute(attribute);
}
/**
@@ -650,7 +648,7 @@ export class CoreDomUtilsProvider {
* @returns HTML without the element.
*/
removeElementFromHtml(html: string, selector: string, removeAll?: boolean): string {
- const element = this.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
if (removeAll) {
const selected = element.querySelectorAll(selector);
@@ -698,7 +696,7 @@ export class CoreDomUtilsProvider {
paths: {[url: string]: string},
anchorFn?: (anchor: HTMLElement, href: string) => void,
): string {
- const element = this.convertToElement(html);
+ const element = convertHTMLToHTMLElement(html);
// Treat elements with src (img, audio, video, ...).
const media = Array.from(element.querySelectorAll('img, video, audio, source, track, iframe, embed'));
@@ -1401,7 +1399,7 @@ export class CoreDomUtilsProvider {
* @returns Same text converted to HTMLCollection.
*/
toDom(text: string): HTMLCollection {
- const element = this.convertToElement(text);
+ const element = convertHTMLToHTMLElement(text);
return element.children;
}
diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts
index a8050115e..e35035472 100644
--- a/src/core/singletons/dom.ts
+++ b/src/core/singletons/dom.ts
@@ -17,9 +17,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver } from '@singletons/events';
import { CorePlatform } from '@services/platform';
import { CoreWait } from './wait';
-
-// A template element to convert HTML to element.
-export const CoreTemplateElement: HTMLTemplateElement = document.createElement('template');
+import { CoreTemplateElement } from '../utils/create-html-element';
/**
* Singleton with helper functions for dom.
diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts
index 58c066655..9520127a5 100644
--- a/src/core/singletons/text.ts
+++ b/src/core/singletons/text.ts
@@ -16,7 +16,7 @@ import { Clipboard, Translate } from '@singletons';
import { CoreToasts } from '@services/toasts';
import { Locutus } from './locutus';
import { CoreError } from '@classes/errors/error';
-import { CoreTemplateElement } from './dom';
+import { convertHTMLToHTMLElement } from '../utils/create-html-element';
/**
* Singleton with helper functions for text manipulation.
@@ -190,7 +190,7 @@ export class CoreText {
// 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.
- text = CoreText.convertToElement(text).textContent || '';
+ text = convertHTMLToHTMLElement(text).textContent || '';
// Trim text
text = options.trim ? text.trim() : text;
// Recover or remove new lines.
@@ -199,20 +199,6 @@ export class CoreText {
return text;
}
- /**
- * 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 html Text to convert.
- * @returns Element.
- */
- protected static convertToElement(html: string): HTMLElement {
- // Add a div to hold the content, that's the element that will be returned.
- CoreTemplateElement.innerHTML = '
' + html + '
';
-
- return CoreTemplateElement.content.children[0];
- }
-
/**
* Decode an escaped HTML text. This implementation is based on PHP's htmlspecialchars_decode.
*
@@ -243,7 +229,7 @@ export class CoreText {
*/
static decodeHTMLEntities(text: string): string {
if (text) {
- text = CoreText.convertToElement(text).textContent || '';
+ text = convertHTMLToHTMLElement(text).textContent || '';
}
return text;
@@ -438,7 +424,7 @@ export class CoreText {
* @returns Processed HTML string.
*/
static processHTML(text: string, process: (element: HTMLElement) => unknown): string {
- const element = CoreText.convertToElement(text);
+ const element = convertHTMLToHTMLElement(text);
process(element);
diff --git a/src/core/utils/create-html-element.ts b/src/core/utils/create-html-element.ts
new file mode 100644
index 000000000..2ce0b29cf
--- /dev/null
+++ b/src/core/utils/create-html-element.ts
@@ -0,0 +1,29 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A template element to convert HTML to element.
+export const CoreTemplateElement: HTMLTemplateElement = document.createElement('template');
+
+/**
+ * Convert some HTML as text into an HTMLElement. This HTML is put inside a div or a body.
+ *
+ * @param html Text to convert.
+ * @returns Element.
+ */
+export function convertHTMLToHTMLElement(html: string): HTMLElement {
+ // Add a div to hold the content, that's the element that will be returned.
+ CoreTemplateElement.innerHTML = '
' + html + '
';
+
+ return CoreTemplateElement.content.children[0];
+}
From b0c494ee51008cc379273c0d1e7cc93e2de8578d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?=
Date: Fri, 19 Jul 2024 15:54:15 +0200
Subject: [PATCH 3/4] MOBILE-4616 chore: Move html mode classes to
CoreHTMLClasses
---
.github/workflows/testing.yml | 2 +-
.../bigbluebuttonbn/components/index/index.ts | 6 +-
.../mod/folder/services/handlers/module.ts | 4 +-
.../mod/lesson/services/lesson-helper.ts | 16 ++--
src/addons/mod/lesson/services/lesson.ts | 4 +-
src/addons/mod/quiz/services/quiz-helper.ts | 4 +-
src/addons/mod/quiz/services/quiz.ts | 4 +-
.../services/handlers/calculated.ts | 4 +-
.../qtype/essay/services/handlers/essay.ts | 12 +--
.../components/show-password/show-password.ts | 4 +-
.../services/handlers/course-tag-area.ts | 4 +-
.../features/grades/services/grades-helper.ts | 4 +-
.../classes/base-question-component.ts | 6 +-
.../question/services/question-helper.ts | 22 +++---
.../settings/services/settings-helper.ts | 5 +-
src/core/features/tag/services/tag-helper.ts | 4 +-
.../user/services/handlers/tag-area.ts | 4 +-
src/core/services/filepool.ts | 4 +-
src/core/services/network.ts | 14 ++--
src/core/services/utils/dom.ts | 25 ++++---
src/core/singletons/html-classes.ts | 74 ++++++++++++++-----
src/core/singletons/text.ts | 8 +-
src/core/utils/create-html-element.ts | 2 +-
23 files changed, 141 insertions(+), 95 deletions(-)
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index d6c708a28..7f710b31b 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -69,7 +69,7 @@ jobs:
cat circular-dependencies
lines=$(cat circular-dependencies | wc -l)
echo "Total circular dependencies: $lines"
- test $lines -eq 131
+ test $lines -eq 130
- name: JavaScript code compatibility
run: |
npx check-es-compat www/*.js --polyfills="\{Array,String,TypedArray\}.prototype.at,Object.hasOwn"
diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts
index 29614ff71..2c3e5d53f 100644
--- a/src/addons/mod/bigbluebuttonbn/components/index/index.ts
+++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts
@@ -33,7 +33,7 @@ import {
} from '../../services/bigbluebuttonbn';
import { ADDON_MOD_BBB_COMPONENT } from '../../constants';
import { CoreLoadings } from '@services/loadings';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
/**
* Component that displays a Big Blue Button activity.
@@ -148,7 +148,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
this.recordings = recordingsTable.parsedData.map(recordingData => {
const details: RecordingDetail[] = [];
- const playbacksEl = convertHTMLToHTMLElement(String(recordingData.playback));
+ const playbacksEl = convertTextToHTMLElement(String(recordingData.playback));
const playbacks: RecordingPlayback[] = Array.from(playbacksEl.querySelectorAll('a')).map(playbackAnchor => ({
name: playbackAnchor.textContent ?? '',
url: playbackAnchor.href,
@@ -165,7 +165,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
value = CoreTimeUtils.userDate(Number(value), 'core.strftimedaydate');
} else if (columnData.allowHTML && typeof value === 'string') {
// If the HTML is empty, don't display it.
- const valueElement = convertHTMLToHTMLElement(value);
+ const valueElement = convertTextToHTMLElement(value);
if (!valueElement.querySelector('img') && (valueElement.textContent ?? '').trim() === '') {
return;
}
diff --git a/src/addons/mod/folder/services/handlers/module.ts b/src/addons/mod/folder/services/handlers/module.ts
index 4470e98f5..d0603a298 100644
--- a/src/addons/mod/folder/services/handlers/module.ts
+++ b/src/addons/mod/folder/services/handlers/module.ts
@@ -18,7 +18,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand
import { CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreNavigator } from '@services/navigator';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { makeSingleton } from '@singletons';
import { ADDON_MOD_FOLDER_PAGE_NAME } from '../../constants';
@@ -58,7 +58,7 @@ export class AddonModFolderModuleHandlerService extends CoreModuleHandlerBase im
if (module.description) {
// Module description can contain the folder contents if it's inline, remove it.
- const descriptionElement = convertHTMLToHTMLElement(module.description);
+ const descriptionElement = convertTextToHTMLElement(module.description);
Array.from(descriptionElement.querySelectorAll('.foldertree, .folderbuttons, .tertiary-navigation'))
.forEach(element => element.remove());
diff --git a/src/addons/mod/lesson/services/lesson-helper.ts b/src/addons/mod/lesson/services/lesson-helper.ts
index 25e014a1a..1bd5c7397 100644
--- a/src/addons/mod/lesson/services/lesson-helper.ts
+++ b/src/addons/mod/lesson/services/lesson-helper.ts
@@ -28,7 +28,7 @@ import {
import { CoreTime } from '@singletons/time';
import { CoreUtils } from '@services/utils/utils';
import { AddonModLessonPageSubtype } from '../constants';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
/**
* Helper service that provides some features for quiz.
@@ -47,7 +47,7 @@ export class AddonModLessonHelperProvider {
* @returns Formatted data.
*/
formatActivityLink(activityLink: string): AddonModLessonActivityLink {
- const element = convertHTMLToHTMLElement(activityLink);
+ const element = convertTextToHTMLElement(activityLink);
const anchor = element.querySelector('a');
if (!anchor) {
@@ -77,7 +77,7 @@ export class AddonModLessonHelperProvider {
buttonText: '',
content: '',
};
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
// Search the input button.
const button = element.querySelector('input[type="button"]');
@@ -101,7 +101,7 @@ export class AddonModLessonHelperProvider {
*/
getPageButtonsFromHtml(html: string): AddonModLessonPageButton[] {
const buttons: AddonModLessonPageButton[] = [];
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
// Get the container of the buttons if it exists.
let buttonsContainer = element.querySelector('.branchbuttoncontainer');
@@ -153,7 +153,7 @@ export class AddonModLessonHelperProvider {
*/
getPageContentsFromPageData(data: AddonModLessonGetPageDataWSResponse): string {
// Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered.
- const element = convertHTMLToHTMLElement(data.pagecontent || '');
+ const element = convertTextToHTMLElement(data.pagecontent || '');
const contents = element.querySelector('.contents');
if (contents) {
@@ -179,7 +179,7 @@ export class AddonModLessonHelperProvider {
* @returns Question data.
*/
getQuestionFromPageData(questionForm: FormGroup, pageData: AddonModLessonGetPageDataWSResponse): AddonModLessonQuestion {
- const element = convertHTMLToHTMLElement(pageData.pagecontent || '');
+ const element = convertTextToHTMLElement(pageData.pagecontent || '');
// Get the container of the question answers if it exists.
const fieldContainer = element.querySelector('.fcontainer');
@@ -464,7 +464,7 @@ export class AddonModLessonHelperProvider {
* @returns Object with the data to render the answer. If the answer doesn't require any parsing, return a string with the HTML.
*/
getQuestionPageAnswerDataFromHtml(html: string): AddonModLessonAnswerData {
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
// Check if it has a checkbox.
let input = element.querySelector('input[type="checkbox"][name*="answer"]');
@@ -589,7 +589,7 @@ export class AddonModLessonHelperProvider {
* @returns Feedback without the question text.
*/
removeQuestionFromFeedback(html: string): string {
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
// Remove the question text.
CoreDomUtils.removeElement(element, '.generalbox:not(.feedback):not(.correctanswer)');
diff --git a/src/addons/mod/lesson/services/lesson.ts b/src/addons/mod/lesson/services/lesson.ts
index dd07e4ae8..b9dcd4105 100644
--- a/src/addons/mod/lesson/services/lesson.ts
+++ b/src/addons/mod/lesson/services/lesson.ts
@@ -18,7 +18,7 @@ import { CoreSite } from '@classes/sites/site';
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { CoreText } from '@singletons/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
@@ -164,7 +164,7 @@ 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]) {
- const element = convertHTMLToHTMLElement(page.answerdata.answers[0][0]);
+ const element = convertTextToHTMLElement(page.answerdata.answers[0][0]);
return !!element.querySelector('input[type="button"]');
}
diff --git a/src/addons/mod/quiz/services/quiz-helper.ts b/src/addons/mod/quiz/services/quiz-helper.ts
index 1113eb95a..d1461a886 100644
--- a/src/addons/mod/quiz/services/quiz-helper.ts
+++ b/src/addons/mod/quiz/services/quiz-helper.ts
@@ -42,7 +42,7 @@ import { CoreGroups } from '@services/groups';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreModals } from '@services/modals';
import { CoreLoadings } from '@services/loadings';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
/**
* Helper service that provides some features for quiz.
@@ -302,7 +302,7 @@ export class AddonModQuizHelperProvider {
* @returns Question's mark.
*/
getQuestionMarkFromHtml(html: string): string | undefined {
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
return CoreDomUtils.getContentsOfElement(element, '.grade');
}
diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts
index 80cdc81ee..7384b276e 100644
--- a/src/addons/mod/quiz/services/quiz.ts
+++ b/src/addons/mod/quiz/services/quiz.ts
@@ -29,7 +29,7 @@ import {
} from '@features/question/services/question';
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
@@ -1588,7 +1588,7 @@ export class AddonModQuizProvider {
* @returns Whether it's blocked.
*/
isQuestionBlocked(question: CoreQuestionQuestionParsed): boolean {
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
return !!element.querySelector('.mod_quiz-blocked_question_warning');
}
diff --git a/src/addons/qtype/calculated/services/handlers/calculated.ts b/src/addons/qtype/calculated/services/handlers/calculated.ts
index 5c42adc04..5bc610356 100644
--- a/src/addons/qtype/calculated/services/handlers/calculated.ts
+++ b/src/addons/qtype/calculated/services/handlers/calculated.ts
@@ -16,7 +16,7 @@ import { Injectable, Type } from '@angular/core';
import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
import { CoreQuestionHandler } from '@features/question/services/question-delegate';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { AddonQtypeCalculatedComponent } from '../../component/calculated';
@@ -53,7 +53,7 @@ export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler {
*/
hasSeparateUnitField(question: CoreQuestionQuestionParsed): boolean {
if (!question.parsedSettings) {
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]'));
}
diff --git a/src/addons/qtype/essay/services/handlers/essay.ts b/src/addons/qtype/essay/services/handlers/essay.ts
index 517317035..cc8166546 100644
--- a/src/addons/qtype/essay/services/handlers/essay.ts
+++ b/src/addons/qtype/essay/services/handlers/essay.ts
@@ -22,7 +22,7 @@ import { CoreQuestionHandler } from '@features/question/services/question-delega
import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { CoreFileSession } from '@services/file-session';
import { CoreSites } from '@services/sites';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { CoreText } from '@singletons/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSFile } from '@services/ws';
@@ -90,7 +90,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
};
}
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
return {
text: !!element.querySelector('textarea[name*=_answer]'),
@@ -116,7 +116,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
* @inheritdoc
*/
getPreventSubmitMessage(question: CoreQuestionQuestionParsed): string | undefined {
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
const uploadFilesSupported = question.responsefileareas !== undefined;
if (!uploadFilesSupported && element.querySelector('div[id*=filemanager]')) {
@@ -293,7 +293,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
siteId?: string,
): Promise {
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]');
// Search the textarea to get its name.
@@ -375,7 +375,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
siteId?: string,
): Promise {
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]');
if (attachmentsInput) {
@@ -454,7 +454,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
isPlainText = question.parsedSettings.responseformat == 'monospaced' ||
question.parsedSettings.responseformat == 'plain';
} else {
- const questionEl = convertHTMLToHTMLElement(question.html);
+ const questionEl = convertTextToHTMLElement(question.html);
isPlainText = !!questionEl.querySelector('.qtype_essay_monospaced') || !!questionEl.querySelector('.qtype_essay_plain');
}
diff --git a/src/core/components/show-password/show-password.ts b/src/core/components/show-password/show-password.ts
index ad01b6273..12ca321ca 100644
--- a/src/core/components/show-password/show-password.ts
+++ b/src/core/components/show-password/show-password.ts
@@ -14,7 +14,7 @@
import { Component, AfterViewInit, Input, ContentChild, ViewEncapsulation } from '@angular/core';
import { IonInput } from '@ionic/angular';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from '@singletons/logger';
@@ -84,7 +84,7 @@ export class CoreShowPasswordComponent implements AfterViewInit {
return;
}
- const toggle = convertHTMLToHTMLElement('');
+ const toggle = convertTextToHTMLElement('');
input.parentElement?.appendChild(toggle.children[0]);
}
diff --git a/src/core/features/course/services/handlers/course-tag-area.ts b/src/core/features/course/services/handlers/course-tag-area.ts
index 93cb1dc5c..e6b1de384 100644
--- a/src/core/features/course/services/handlers/course-tag-area.ts
+++ b/src/core/features/course/services/handlers/course-tag-area.ts
@@ -14,7 +14,7 @@
import { Injectable, Type } from '@angular/core';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
import { CoreCourseTagAreaComponent } from '../../components/tag-area/tag-area';
import { makeSingleton } from '@singletons';
@@ -45,7 +45,7 @@ export class CoreCourseTagAreaHandlerService implements CoreTagAreaHandler {
*/
parseContent(content: string): CoreCouseTagItems[] {
const items: CoreCouseTagItems[] = [];
- const element = convertHTMLToHTMLElement(content);
+ const element = convertTextToHTMLElement(content);
Array.from(element.querySelectorAll('div.coursebox')).forEach((coursebox) => {
const courseId = parseInt(coursebox.getAttribute('data-courseid') || '', 10);
diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts
index be49fb3fd..e7a6b6632 100644
--- a/src/core/features/grades/services/grades-helper.ts
+++ b/src/core/features/grades/services/grades-helper.ts
@@ -44,7 +44,7 @@ import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseAccess } from '@features/course/services/course-options-delegate';
import { CoreLoadings } from '@services/loadings';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
export const GRADES_PAGE_NAME = 'grades';
export const GRADES_PARTICIPANTS_PAGE_NAME = 'participant-grades';
@@ -574,7 +574,7 @@ export class CoreGradesHelperProvider {
const modname = module?.[1];
if (modname !== undefined) {
- const modicon = convertHTMLToHTMLElement(text).querySelector('img')?.getAttribute('src') ?? undefined;
+ const modicon = convertTextToHTMLElement(text).querySelector('img')?.getAttribute('src') ?? undefined;
row.itemtype = 'mod';
row.itemmodule = modname;
diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts
index 34085904c..2c7c13cd5 100644
--- a/src/core/features/question/classes/base-question-component.ts
+++ b/src/core/features/question/classes/base-question-component.ts
@@ -25,7 +25,7 @@ import { CoreLogger } from '@singletons/logger';
import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion } from '../services/question-helper';
import { ContextLevel } from '@/core/constants';
import { toBoolean } from '@/core/transforms/boolean';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
/**
* Base class for components to render a question.
@@ -88,7 +88,7 @@ export class CoreQuestionBaseComponent(contentSelector);
diff --git a/src/core/features/question/services/question-helper.ts b/src/core/features/question/services/question-helper.ts
index be8afac1e..736d3a625 100644
--- a/src/core/features/question/services/question-helper.ts
+++ b/src/core/features/question/services/question-helper.ts
@@ -31,7 +31,7 @@ import { CoreUrl } from '@singletons/url';
import { ContextLevel } from '@/core/constants';
import { CoreIonicColorNames } from '@singletons/colors';
import { CoreViewer } from '@features/viewer/services/viewer';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
/**
* Service with some common functions to handle questions.
@@ -132,7 +132,7 @@ export class CoreQuestionHelperProvider {
selector = selector || '.im-controls [type="submit"]';
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
// Search the buttons.
const buttons = Array.from(element.querySelectorAll(selector));
@@ -151,7 +151,7 @@ export class CoreQuestionHelperProvider {
* @returns Wether the certainty is found.
*/
extractQbehaviourCBM(question: CoreQuestionQuestion): boolean {
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
const labels = Array.from(element.querySelectorAll('.im-controls .certaintychoices label[for*="certainty"]'));
question.behaviourCertaintyOptions = [];
@@ -218,7 +218,7 @@ export class CoreQuestionHelperProvider {
* @returns Whether the seen input is found.
*/
extractQbehaviourSeenInput(question: CoreQuestionQuestion): boolean {
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
// Search the "seen" input.
const seenInput = element.querySelector('input[type="hidden"][name*=seen]');
@@ -274,7 +274,7 @@ export class CoreQuestionHelperProvider {
* @param attrName Name of the attribute to store the HTML in.
*/
protected extractQuestionLastElementNotInContent(question: CoreQuestionQuestion, selector: string, attrName: string): void {
- const element = convertHTMLToHTMLElement(question.html);
+ const element = convertTextToHTMLElement(question.html);
const matches = Array.from(element.querySelectorAll(selector));
// Get the last element and check it's not in the question contents.
@@ -359,7 +359,7 @@ export class CoreQuestionHelperProvider {
* @returns Object where the keys are the names.
*/
getAllInputNamesFromHtml(html: string): Record {
- const element = convertHTMLToHTMLElement('');
+ const element = convertTextToHTMLElement('');
const form = element.children[0];
const answers: Record = {};
@@ -425,7 +425,7 @@ export class CoreQuestionHelperProvider {
* @returns Attachments.
*/
getQuestionAttachmentsFromHtml(html: string): CoreWSFile[] {
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
// Remove the filemanager (area to attach files to a question).
CoreDomUtils.removeElement(element, 'div[id*=filemanager]');
@@ -462,7 +462,7 @@ export class CoreQuestionHelperProvider {
}
// Search the input holding the sequencecheck.
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
const input = element.querySelector('input[name*=sequencecheck]');
if (!input || input.name === undefined || input.value === undefined) {
@@ -532,7 +532,7 @@ export class CoreQuestionHelperProvider {
* @returns Validation error message if present.
*/
getValidationErrorFromHtml(html: string): string | undefined {
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
return CoreDomUtils.getContentsOfElement(element, '.validationerror');
}
@@ -584,7 +584,7 @@ export class CoreQuestionHelperProvider {
* @param question Question.
*/
loadLocalAnswersInHtml(question: CoreQuestionQuestion): void {
- const element = convertHTMLToHTMLElement('');
+ const element = convertTextToHTMLElement('');
const form = element.children[0];
// Search all input elements.
@@ -759,7 +759,7 @@ export class CoreQuestionHelperProvider {
* @returns Whether the button is found.
*/
protected searchBehaviourButton(question: CoreQuestionQuestion, htmlProperty: string, selector: string): boolean {
- const element = convertHTMLToHTMLElement(question[htmlProperty]);
+ const element = convertTextToHTMLElement(question[htmlProperty]);
const button = element.querySelector(selector);
if (!button) {
diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts
index de0febbf4..0936acaca 100644
--- a/src/core/features/settings/services/settings-helper.ts
+++ b/src/core/features/settings/services/settings-helper.ts
@@ -31,6 +31,7 @@ import { CoreError } from '@classes/errors/error';
import { Observable, Subject } from 'rxjs';
import { CoreErrorHelper } from '@services/error-helper';
import { CoreNavigator } from '@services/navigator';
+import { CoreHTMLClasses } from '@singletons/html-classes';
/**
* Object with space usage and cache entries that can be erased.
@@ -433,10 +434,10 @@ export class CoreSettingsHelperProvider {
* @param enable True to enable dark mode, false to disable.
*/
protected toggleDarkMode(enable: boolean = false): void {
- const isDark = CoreDomUtils.hasModeClass('dark');
+ const isDark = CoreHTMLClasses.hasModeClass('dark');
if (isDark !== enable) {
- CoreDomUtils.toggleModeClass('dark', enable);
+ CoreHTMLClasses.toggleModeClass('dark', enable);
this.darkModeObservable.next(enable);
CoreApp.setSystemUIColors();
diff --git a/src/core/features/tag/services/tag-helper.ts b/src/core/features/tag/services/tag-helper.ts
index da1d45ddf..448dcfd59 100644
--- a/src/core/features/tag/services/tag-helper.ts
+++ b/src/core/features/tag/services/tag-helper.ts
@@ -14,7 +14,7 @@
import { makeSingleton } from '@singletons';
import { Injectable } from '@angular/core';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
/**
* Service with helper functions for tags.
@@ -30,7 +30,7 @@ export class CoreTagHelperProvider {
*/
parseFeedContent(content: string): CoreTagFeedElement[] {
const items: CoreTagFeedElement[] = [];
- const element = convertHTMLToHTMLElement(content);
+ const element = convertTextToHTMLElement(content);
Array.from(element.querySelectorAll('ul.tag_feed > li')).forEach((itemElement) => {
const item: CoreTagFeedElement = { details: [] };
diff --git a/src/core/features/user/services/handlers/tag-area.ts b/src/core/features/user/services/handlers/tag-area.ts
index 9fae87eaf..4e5c6761f 100644
--- a/src/core/features/user/services/handlers/tag-area.ts
+++ b/src/core/features/user/services/handlers/tag-area.ts
@@ -14,7 +14,7 @@
import { Injectable, Type } from '@angular/core';
-import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
import { CoreUserTagAreaComponent } from '@features/user/components/tag-area/tag-area';
import { CoreTagFeedElement } from '@features/tag/services/tag-helper';
@@ -47,7 +47,7 @@ export class CoreUserTagAreaHandlerService implements CoreTagAreaHandler {
*/
parseContent(content: string): CoreUserTagFeedElement[] {
const items: CoreUserTagFeedElement[] = [];
- const element = convertHTMLToHTMLElement(content);
+ const element = convertTextToHTMLElement(content);
Array.from(element.querySelectorAll('div.user-box')).forEach((userbox: HTMLElement) => {
const avatarLink = userbox.querySelector('a:first-child');
diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts
index 77201627a..e0417317f 100644
--- a/src/core/services/filepool.ts
+++ b/src/core/services/filepool.ts
@@ -58,7 +58,7 @@ import { asyncInstance, AsyncInstance } from '../utils/async-instance';
import { CorePath } from '@singletons/path';
import { CorePromisedValue } from '@classes/promised-value';
import { CoreAnalytics, CoreAnalyticsEventType } from './analytics';
-import { convertHTMLToHTMLElement } from '../utils/create-html-element';
+import { convertTextToHTMLElement } from '../utils/create-html-element';
/*
* Factory for handling downloading files and retrieve downloaded files.
@@ -1148,7 +1148,7 @@ export class CoreFilepoolProvider {
extractDownloadableFilesFromHtml(html: string): string[] {
let urls: string[] = [];
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
const elements: AnchorOrMediaElement[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track'));
for (let i = 0; i < elements.length; i++) {
diff --git a/src/core/services/network.ts b/src/core/services/network.ts
index 3c8cc18f9..35597f4f5 100644
--- a/src/core/services/network.ts
+++ b/src/core/services/network.ts
@@ -17,7 +17,7 @@ import { CorePlatform } from '@services/platform';
import { Network } from '@awesome-cordova-plugins/network/ngx';
import { NgZone, makeSingleton } from '@singletons';
import { Observable, Subject, merge } from 'rxjs';
-import { CoreDomUtils } from './utils/dom';
+import { CoreHTMLClasses } from '@singletons/html-classes';
export enum CoreNetworkConnection {
UNKNOWN = 'unknown',
@@ -109,24 +109,24 @@ export class CoreNetworkService extends Network {
NgZone.run(() => {
const isOnline = this.isOnline();
- const hadOfflineMessage = CoreDomUtils.hasModeClass('core-offline');
+ const hadOfflineMessage = CoreHTMLClasses.hasModeClass('core-offline');
- CoreDomUtils.toggleModeClass('core-offline', !isOnline);
+ CoreHTMLClasses.toggleModeClass('core-offline', !isOnline);
if (isOnline && hadOfflineMessage) {
- CoreDomUtils.toggleModeClass('core-online', true);
+ CoreHTMLClasses.toggleModeClass('core-online', true);
setTimeout(() => {
- CoreDomUtils.toggleModeClass('core-online', false);
+ CoreHTMLClasses.toggleModeClass('core-online', false);
}, 3000);
} else if (!isOnline) {
- CoreDomUtils.toggleModeClass('core-online', false);
+ CoreHTMLClasses.toggleModeClass('core-online', false);
}
});
});
const isOnline = this.isOnline();
- CoreDomUtils.toggleModeClass('core-offline', !isOnline);
+ CoreHTMLClasses.toggleModeClass('core-offline', !isOnline);
}
/**
diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts
index 2f677a978..f074d20c8 100644
--- a/src/core/services/utils/dom.ts
+++ b/src/core/services/utils/dom.ts
@@ -55,7 +55,8 @@ import { CorePopovers, OpenPopoverOptions } from '@services/popovers';
import { CoreViewer } from '@features/viewer/services/viewer';
import { CoreLoadings } from '@services/loadings';
import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper';
-import { convertHTMLToHTMLElement, CoreTemplateElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement, CoreTemplateElement } from '@/core/utils/create-html-element';
+import { CoreHTMLClasses } from '@singletons/html-classes';
/*
* "Utils" service with helper functions for UI, DOM elements and HTML code.
@@ -199,7 +200,7 @@ export class CoreDomUtilsProvider {
* @deprecated since 4.5. Use convertToElement directly instead.
*/
convertToElement(html: string): HTMLElement {
- return convertHTMLToHTMLElement(html);
+ return convertTextToHTMLElement(html);
}
/**
@@ -387,7 +388,7 @@ export class CoreDomUtilsProvider {
* @returns Attribute value.
*/
getHTMLElementAttribute(html: string, attribute: string): string | null {
- return convertHTMLToHTMLElement(html).children[0].getAttribute(attribute);
+ return convertTextToHTMLElement(html).children[0].getAttribute(attribute);
}
/**
@@ -648,7 +649,7 @@ export class CoreDomUtilsProvider {
* @returns HTML without the element.
*/
removeElementFromHtml(html: string, selector: string, removeAll?: boolean): string {
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
if (removeAll) {
const selected = element.querySelectorAll(selector);
@@ -696,7 +697,7 @@ export class CoreDomUtilsProvider {
paths: {[url: string]: string},
anchorFn?: (anchor: HTMLElement, href: string) => void,
): string {
- const element = convertHTMLToHTMLElement(html);
+ const element = convertTextToHTMLElement(html);
// Treat elements with src (img, audio, video, ...).
const media = Array.from(element.querySelectorAll('img, video, audio, source, track, iframe, embed'));
@@ -1399,7 +1400,7 @@ export class CoreDomUtilsProvider {
* @returns Same text converted to HTMLCollection.
*/
toDom(text: string): HTMLCollection {
- const element = convertHTMLToHTMLElement(text);
+ const element = convertTextToHTMLElement(text);
return element.children;
}
@@ -1610,18 +1611,22 @@ export class CoreDomUtilsProvider {
*
* @param className Class name.
* @returns Whether the CSS class is set.
+ *
+ * @deprecated since 4.5. Use CoreHTMLClasses.hasModeClass instead.
*/
hasModeClass(className: string): boolean {
- return document.documentElement.classList.contains(className);
+ return CoreHTMLClasses.hasModeClass(className);
}
/**
* Get active mode CSS classes.
*
* @returns Mode classes.
+ *
+ * @deprecated since 4.5. Use CoreHTMLClasses.getModeClasses instead.
*/
getModeClasses(): string[] {
- return Array.from(document.documentElement.classList);
+ return CoreHTMLClasses.getModeClasses();
}
/**
@@ -1629,12 +1634,14 @@ export class CoreDomUtilsProvider {
*
* @param className Class name.
* @param enable Whether to add or remove the class.
+ *
+ * @deprecated since 4.5. Use CoreHTMLClasses.toggleModeClass instead.
*/
toggleModeClass(
className: string,
enable = false,
): void {
- document.documentElement.classList.toggle(className, enable);
+ CoreHTMLClasses.toggleModeClass(className, enable);
}
}
diff --git a/src/core/singletons/html-classes.ts b/src/core/singletons/html-classes.ts
index ff74fda5b..9803f1e19 100644
--- a/src/core/singletons/html-classes.ts
+++ b/src/core/singletons/html-classes.ts
@@ -14,29 +14,31 @@
import { CoreSiteInfo, CoreSiteInfoResponse } from '@classes/sites/unauthenticated-site';
import { CoreSites } from '@services/sites';
-import { CoreDomUtils } from '@services/utils/dom';
import { CoreUrl } from './url';
import { CoreConstants } from '../constants';
import { ScrollDetail } from '@ionic/angular';
import { CoreDom } from './dom';
-const MOODLE_SITE_URL_PREFIX = 'url-';
-const MOODLE_VERSION_PREFIX = 'version-';
-const MOODLEAPP_VERSION_PREFIX = 'moodleapp-';
-const MOODLE_SITE_THEME_PREFIX = 'theme-site-';
-
/**
* Singleton with helper functions to manage HTML classes.
*/
export class CoreHTMLClasses {
+ protected static readonly MOODLE_SITE_URL_PREFIX = 'url-';
+ protected static readonly MOODLE_VERSION_PREFIX = 'version-';
+ protected static readonly MOODLEAPP_VERSION_PREFIX = 'moodleapp-';
+ protected static readonly MOODLE_SITE_THEME_PREFIX = 'theme-site-';
+
/**
* Initialize HTML classes.
*/
static initialize(): void {
- CoreDomUtils.toggleModeClass('ionic8', true);
- CoreDomUtils.toggleModeClass('development', CoreConstants.BUILD.isDevelopment);
- CoreHTMLClasses.addVersionClass(MOODLEAPP_VERSION_PREFIX, CoreConstants.CONFIG.versionname.replace('-dev', ''));
+ CoreHTMLClasses.toggleModeClass('ionic8', true);
+ CoreHTMLClasses.toggleModeClass('development', CoreConstants.BUILD.isDevelopment);
+ CoreHTMLClasses.addVersionClass(
+ CoreHTMLClasses.MOODLEAPP_VERSION_PREFIX,
+ CoreConstants.CONFIG.versionname.replace('-dev', ''),
+ );
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = window;
@@ -72,9 +74,9 @@ export class CoreHTMLClasses {
parts[1] = parts[1] || '0';
parts[2] = parts[2] || '0';
- CoreDomUtils.toggleModeClass(prefix + parts[0], true);
- CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1], true);
- CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1] + '-' + parts[2], true);
+ CoreHTMLClasses.toggleModeClass(prefix + parts[0], true);
+ CoreHTMLClasses.toggleModeClass(prefix + parts[0] + '-' + parts[1], true);
+ CoreHTMLClasses.toggleModeClass(prefix + parts[0] + '-' + parts[1] + '-' + parts[2], true);
}
/**
@@ -83,12 +85,12 @@ export class CoreHTMLClasses {
* @param prefixes Prefixes of the class mode to be removed.
*/
protected static removeModeClasses(prefixes: string[]): void {
- for (const modeClass of CoreDomUtils.getModeClasses()) {
+ for (const modeClass of CoreHTMLClasses.getModeClasses()) {
if (!prefixes.some((prefix) => modeClass.startsWith(prefix))) {
continue;
}
- CoreDomUtils.toggleModeClass(modeClass, false);
+ CoreHTMLClasses.toggleModeClass(modeClass, false);
}
}
@@ -101,11 +103,11 @@ export class CoreHTMLClasses {
// Add version classes to html tag.
this.removeSiteClasses();
- this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(siteInfo.release || ''));
+ this.addVersionClass(CoreHTMLClasses.MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(siteInfo.release || ''));
this.addSiteUrlClass(siteInfo.siteurl);
if (siteInfo.theme) {
- CoreDomUtils.toggleModeClass(MOODLE_SITE_THEME_PREFIX + siteInfo.theme, true);
+ CoreHTMLClasses.toggleModeClass(CoreHTMLClasses.MOODLE_SITE_THEME_PREFIX + siteInfo.theme, true);
}
}
@@ -115,7 +117,11 @@ export class CoreHTMLClasses {
static removeSiteClasses(): void {
// Remove version classes from html tag.
this.removeModeClasses(
- [MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX, MOODLE_SITE_THEME_PREFIX],
+ [
+ CoreHTMLClasses.MOODLE_VERSION_PREFIX,
+ CoreHTMLClasses.MOODLE_SITE_URL_PREFIX,
+ CoreHTMLClasses.MOODLE_SITE_THEME_PREFIX,
+ ],
);
}
@@ -157,7 +163,39 @@ export class CoreHTMLClasses {
static addSiteUrlClass(siteUrl: string): void {
const className = this.urlToClassName(siteUrl);
- CoreDomUtils.toggleModeClass(MOODLE_SITE_URL_PREFIX + className, true);
+ CoreHTMLClasses.toggleModeClass(CoreHTMLClasses.MOODLE_SITE_URL_PREFIX + className, true);
+ }
+
+ /**
+ * Check whether a CSS class indicating an app mode is set.
+ *
+ * @param className Class name.
+ * @returns Whether the CSS class is set.
+ */
+ static hasModeClass(className: string): boolean {
+ return document.documentElement.classList.contains(className);
+ }
+
+ /**
+ * Get active mode CSS classes.
+ *
+ * @returns Mode classes.
+ */
+ static getModeClasses(): string[] {
+ return Array.from(document.documentElement.classList);
+ }
+
+ /**
+ * Toggle a CSS class in the root element used to indicate app modes.
+ *
+ * @param className Class name.
+ * @param enable Whether to add or remove the class.
+ */
+ static toggleModeClass(
+ className: string,
+ enable = false,
+ ): void {
+ document.documentElement.classList.toggle(className, enable);
}
}
diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts
index 9520127a5..482ce044a 100644
--- a/src/core/singletons/text.ts
+++ b/src/core/singletons/text.ts
@@ -16,7 +16,7 @@ import { Clipboard, Translate } from '@singletons';
import { CoreToasts } from '@services/toasts';
import { Locutus } from './locutus';
import { CoreError } from '@classes/errors/error';
-import { convertHTMLToHTMLElement } from '../utils/create-html-element';
+import { convertTextToHTMLElement } from '../utils/create-html-element';
/**
* Singleton with helper functions for text manipulation.
@@ -190,7 +190,7 @@ export class CoreText {
// 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.
- text = convertHTMLToHTMLElement(text).textContent || '';
+ text = convertTextToHTMLElement(text).textContent || '';
// Trim text
text = options.trim ? text.trim() : text;
// Recover or remove new lines.
@@ -229,7 +229,7 @@ export class CoreText {
*/
static decodeHTMLEntities(text: string): string {
if (text) {
- text = convertHTMLToHTMLElement(text).textContent || '';
+ text = convertTextToHTMLElement(text).textContent || '';
}
return text;
@@ -424,7 +424,7 @@ export class CoreText {
* @returns Processed HTML string.
*/
static processHTML(text: string, process: (element: HTMLElement) => unknown): string {
- const element = convertHTMLToHTMLElement(text);
+ const element = convertTextToHTMLElement(text);
process(element);
diff --git a/src/core/utils/create-html-element.ts b/src/core/utils/create-html-element.ts
index 2ce0b29cf..60f98e5c2 100644
--- a/src/core/utils/create-html-element.ts
+++ b/src/core/utils/create-html-element.ts
@@ -21,7 +21,7 @@ export const CoreTemplateElement: HTMLTemplateElement = document.createElement('
* @param html Text to convert.
* @returns Element.
*/
-export function convertHTMLToHTMLElement(html: string): HTMLElement {
+export function convertTextToHTMLElement(html: string): HTMLElement {
// Add a div to hold the content, that's the element that will be returned.
CoreTemplateElement.innerHTML = '
' + html + '
';
From 00951b22d5377354b77a13215c687c85f9ae2cd0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?=
Date: Wed, 14 Aug 2024 14:29:51 +0200
Subject: [PATCH 4/4] MOBILE-4616 chore: Always use convertTextToHTMLElement to
convert HTML
---
.../services/handlers/displayh5p.ts | 60 +++++++++----------
.../services/handlers/mediaplugin.ts | 14 ++---
src/core/services/utils/dom.ts | 31 +++++-----
src/core/singletons/dom.ts | 6 +-
src/core/utils/create-html-element.ts | 13 ++--
5 files changed, 59 insertions(+), 65 deletions(-)
diff --git a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts
index 0ef26c726..8e4dbcb47 100644
--- a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts
+++ b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts
@@ -20,7 +20,7 @@ import { makeSingleton } from '@singletons';
import { CoreH5PPlayerComponent } from '@features/h5p/components/h5p-player/h5p-player';
import { CoreUrl } from '@singletons/url';
import { CoreH5PHelper } from '@features/h5p/classes/helper';
-import { CoreTemplateElement } from '@/core/utils/create-html-element';
+import { CoreText } from '@singletons/text';
/**
* Handler to support the Display H5P filter.
@@ -37,41 +37,39 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle
filter(
text: string,
): string | Promise {
- CoreTemplateElement.innerHTML = text;
+ return CoreText.processHTML(text, (element) => {
+ const h5pIframes = Array.from(element.querySelectorAll('iframe.h5p-iframe'));
- const h5pIframes = Array.from(CoreTemplateElement.content.querySelectorAll('iframe.h5p-iframe'));
+ // Replace all iframes with an empty div that will be treated in handleHtml.
+ h5pIframes.forEach((iframe) => {
+ const placeholder = document.createElement('div');
- // Replace all iframes with an empty div that will be treated in handleHtml.
- h5pIframes.forEach((iframe) => {
- const placeholder = document.createElement('div');
+ placeholder.classList.add('core-h5p-tmp-placeholder');
+ placeholder.setAttribute('data-player-src', iframe.src);
- placeholder.classList.add('core-h5p-tmp-placeholder');
- placeholder.setAttribute('data-player-src', iframe.src);
+ iframe.parentElement?.replaceChild(placeholder, iframe);
+ });
- iframe.parentElement?.replaceChild(placeholder, iframe);
+ // Handle H5P iframes embedded using the embed HTML code.
+ const embeddedH5PIframes = Array.from(
+ element.querySelectorAll('iframe.h5p-player'),
+ );
+
+ embeddedH5PIframes.forEach((iframe) => {
+ // Add the preventredirect param to allow authenticating if auto-login fails.
+ iframe.src = CoreUrl.addParamsToUrl(iframe.src, { preventredirect: false });
+
+ // Add resizer script so the H5P has the right height.
+ CoreH5PHelper.addResizerScript();
+
+ // If the iframe has a small height, add some minimum initial height so it's seen if auto-login fails.
+ const styleHeight = Number(iframe.style.height);
+ const height = Number(iframe.getAttribute('height'));
+ if ((!height || height < 400) && (!styleHeight || styleHeight < 400)) {
+ iframe.style.height = '400px';
+ }
+ });
});
-
- // Handle H5P iframes embedded using the embed HTML code.
- const embeddedH5PIframes = Array.from(
- CoreTemplateElement.content.querySelectorAll('iframe.h5p-player'),
- );
-
- embeddedH5PIframes.forEach((iframe) => {
- // Add the preventredirect param to allow authenticating if auto-login fails.
- iframe.src = CoreUrl.addParamsToUrl(iframe.src, { preventredirect: false });
-
- // Add resizer script so the H5P has the right height.
- CoreH5PHelper.addResizerScript();
-
- // If the iframe has a small height, add some minimum initial height so it's seen if auto-login fails.
- const styleHeight = Number(iframe.style.height);
- const height = Number(iframe.getAttribute('height'));
- if ((!height || height < 400) && (!styleHeight || styleHeight < 400)) {
- iframe.style.height = '400px';
- }
- });
-
- return CoreTemplateElement.innerHTML;
}
/**
diff --git a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts
index 6d111dca4..363cc109c 100644
--- a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts
+++ b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { CoreTemplateElement } from '@/core/utils/create-html-element';
+import { CoreText } from '@singletons/text';
import { AddonFilterMediaPluginVideoJS } from '@addons/filter/mediaplugin/services/videojs';
import { Injectable } from '@angular/core';
@@ -33,15 +33,13 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
* @inheritdoc
*/
filter(text: string): string | Promise {
- CoreTemplateElement.innerHTML = text;
+ return CoreText.processHTML(text, (element) => {
+ const videos = Array.from(element.querySelectorAll('video'));
- const videos = Array.from(CoreTemplateElement.content.querySelectorAll('video'));
-
- videos.forEach((video) => {
- AddonFilterMediaPluginVideoJS.treatYoutubeVideos(video);
+ videos.forEach((video) => {
+ AddonFilterMediaPluginVideoJS.treatYoutubeVideos(video);
+ });
});
-
- return CoreTemplateElement.innerHTML;
}
/**
diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts
index f074d20c8..924ef2b8c 100644
--- a/src/core/services/utils/dom.ts
+++ b/src/core/services/utils/dom.ts
@@ -55,7 +55,7 @@ import { CorePopovers, OpenPopoverOptions } from '@services/popovers';
import { CoreViewer } from '@features/viewer/services/viewer';
import { CoreLoadings } from '@services/loadings';
import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper';
-import { convertTextToHTMLElement, CoreTemplateElement } from '@/core/utils/create-html-element';
+import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { CoreHTMLClasses } from '@singletons/html-classes';
/*
@@ -197,7 +197,7 @@ export class CoreDomUtilsProvider {
* @param html Text to convert.
* @returns Element.
*
- * @deprecated since 4.5. Use convertToElement directly instead.
+ * @deprecated since 4.5. Use convertTextToHTMLElement directly instead.
*/
convertToElement(html: string): HTMLElement {
return convertTextToHTMLElement(html);
@@ -262,24 +262,23 @@ export class CoreDomUtilsProvider {
* @returns Fixed HTML text.
*/
fixHtml(html: string): string {
- CoreTemplateElement.innerHTML = html;
+ return CoreText.processHTML(html, (element) => {
+ // eslint-disable-next-line no-control-regex
+ const attrNameRegExp = /[^\x00-\x20\x7F-\x9F"'>/=]+/;
+ const fixElement = (element: Element): void => {
+ // Remove attributes with an invalid name.
+ Array.from(element.attributes).forEach((attr) => {
+ if (!attrNameRegExp.test(attr.name)) {
+ element.removeAttributeNode(attr);
+ }
+ });
- // eslint-disable-next-line no-control-regex
- const attrNameRegExp = /[^\x00-\x20\x7F-\x9F"'>/=]+/;
- const fixElement = (element: Element): void => {
- // Remove attributes with an invalid name.
- Array.from(element.attributes).forEach((attr) => {
- if (!attrNameRegExp.test(attr.name)) {
- element.removeAttributeNode(attr);
- }
- });
+ Array.from(element.children).forEach(fixElement);
+ };
Array.from(element.children).forEach(fixElement);
- };
+ });
- Array.from(CoreTemplateElement.content.children).forEach(fixElement);
-
- return CoreTemplateElement.innerHTML;
}
/**
diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts
index e35035472..c35b87ed6 100644
--- a/src/core/singletons/dom.ts
+++ b/src/core/singletons/dom.ts
@@ -17,7 +17,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver } from '@singletons/events';
import { CorePlatform } from '@services/platform';
import { CoreWait } from './wait';
-import { CoreTemplateElement } from '../utils/create-html-element';
+import { convertTextToHTMLElement } from '../utils/create-html-element';
/**
* Singleton with helper functions for dom.
@@ -118,9 +118,9 @@ export class CoreDom {
return true;
}
- CoreTemplateElement.innerHTML = content;
+ const element = convertTextToHTMLElement(content);
- return !CoreDom.elementHasContent(CoreTemplateElement.content);
+ return !CoreDom.elementHasContent(element);
}
/**
diff --git a/src/core/utils/create-html-element.ts b/src/core/utils/create-html-element.ts
index 60f98e5c2..076d6bbc4 100644
--- a/src/core/utils/create-html-element.ts
+++ b/src/core/utils/create-html-element.ts
@@ -12,18 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-// A template element to convert HTML to element.
-export const CoreTemplateElement: HTMLTemplateElement = document.createElement('template');
-
/**
- * Convert some HTML as text into an HTMLElement. This HTML is put inside a div or a body.
+ * Convert some HTML as text into an HTMLElement. This HTML is put inside a div.
*
* @param html Text to convert.
* @returns Element.
*/
export function convertTextToHTMLElement(html: string): HTMLElement {
- // Add a div to hold the content, that's the element that will be returned.
- CoreTemplateElement.innerHTML = '
' + html + '
';
+ const element = document.createElement('template');
- return CoreTemplateElement.content.children[0];
+ // Add a div to hold the content, that's the element that will be returned.
+ element.innerHTML = '