Merge pull request #3717 from crazyserver/MOBILE-4309
MOBILE-4309 format-text: Treat font awesome tags to be renderedmain
commit
8e413bad8e
|
@ -14,12 +14,10 @@
|
|||
|
||||
import { AfterViewInit, Directive, ElementRef, Input, OnChanges, SimpleChange } from '@angular/core';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { Http } from '@singletons';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
import { CoreIcons } from '@singletons/icons';
|
||||
|
||||
/**
|
||||
* Directive to enable font-awesome 6.3 as ionicons.
|
||||
* Directive to enable font-awesome 6.4 as ionicons.
|
||||
* Check available icons at https://fontawesome.com/search?o=r&m=free
|
||||
*
|
||||
* Example usage:
|
||||
|
@ -31,13 +29,6 @@ import { CorePromisedValue } from '@classes/promised-value';
|
|||
})
|
||||
export class CoreFaIconDirective implements AfterViewInit, OnChanges {
|
||||
|
||||
/**
|
||||
* Object used to store whether icons exist or not during development.
|
||||
*/
|
||||
private static readonly DEV_ICONS_STATUS: Record<string, Promise<boolean>> = {};
|
||||
|
||||
protected static aliases?: CorePromisedValue<Record<string, string>>;
|
||||
|
||||
@Input() name = '';
|
||||
|
||||
protected element: HTMLElement;
|
||||
|
@ -95,19 +86,19 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
|
|||
iconName = iconName.substring(parts[0].length + 1);
|
||||
|
||||
// Set it here to avoid loading unexisting icon paths (svg/iconName) caused by the tick delay of the checkIconAlias promise.
|
||||
let src = `assets/fonts/${font}/${library}/${iconName}.svg`;
|
||||
let src = CoreIcons.getIconSrc(font, library, iconName);
|
||||
this.element.setAttribute('src', src);
|
||||
|
||||
if (font === 'font-awesome') {
|
||||
const iconNameChecked = await this.checkIconAlias(iconName);
|
||||
if (iconNameChecked !== iconName) {
|
||||
src = `assets/fonts/${font}/${library}/${iconName}.svg`;
|
||||
const { fileName } = await CoreIcons.getFontAwesomeIconFileName(iconName);
|
||||
if (fileName !== iconName) {
|
||||
src = CoreIcons.getIconSrc(font, library, fileName);
|
||||
this.element.setAttribute('src', src);
|
||||
}
|
||||
}
|
||||
|
||||
this.element.classList.add('faicon');
|
||||
this.validateIcon(this.name, src);
|
||||
CoreIcons.validateIcon(this.name, src);
|
||||
|
||||
}
|
||||
|
||||
|
@ -135,77 +126,4 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
|
|||
this.setIcon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check icon alias and returns the new icon name.
|
||||
*
|
||||
* @param iconName Icon name.
|
||||
* @returns New icon name.
|
||||
*/
|
||||
protected async checkIconAlias(iconName: string): Promise<string> {
|
||||
const aliases = await CoreFaIconDirective.getIconsAliases();
|
||||
|
||||
if (aliases[iconName]) {
|
||||
this.logger.error(`Icon ${iconName} is an alias of ${aliases[iconName]}, please use the new name.`);
|
||||
|
||||
return aliases[iconName];
|
||||
}
|
||||
|
||||
return iconName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the icon aliases json file.
|
||||
*
|
||||
* @returns Promise resolved when loaded.
|
||||
*/
|
||||
protected static async getIconsAliases(): Promise<Record<string, string>> {
|
||||
if (CoreFaIconDirective.aliases !== undefined) {
|
||||
return CoreFaIconDirective.aliases;
|
||||
}
|
||||
|
||||
CoreFaIconDirective.aliases = new CorePromisedValue();
|
||||
|
||||
try {
|
||||
const aliases = await Http.get<Record<string, string>>('assets/fonts/font-awesome/aliases.json', {
|
||||
responseType: 'json',
|
||||
}).toPromise();
|
||||
|
||||
CoreFaIconDirective.aliases.resolve(aliases);
|
||||
|
||||
return aliases;
|
||||
} catch {
|
||||
CoreFaIconDirective.aliases.resolve({});
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an icon exists, or show warning otherwise (only in development and testing environments).
|
||||
*
|
||||
* @param name Icon name.
|
||||
* @param src Icon source url.
|
||||
*/
|
||||
private validateIcon(name: string, src: string): void {
|
||||
if (!CoreConstants.BUILD.isDevelopment && !CoreConstants.BUILD.isTesting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(src in CoreFaIconDirective.DEV_ICONS_STATUS)) {
|
||||
CoreFaIconDirective.DEV_ICONS_STATUS[src] = Http.get(src, { responseType: 'text' })
|
||||
.toPromise()
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
CoreFaIconDirective.DEV_ICONS_STATUS[src].then(exists => {
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.logger.error(`Icon ${name} not found`);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ import { ElementController } from '@classes/element-controllers/ElementControlle
|
|||
import { MediaElementController } from '@classes/element-controllers/MediaElementController';
|
||||
import { FrameElementController } from '@classes/element-controllers/FrameElementController';
|
||||
import { CoreUrl } from '@singletons/url';
|
||||
import { CoreIcons } from '@singletons/icons';
|
||||
|
||||
/**
|
||||
* Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
|
||||
|
@ -278,10 +279,10 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
|
|||
button.classList.add('core-image-viewer-icon');
|
||||
button.classList.add('hidden');
|
||||
button.setAttribute('aria-label', label);
|
||||
const iconName = 'up-right-and-down-left-from-center';
|
||||
const src = CoreIcons.getIconSrc('font-awesome', 'solid', iconName);
|
||||
// Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed.
|
||||
button.innerHTML = '<ion-icon name="fas-up-right-and-down-left-from-center" aria-hidden="true" \
|
||||
src="assets/fonts/font-awesome/solid/up-right-and-down-left-from-center.svg">\
|
||||
</ion-icon>';
|
||||
button.innerHTML = `<ion-icon name="fas-${iconName}" aria-hidden="true" src="${src}"></ion-icon>`;
|
||||
|
||||
button.addEventListener('click', (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
@ -478,6 +479,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
|
|||
const videos = Array.from(div.querySelectorAll('video'));
|
||||
const iframes = Array.from(div.querySelectorAll('iframe'));
|
||||
const buttons = Array.from(div.querySelectorAll('.button'));
|
||||
const icons = Array.from(div.querySelectorAll('i.fa,i.fas,i.far,i.fab'));
|
||||
const elementsWithInlineStyles = Array.from(div.querySelectorAll('*[style]'));
|
||||
const stopClicksElements = Array.from(div.querySelectorAll('button,input,select,textarea'));
|
||||
const frames = Array.from(div.querySelectorAll(CoreIframeUtilsProvider.FRAME_TAGS.join(',').replace(/iframe,?/, '')));
|
||||
|
@ -550,6 +552,11 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
|
|||
}
|
||||
});
|
||||
|
||||
// Handle Font Awesome icons to be rendered by the app.
|
||||
icons.forEach((icon) => {
|
||||
CoreIcons.replaceCSSIcon(icon);
|
||||
});
|
||||
|
||||
// Handle inline styles.
|
||||
elementsWithInlineStyles.forEach((el: HTMLElement) => {
|
||||
// Only add external content for tags that haven't been treated already.
|
||||
|
|
|
@ -26,6 +26,7 @@ import { CoreWSFile } from '@services/ws';
|
|||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreQuestion, CoreQuestionProvider, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from './question';
|
||||
import { CoreQuestionDelegate } from './question-delegate';
|
||||
import { CoreIcons } from '@singletons/icons';
|
||||
|
||||
/**
|
||||
* Service with some common functions to handle questions.
|
||||
|
@ -801,12 +802,14 @@ export class CoreQuestionHelperProvider {
|
|||
const newIcon: HTMLIonIconElement = document.createElement('ion-icon');
|
||||
|
||||
if (correct) {
|
||||
newIcon.setAttribute('name', 'fas-check');
|
||||
newIcon.setAttribute('src', 'assets/fonts/font-awesome/solid/check.svg');
|
||||
const iconName = 'check';
|
||||
newIcon.setAttribute('name', `fas-${iconName}`);
|
||||
newIcon.setAttribute('src', CoreIcons.getIconSrc('font-awesome', 'solid', iconName));
|
||||
newIcon.className = 'core-correct-icon ion-color ion-color-success questioncorrectnessicon';
|
||||
} else {
|
||||
newIcon.setAttribute('name', 'fas-xmark');
|
||||
newIcon.setAttribute('src', 'assets/fonts/font-awesome/solid/xmark.svg');
|
||||
const iconName = 'xmark';
|
||||
newIcon.setAttribute('name', `fas-${iconName}`);
|
||||
newIcon.setAttribute('src', CoreIcons.getIconSrc('font-awesome', 'solid', iconName));
|
||||
newIcon.className = 'core-correct-icon ion-color ion-color-danger questioncorrectnessicon';
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
// (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.
|
||||
|
||||
import { Http } from '@singletons';
|
||||
import { CoreConstants } from '../constants';
|
||||
import { CoreLogger } from './logger';
|
||||
import aliases from '@/assets/fonts/font-awesome/aliases.json';
|
||||
|
||||
/**
|
||||
* Singleton with helper functions for icon management.
|
||||
*/
|
||||
export class CoreIcons {
|
||||
|
||||
/**
|
||||
* Object used to store whether icons exist or not during development.
|
||||
*/
|
||||
private static readonly DEV_ICONS_STATUS: Record<string, Promise<boolean>> = {};
|
||||
|
||||
private static readonly ALIASES = { ...aliases } as unknown as Record<string, string>;
|
||||
|
||||
protected static logger = CoreLogger.getInstance('CoreIcons');
|
||||
|
||||
/**
|
||||
* Check icon alias and returns the new icon name.
|
||||
*
|
||||
* @param icon Icon name.
|
||||
* @returns New icon name and new library if changed.
|
||||
*/
|
||||
static async getFontAwesomeIconFileName(icon: string): Promise<{fileName: string; newLibrary?: string}> {
|
||||
let newLibrary: string | undefined = undefined;
|
||||
if (icon.endsWith('-o')) {
|
||||
newLibrary = 'regular';
|
||||
icon = icon.substring(0, icon.length - 2);
|
||||
}
|
||||
|
||||
if (CoreIcons.ALIASES[icon]) {
|
||||
this.logger.error(`Icon ${icon} is an alias of ${CoreIcons.ALIASES[icon]}, please use the new name.`);
|
||||
|
||||
return { newLibrary, fileName: CoreIcons.ALIASES[icon] };
|
||||
}
|
||||
|
||||
return { newLibrary, fileName: icon };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an icon exists, or show warning otherwise (only in development and testing environments).
|
||||
*
|
||||
* @param name Icon name.
|
||||
* @param src Icon source url.
|
||||
*/
|
||||
static validateIcon(name: string, src: string): void {
|
||||
if (!CoreConstants.BUILD.isDevelopment && !CoreConstants.BUILD.isTesting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(src in CoreIcons.DEV_ICONS_STATUS)) {
|
||||
CoreIcons.DEV_ICONS_STATUS[src] = Http.get(src, { responseType: 'text' })
|
||||
.toPromise()
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
CoreIcons.DEV_ICONS_STATUS[src].then(exists => {
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.logger.error(`Icon ${name} not found`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an <i> icon that uses CSS by a ion-icon with SVG.
|
||||
* It supports from 4.7 to 6.4 Font awesome versions.
|
||||
* But it can fail on 4.7 and 5.x because of the lack of assets.
|
||||
*
|
||||
* @param icon Current icon element.
|
||||
* @returns New icon, already included in the DOM.
|
||||
*/
|
||||
static async replaceCSSIcon(icon: Element): Promise<HTMLIonIconElement | undefined> {
|
||||
let library = 'solid';
|
||||
let iconName = '';
|
||||
|
||||
Array.from(icon.classList).forEach(async (className) => {
|
||||
// Library name of 5.x
|
||||
switch (className) {
|
||||
case 'fas':
|
||||
library = 'solid';
|
||||
|
||||
return;
|
||||
case 'far':
|
||||
library = 'regular';
|
||||
|
||||
return;
|
||||
case 'fab':
|
||||
library = 'brands';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check fa- style class names.
|
||||
const faPart = className.match(/fa-([a-zA-Z0-9-]+)/);
|
||||
if (!faPart) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstPart = faPart[1].split('-')[0];
|
||||
|
||||
switch (firstPart) {
|
||||
// Class is defining library.
|
||||
case 'solid':
|
||||
library = 'solid';
|
||||
break;
|
||||
case 'regular':
|
||||
case 'light':
|
||||
library = 'regular';
|
||||
break;
|
||||
case 'brands':
|
||||
library = 'brands';
|
||||
break;
|
||||
// Class is defining special cases.
|
||||
case '2xs':
|
||||
case 'xs':
|
||||
case 'sm':
|
||||
case 'lg':
|
||||
case 'xl':
|
||||
case '2xl':
|
||||
case 'fw':
|
||||
case 'sharp':
|
||||
case 'rotate':
|
||||
return;
|
||||
// Class is defining the icon name (fa-ICONNAME).
|
||||
default:
|
||||
iconName = faPart[1];
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (!iconName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newIcon = document.createElement('ion-icon');
|
||||
|
||||
Array.from(icon.attributes).forEach(attr => {
|
||||
newIcon.setAttribute(attr.nodeName, attr.nodeValue || '');
|
||||
});
|
||||
|
||||
if (!newIcon.getAttribute('aria-label') &&
|
||||
!newIcon.getAttribute('aria-labelledby') &&
|
||||
!newIcon.getAttribute('title')) {
|
||||
newIcon.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
const { fileName, newLibrary } = await CoreIcons.getFontAwesomeIconFileName(iconName);
|
||||
if (newLibrary) {
|
||||
library = newLibrary;
|
||||
}
|
||||
iconName = fileName;
|
||||
|
||||
const src = CoreIcons.getIconSrc('font-awesome', library, iconName);
|
||||
|
||||
newIcon.setAttribute('src', src);
|
||||
|
||||
newIcon.classList.add('faicon');
|
||||
CoreIcons.validateIcon(iconName, src);
|
||||
|
||||
icon.parentElement?.insertBefore(newIcon, icon);
|
||||
icon.remove();
|
||||
|
||||
return newIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon SVG path.
|
||||
*
|
||||
* @param font Font Family.
|
||||
* @param library Library to use.
|
||||
* @param icon Icon Name.
|
||||
* @returns Path.
|
||||
*/
|
||||
static getIconSrc(font: string, library: string, icon: string): string {
|
||||
return `assets/fonts/${font}/${library}/${icon}.svg`;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// (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.
|
||||
|
||||
import { CoreIcons } from '@singletons/icons';
|
||||
|
||||
describe('CoreIcons singleton', () => {
|
||||
|
||||
it('replaces CSS icon with the correspondant ion-icon', async () => {
|
||||
const icon = document.createElement('i');
|
||||
|
||||
// Not an icon
|
||||
icon.className = 'test';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon)))
|
||||
.toEqual(undefined);
|
||||
|
||||
icon.className = 'fas fanoicon';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon)))
|
||||
.toEqual(undefined);
|
||||
|
||||
icon.className = 'fa-solid fanoicon';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon)))
|
||||
.toEqual(undefined);
|
||||
|
||||
// Font awesome 6
|
||||
icon.className = 'fa-solid fa-face-awesome';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/solid/face-awesome.svg');
|
||||
|
||||
icon.className = 'fa-regular fa-face-awesome';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/regular/face-awesome.svg');
|
||||
|
||||
icon.className = 'fa-light fa-face-awesome';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/regular/face-awesome.svg');
|
||||
|
||||
icon.className = 'fa-brands fa-facebook';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/brands/facebook.svg');
|
||||
|
||||
// Font awesome 5
|
||||
icon.className = 'fas fa-yin-yang';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/solid/yin-yang.svg');
|
||||
|
||||
icon.className = 'far fa-wrench';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/regular/wrench.svg');
|
||||
|
||||
icon.className = 'fab fa-youtube';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/brands/youtube.svg');
|
||||
|
||||
// Font awesome 4.7
|
||||
icon.className = 'fa fa-address-book';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/solid/address-book.svg');
|
||||
|
||||
icon.className = 'fa fa-address-book-o';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/regular/address-book.svg');
|
||||
|
||||
// Aliases
|
||||
icon.className = 'fas fa-battery-5';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/solid/battery-full.svg');
|
||||
|
||||
icon.className = 'fa fa-check-square';
|
||||
expect((await CoreIcons.replaceCSSIcon(icon))?.getAttribute('src'))
|
||||
.toEqual('assets/fonts/font-awesome/solid/square-check.svg');
|
||||
});
|
||||
|
||||
});
|
|
@ -4,6 +4,7 @@ information provided here is intended especially for developers.
|
|||
=== 4.3.0 ===
|
||||
|
||||
- CoreSiteBasicInfo fullName attribute has changed to fullname and avatar to userpictureurl to match user fields.
|
||||
- Font Awesome icon library has been updated to 6.4.0. But nothing has changed, only version number.
|
||||
|
||||
=== 4.2.0 ===
|
||||
|
||||
|
|
Loading…
Reference in New Issue