Merge pull request #4079 from crazyserver/MOBILE-4470

MOBILE-4470 icon: Revert load SVG directly and use img and filters
main
Noel De Martin 2024-06-04 10:52:05 +02:00 committed by GitHub
commit ef3b6d65c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 22 additions and 175 deletions

View File

@ -1,6 +1,3 @@
<ng-container *ngIf="loaded() && !svgIcon()">
<img *ngIf="!isLocalUrl()" [url]="iconUrl()" core-external-content alt="" [component]="linkIconWithComponent() ? modname : null"
[componentId]="linkIconWithComponent() ? componentId : null" (error)="loadFallbackIcon()">
<img *ngIf="isLocalUrl()" [src]="iconUrl()" (error)="loadFallbackIcon()" alt="">
</ng-container>
<div *ngIf="svgIcon()" [innerHTML]="svgIcon()"></div>
<img *ngIf="!isLocalUrl()" [url]="iconUrl()" core-external-content alt="" [component]="linkIconWithComponent() ? modname : null"
[componentId]="linkIconWithComponent() ? componentId : null" (error)="loadFallbackIcon()">
<img *ngIf="isLocalUrl()" [src]="iconUrl()" (error)="loadFallbackIcon()" alt="">

View File

@ -23,13 +23,13 @@
background-color: transparent;
line-height: var(--size);
--color: var(--activity-base-icon-color);
&.colorize {
&.version_current {
@each $type, $value in $activity-icon-colors {
&.#{$type} {
--color: var(--activity#{$type});
@each $type, $value in $activity-icon-color-filters {
&.#{$type}:not(.branded) {
img {
filter: var(--activity#{$type});
}
}
}
}
@ -40,7 +40,7 @@
@each $type, $value in $activity-icon-background-colors {
&.#{$type}:not(.branded) {
background-color: var(--activity-40-#{$type});
::ng-deep svg, img {
img {
filter: brightness(0) invert(1);
}
}
@ -57,15 +57,7 @@
--padding-bottom: var(--module-legacy-icon-padding, 8px);
}
&.colorize.version_current:not(.branded) {
::ng-deep svg,
::ng-deep svg * {
fill: var(--color) !important;
}
}
img,
::ng-deep svg {
img {
width: var(--size);
height: var(--size);
max-width: var(--size);

View File

@ -24,17 +24,11 @@ import {
SimpleChange,
signal,
} from '@angular/core';
import { SafeHtml } from '@angular/platform-browser';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreFile } from '@services/file';
import { CoreFileHelper } from '@services/file-helper';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { DomSanitizer, Http } from '@singletons';
import { firstValueFrom } from 'rxjs';
const assetsPath = 'assets/img/';
const fallbackModName = 'external-tool';
@ -80,9 +74,7 @@ export class CoreModIconComponent implements OnInit, OnChanges {
iconUrl = signal('');
modNameTranslated = signal('');
isLocalUrl = signal(false);
svgIcon = signal<SafeHtml>('');
linkIconWithComponent = signal(false);
loaded = signal(false);
protected iconVersion: IconVersion = IconVersion.LEGACY_VERSION;
protected purposeClass = '';
@ -208,8 +200,6 @@ export class CoreModIconComponent implements OnInit, OnChanges {
);
this.setBrandedClass();
await this.setSVGIcon();
}
/**
@ -230,8 +220,6 @@ export class CoreModIconComponent implements OnInit, OnChanges {
const path = CoreCourse.getModuleIconsPath();
this.iconUrl.set(path + moduleName + '.svg');
await this.setSVGIcon();
}
/**
@ -306,141 +294,4 @@ export class CoreModIconComponent implements OnInit, OnChanges {
return IconVersion.CURRENT_VERSION;
}
/**
* Sets SVG markup for the icon (if the URL is an SVG).
*
* @returns Promise resolved when done.
*/
protected async setSVGIcon(): Promise<void> {
if (this.iconVersion === IconVersion.LEGACY_VERSION) {
this.loaded.set(true);
this.svgIcon.set('');
return;
}
this.loaded.set(false);
let mimetype = '';
let fileContents = '';
// Download the icon if it's not local to cache it.
if (!this.isLocalUrl()) {
try {
const iconUrl = await CoreFileHelper.downloadFile(
this.iconUrl(),
this.linkIconWithComponent() ? this.modname : undefined,
this.linkIconWithComponent() ? this.componentId : undefined,
);
if (iconUrl) {
mimetype = await CoreUtils.getMimeTypeFromUrl(iconUrl);
fileContents = await CoreFile.readFile(iconUrl);
}
} catch {
// Ignore errors.
}
}
try {
if (!fileContents) {
// Try to download the icon directly (also for local files).
const response = await firstValueFrom(Http.get(
this.iconUrl(),
{
observe: 'response',
responseType: 'text',
},
));
mimetype = response.headers.get('content-type') || mimetype;
fileContents = response.body || '';
}
if (mimetype !== 'image/svg+xml' || !fileContents) {
this.svgIcon.set('');
return;
}
// Clean the DOM to avoid security issues.
const parser = new DOMParser();
const doc = parser.parseFromString(fileContents, 'image/svg+xml');
// Safety check.
if (doc.documentElement.nodeName !== 'svg') {
this.svgIcon.set('');
return;
}
// Remove scripts tags.
const scripts = doc.documentElement.getElementsByTagName('script');
for (let i = scripts.length - 1; i >= 0; i--) {
scripts[i].parentNode?.removeChild(scripts[i]);
}
// Has own styles, do not apply colors.
if (doc.documentElement.getElementsByTagName('style').length > 0) {
this.brandedClass = true;
}
// Recursively remove attributes starting with on.
const removeAttributes = (element: Element): void => {
Array.from(element.attributes).forEach((attr) => {
if (attr.name.startsWith('on')) {
element.removeAttribute(attr.name);
}
});
Array.from(element.children).forEach((child) => {
removeAttributes(child);
});
};
removeAttributes(doc.documentElement);
// Add viewBox to avoid scaling issues.
if (!doc.documentElement.getAttribute('viewBox')) {
const width = doc.documentElement.getAttribute('width');
const height = doc.documentElement.getAttribute('height');
if (width && height) {
doc.documentElement.setAttribute('viewBox', '0 0 '+ width + ' ' + height);
}
}
// Prefix id's on svg DOM to avoid conflicts.
const uniqueId = 'modicon' + CoreUtils.getUniqueId('modicon') + '_';
const styleTags = Array.from(doc.documentElement.getElementsByTagName('style'));
const styleAttrs = Array.from(doc.documentElement.querySelectorAll('[style]'));
const idTags = Array.from(doc.documentElement.querySelectorAll('[id]'));
idTags.forEach((element) => {
if (!element.id) {
return;
}
const newId = uniqueId + element.id;
// Regexp to replace all ocurrences of the id with workd bondaries.
const oldIdFinder = new RegExp(`#${element.id}\\b`, 'g');
element.id = newId;
// Prefix the elementId on style Tags.
styleTags.forEach((style) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
style.textContent = style.textContent!.replace(oldIdFinder, `#${newId}`);
});
// Also change ids on style attributes.
styleAttrs.forEach((attr) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
attr.setAttribute('style', attr.getAttribute('style')!.replace(oldIdFinder, `#${newId}`));
});
});
this.svgIcon.set(DomSanitizer.bypassSecurityTrustHtml(doc.documentElement.outerHTML));
} catch {
this.svgIcon.set('');
} finally {
this.loaded.set(true);
}
}
}

View File

@ -5,7 +5,7 @@
</div>
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control"
[placeholder]="placeholder" [attr.aria-labelledby]="ariaLabelledBy" (ionChange)="onChange()" (ionFocus)="focusRTE($event)"
[placeholder]="placeholder" [aria-labelledby]="ariaLabelledBy" (ionChange)="onChange()" (ionFocus)="focusRTE($event)"
(ionBlur)="blurRTE($event)" />
<div class="core-rte-info-message" *ngIf="infoMessage">

View File

@ -23,7 +23,7 @@
<core-sites-list [accountsList]="accountsList" [sitesClickable]="true" [currentSiteClickable]="false"
(onSiteClicked)="login($event)">
<ng-template #siteItem let-site="site" let-isCurrentSite="isCurrentSite">
<ion-icon *ngIf="isCurrentSite" color="success" name="fas-check" />
<ion-icon *ngIf="isCurrentSite" color="success" name="fas-check" aria-hidden="true" />
<ng-container *ngIf="!isCurrentSite">
<ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation>

View File

@ -136,6 +136,15 @@ $activity-icon-colors: (
interactivecontent: #8d3d1b
) !default;
$activity-icon-color-filters: (
administration: brightness(0%) invert(45%) sepia(46%) saturate(3819%) hue-rotate(260deg) brightness(101%) contrast(87%),
assessment: brightness(0%) invert(36%) sepia(98%) saturate(6969%) hue-rotate(315deg) brightness(90%) contrast(119%),
collaboration: brightness(0%) invert(25%) sepia(54%) saturate(6226%) hue-rotate(245deg) brightness(100%) contrast(102%),
communication: brightness(0%) invert(48%) sepia(74%) saturate(4887%) hue-rotate(11deg) brightness(102%) contrast(101%),
content: brightness(0%) invert(49%) sepia(52%) saturate(4675%) hue-rotate(156deg) brightness(89%) contrast(102%),
interactivecontent: brightness(0%) invert(25%) sepia(63%) saturate(1152%) hue-rotate(344deg) brightness(94%) contrast(91%)
) !default;
$calendar-event-category-category: #8e24aa !default;
$calendar-event-category-course: $red !default;
$calendar-event-category-group: $yellow !default;

View File

@ -172,5 +172,4 @@ html.dark {
--core-table-even-cell-background: var(--gray-900);
--core-table-even-cell-hover: var(--gray-700);
--activity-base-icon-color: var(--white);
}

View File

@ -201,9 +201,8 @@ html {
--core-dd-question-color-#{$i + 1}-contrast: #{get_contrast_color(nth($core-dd-question-colors, $i + 1))};
}
--activity-base-icon-color: var(--gray-900);
// Make activtity colours available for custom modules.
@each $type, $value in $activity-icon-colors {
@each $type, $value in $activity-icon-color-filters {
--activity#{$type}: #{$value};
}