MOBILE-4456 course: Load icon svg as inline and style them properly
|
@ -1,7 +1,7 @@
|
|||
:host {
|
||||
core-mod-icon {
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
--filter: var(--module-icon-filter);
|
||||
--padding-start: 0px;
|
||||
--module-icon-size: 24px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let entry of entries" [detail]="true" button (click)="gotoCoureListModType(entry)">
|
||||
<core-mod-icon slot="start" [modicon]="entry.icon" [modname]="entry.iconModName" [showAlt]="false" />
|
||||
<core-mod-icon slot="start" [modicon]="entry.icon" [modname]="entry.iconModName" [showAlt]="false" [colorize]="false" />
|
||||
<ion-label>{{ entry.name }}</ion-label>
|
||||
</ion-item>
|
||||
</core-loading>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<ion-card>
|
||||
<ion-item class="core-course-module-handler ion-text-wrap" [detail]="false" (click)="action($event, item)" button>
|
||||
<core-mod-icon slot="start" *ngIf="item.iconUrl" [modicon]="item.iconUrl" [modname]="item.modname"
|
||||
[componentId]="item.cmid" [showAlt]="false" [purpose]="item.purpose" />
|
||||
[componentId]="item.cmid" [showAlt]="false" [purpose]="item.purpose" [isBranded]="item.branded" />
|
||||
<ion-label>
|
||||
<!-- Add the icon title so accessibility tools read it. -->
|
||||
<span class="sr-only" *ngIf="item.iconTitle">{{ item.iconTitle }}</span>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<ion-col class="addon-block-timeline-activity-time ion-no-padding ion-text-nowrap">
|
||||
<small>{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</small>
|
||||
<core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance"
|
||||
[modname]="event.modulename" [purpose]="event.purpose" />
|
||||
[modname]="event.modulename" [purpose]="event.purpose" [isBranded]="event.branded" />
|
||||
</ion-col>
|
||||
<ion-col class="addon-block-timeline-activity-name ion-no-padding">
|
||||
<p class="item-heading">
|
||||
|
|
|
@ -23,7 +23,7 @@ h4.core-bold {
|
|||
}
|
||||
|
||||
core-mod-icon {
|
||||
--padding: 8px;
|
||||
--module-legacy-icon-padding: 8px;
|
||||
--margin-end: 0.5rem;
|
||||
--margin-vertical: 0;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<ion-item class="ion-text-wrap addon-calendar-event" [attr.aria-label]="event.name" (click)="eventClicked(event)" button
|
||||
[ngClass]="['addon-calendar-eventtype-'+event.eventtype]" [detail]="false">
|
||||
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [modname]="event.modulename"
|
||||
[componentId]="event.instance" [showAlt]="false" [purpose]="event.purpose" />
|
||||
[componentId]="event.instance" [showAlt]="false" [purpose]="event.purpose" [isBranded]="event.branded" />
|
||||
<ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<!-- Add the icon title so accessibility tools read it. -->
|
||||
|
|
|
@ -67,7 +67,8 @@
|
|||
(click)="gotoEvent(event.id, day)" [class.item-dimmed]="event.ispast"
|
||||
[ngClass]="['addon-calendar-eventtype-'+event.eventtype]" button [detail]="false">
|
||||
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [showAlt]="false"
|
||||
[modname]="event.modulename" [componentId]="event.instance" [purpose]="event.purpose" />
|
||||
[modname]="event.modulename" [componentId]="event.instance" [purpose]="event.purpose"
|
||||
[isBranded]="event.branded" />
|
||||
<ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start"
|
||||
aria-hidden="true" />
|
||||
<ion-label>
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
<ion-item class="ion-text-wrap addon-calendar-event" collapsible [ngClass]="['addon-calendar-eventtype-'+event.eventtype]">
|
||||
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false" [modname]="event.modulename"
|
||||
[componentId]="event.instance" slot="start" [purpose]="event.purpose" />
|
||||
<!-- TODO MOBILE-4530 Add isBranded when available -->
|
||||
<ion-icon *ngIf=" event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" aria-hidden="true" slot="start" />
|
||||
<ion-label>
|
||||
<!-- Add the icon title so accessibility tools read it. -->
|
||||
|
|
|
@ -84,7 +84,8 @@
|
|||
</p>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let activity of coursemodules" [href]="activity.url"
|
||||
[attr.aria-label]="activity.name" core-link capture="true">
|
||||
<core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl" />
|
||||
<core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl"
|
||||
[colorize]="false" />
|
||||
<ion-label>
|
||||
<core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id"
|
||||
[courseId]="courseId" />
|
||||
|
|
|
@ -119,7 +119,8 @@
|
|||
</p>
|
||||
<ion-item class="ion-text-wrap core-course-module-handler" [attr.aria-label]="activity.name" core-link
|
||||
*ngFor="let activity of competency.coursemodules" [href]="activity.url" capture="true">
|
||||
<core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl" />
|
||||
<core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl"
|
||||
[colorize]="false" />
|
||||
<ion-label>
|
||||
<core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id"
|
||||
[courseId]="courseId" />
|
||||
|
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
@ -1,17 +1,17 @@
|
|||
@use "theme/globals" as *;
|
||||
|
||||
:host {
|
||||
--extra-icon-size: 16px;
|
||||
--icon-size: 24px;
|
||||
--icon-size: 32px;
|
||||
--core-avatar-size: var(--icon-size);
|
||||
--extra-icon-size: 12px;
|
||||
|
||||
::ng-deep core-user-avatar {
|
||||
.core-avatar-extra-img,
|
||||
core-mod-icon {
|
||||
.core-avatar-extra-img {
|
||||
margin: 0 !important;
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
--padding: 0.2rem;
|
||||
--module-icon-padding: 0.2rem;
|
||||
}
|
||||
|
||||
.core-avatar-extra-img {
|
||||
|
@ -20,16 +20,16 @@
|
|||
img {
|
||||
max-width: var(--extra-icon-size);
|
||||
max-height: var(--extra-icon-size);
|
||||
width: var(--extra-icon-size);
|
||||
height: var(--extra-icon-size);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
core-mod-icon {
|
||||
--size: var(--extra-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
div.core-notification-icon {
|
||||
max-width: var(--icon-size);
|
||||
max-height: var(--icon-size);
|
||||
img {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
|
@ -37,20 +37,17 @@
|
|||
ion-icon {
|
||||
font-size: var(--icon-size);
|
||||
}
|
||||
padding: 0.7rem;
|
||||
padding: 0px;
|
||||
background: var(--background-color);
|
||||
border-radius: var(--radius-xs);
|
||||
}
|
||||
|
||||
.core-notification-icon {
|
||||
--module-icon-size: var(--icon-size);
|
||||
@include margin(6px, 8px, 6px, 0px);
|
||||
}
|
||||
|
||||
.core-notification-img {
|
||||
@include margin(6px, 8px, 6px, 0px);
|
||||
width: var(--core-avatar-size);
|
||||
height: var(--core-avatar-size);
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
object-fit: cover;
|
||||
border-radius: var(--core-avatar-radius);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
[profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"
|
||||
[userId]="notification.useridfrom">
|
||||
<div class="core-avatar-extra-img" *ngIf="notification.iconurl">
|
||||
<img [src]="notification.iconurl" alt="" role="presentation">
|
||||
<img [src]="notification.iconurl" alt="" role="presentation" core-external-content>
|
||||
</div>
|
||||
</core-user-avatar>
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@use "theme/globals" as *;
|
||||
|
||||
ion-item.addon-notification-item {
|
||||
|
||||
ion-label {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
@ -32,17 +33,6 @@ ion-item.addon-notification-item {
|
|||
align-self: start;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
--icon-size: 16px;
|
||||
--extra-icon-size: 12px;
|
||||
--core-avatar-size: 32px;
|
||||
|
||||
div.core-notification-icon,
|
||||
core-mod-icon.core-notification-icon {
|
||||
--padding: 8px;
|
||||
max-width: var(--core-avatar-size);
|
||||
max-height: var(--core-avatar-size);
|
||||
}
|
||||
}
|
||||
|
||||
.mark-all-as-read {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<core-user-avatar *ngIf="notification.useridfrom > 0" slot="start" [userId]="notification.useridfrom"
|
||||
[profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname">
|
||||
<div class="core-avatar-extra-img" *ngIf="notification.iconurl">
|
||||
<img [src]="notification.iconurl" alt="" role="presentation">
|
||||
<img [src]="notification.iconurl" alt="" role="presentation" core-external-content>
|
||||
</div>
|
||||
</core-user-avatar>
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
@use "theme/globals" as *;
|
||||
|
||||
:host {
|
||||
--icon-size: 44px;
|
||||
--extra-icon-size: 16px;
|
||||
|
||||
.core-notification-title {
|
||||
[slot=start] {
|
||||
|
|
|
@ -100,7 +100,8 @@
|
|||
<ion-item class="core-course-storage-activity"
|
||||
*ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
|
||||
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
|
||||
[modname]="module.modname" [componentId]="module.instance" [fallbackTranslation]="module.modplural" />
|
||||
[modname]="module.modname" [componentId]="module.instance" [fallbackTranslation]="module.modplural"
|
||||
[isBranded]="module.branded" />
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p class="item-heading {{module.handlerData!.class}} addon-storagemanager-module-size">
|
||||
<core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
|
||||
|
|
|
@ -50,8 +50,9 @@ ion-badge {
|
|||
}
|
||||
|
||||
ion-item core-mod-icon {
|
||||
--size: 18px;
|
||||
--padding: 9px;
|
||||
--module-icon-size: 24px;
|
||||
--module-legacy-icon-size: 16px;
|
||||
|
||||
--margin-vertical: 8px;
|
||||
--margin-end: 8px;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<img *ngIf="!isLocalUrl && !iconSVG" [src]="icon" [alt]="showAlt ? modNameTranslated : ''" [attr.role]="!showAlt ? 'presentation' : null"
|
||||
class="core-module-icon" core-external-content [component]="linkIconWithComponent ? modname : null"
|
||||
<ng-container *ngIf="loaded">
|
||||
<img *ngIf="!isLocalUrl" [src]="iconUrl" core-external-content alt="" [component]="linkIconWithComponent ? modname : null"
|
||||
[componentId]="linkIconWithComponent ? componentId : null" (error)="loadFallbackIcon()">
|
||||
<img *ngIf="isLocalUrl && !iconSVG" [src]="icon" [alt]="showAlt ? modNameTranslated : ''" [attr.role]="!showAlt ? 'presentation' : null"
|
||||
class="core-module-icon" (error)="loadFallbackIcon()">
|
||||
<div *ngIf="iconSVG" class="core-module-icon" [innerHTML]="iconSVG" [attr.aria-label]="showAlt ? modNameTranslated : ''"
|
||||
[attr.role]="!showAlt ? 'presentation' : null"></div>
|
||||
<img *ngIf="isLocalUrl" [src]="iconUrl" (error)="loadFallbackIcon()" alt="">
|
||||
</ng-container>
|
||||
|
|
|
@ -3,58 +3,68 @@
|
|||
:host {
|
||||
display: inline-block;
|
||||
--size: var(--module-icon-size, 32px);
|
||||
--padding: var(--module-icon-padding, 8px);
|
||||
--padding-start: var(--module-icon-padding, 4px);
|
||||
--padding-top: var(--module-icon-padding, 4px);
|
||||
--padding-end: var(--module-icon-padding, 4px);
|
||||
--padding-bottom: var(--module-icon-padding, 4px);
|
||||
--icon-radius: var(--module-icon-radius, var(--radius-xs));
|
||||
--margin-end: 0px;
|
||||
--margin-vertical: 0px;
|
||||
|
||||
min-width: calc(var(--size) + var(--padding-start) + var(--padding-end));
|
||||
min-height: calc(var(--size) + var(--padding-top) + var(--padding-bottom));
|
||||
|
||||
margin-top: var(--margin-vertical);
|
||||
margin-bottom: var(--margin-vertical);
|
||||
@include margin-horizontal(null, var(--margin-end));
|
||||
|
||||
border-radius: var(--icon-radius);
|
||||
padding: var(--padding);
|
||||
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
|
||||
background-color: transparent;
|
||||
line-height: var(--size);
|
||||
|
||||
&.version_current {
|
||||
@each $type, $value in $activity-icon-color-filters {
|
||||
&.#{$type} {
|
||||
--filter: var(--activity#{$type});
|
||||
}
|
||||
}
|
||||
--color: var(--text-color);
|
||||
|
||||
img {
|
||||
filter: var(--filter, brightness(0) invert(1));
|
||||
&.colorize {
|
||||
&.version_current {
|
||||
@each $type, $value in $activity-icon-colors {
|
||||
&.#{$type} {
|
||||
--color: var(--activity#{$type});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.version_40 {
|
||||
--size: var(--module-icon-size, 24px);
|
||||
background-color: $gray-100;
|
||||
background-color: var(--gray-100);
|
||||
|
||||
--color: white;
|
||||
|
||||
@each $type, $value in $activity-icon-background-colors {
|
||||
&.#{$type} {
|
||||
background-color: var(--activity-40-#{$type});
|
||||
img {
|
||||
filter: var(--filter, brightness(0) invert(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.version_40,
|
||||
&.version_legacy {
|
||||
--size: var(--module-icon-size, 24px);
|
||||
--filter: none;
|
||||
background-color: $gray-100;
|
||||
--size: var(--module-legacy-icon-size, 24px);
|
||||
--padding-start: var(--module-legacy-icon-padding, 8px);
|
||||
--padding-top: var(--module-legacy-icon-padding, 8px);
|
||||
--padding-end: var(--module-legacy-icon-padding, 8px);
|
||||
--padding-bottom: var(--module-legacy-icon-padding, 8px);
|
||||
}
|
||||
|
||||
&.no-filter {
|
||||
--filter: none !important;
|
||||
&:not(.branded) {
|
||||
::ng-deep svg,
|
||||
::ng-deep svg * {
|
||||
fill: var(--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
img,
|
||||
::ng-deep svg {
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
max-width: var(--size);
|
||||
|
@ -62,11 +72,6 @@ img {
|
|||
min-width: var(--size);
|
||||
min-height: var(--size);
|
||||
vertical-align: top;
|
||||
|
||||
&[alt] {
|
||||
text-indent: -999999px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,9 +16,14 @@ import { CoreConstants, ModPurpose } from '@/core/constants';
|
|||
import { Component, ElementRef, HostBinding, Input, OnChanges, OnInit, SimpleChange } from '@angular/core';
|
||||
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 { Http } from '@singletons';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
const assetsPath = 'assets/img/';
|
||||
const fallbackModName = 'external-tool';
|
||||
|
@ -39,30 +44,46 @@ const enum IconVersion {
|
|||
})
|
||||
export class CoreModIconComponent implements OnInit, OnChanges {
|
||||
|
||||
@HostBinding('class.no-filter') noFilter = false;
|
||||
|
||||
@Input() modname = ''; // The module name. Used also as component if set.
|
||||
@Input() fallbackTranslation = ''; // Fallback translation string if cannot auto translate.
|
||||
@Input() componentId?: number; // Component Id for external icons.
|
||||
@Input() modicon?: string; // Module icon url or local url.
|
||||
@Input() showAlt = true; // Show alt otherwise it's only presentation icon.
|
||||
@Input() purpose: ModPurpose = ModPurpose.MOD_PURPOSE_OTHER; // Purpose of the module.
|
||||
@Input() @HostBinding('class.colorize') colorize = true; // Colorize the icon. Only applies on 4.0 onwards.
|
||||
@Input() @HostBinding('class.branded') isBranded?: boolean; // If icon is branded and no colorize will be applied.
|
||||
|
||||
icon = '';
|
||||
@HostBinding('attr.role')
|
||||
get getRole(): string | null {
|
||||
return !this.showAlt ? 'presentation' : null;
|
||||
}
|
||||
|
||||
@HostBinding('attr.aria-label')
|
||||
get getAriaLabel(): string {
|
||||
return this.showAlt ? this.modNameTranslated : '';
|
||||
}
|
||||
|
||||
iconUrl = '';
|
||||
|
||||
modNameTranslated = '';
|
||||
isLocalUrl = true;
|
||||
isLocalUrl = false;
|
||||
linkIconWithComponent = false;
|
||||
loaded = false;
|
||||
|
||||
@HostBinding('class') iconVersion = 'legacy';
|
||||
protected iconVersion: IconVersion = IconVersion.LEGACY_VERSION;
|
||||
protected purposeClass = '';
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(protected el: ElementRef) { }
|
||||
constructor(element: ElementRef) {
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.iconVersion = this.getIconVersion();
|
||||
this.element.classList.add(this.iconVersion);
|
||||
|
||||
if (!this.modname && this.modicon) {
|
||||
// Guess module from the icon url.
|
||||
|
@ -70,32 +91,9 @@ export class CoreModIconComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
this.modNameTranslated = CoreCourse.translateModuleName(this.modname, this.fallbackTranslation);
|
||||
if (this.iconVersion !== IconVersion.LEGACY_VERSION) {
|
||||
|
||||
let purposeClass =
|
||||
CoreCourseModuleDelegate.supportsFeature<ModPurpose>(
|
||||
this.modname || '',
|
||||
CoreConstants.FEATURE_MOD_PURPOSE,
|
||||
this.purpose,
|
||||
);
|
||||
|
||||
if (this.iconVersion === IconVersion.VERSION_4_0) {
|
||||
if (purposeClass === ModPurpose.MOD_PURPOSE_INTERACTIVECONTENT) {
|
||||
// Interactive content was introduced on 4.4, on previous versions CONTENT is used instead.
|
||||
purposeClass = ModPurpose.MOD_PURPOSE_CONTENT;
|
||||
}
|
||||
|
||||
if (this.modname === 'lti') {
|
||||
// LTI had content purpose with 4.0 icons.
|
||||
purposeClass = ModPurpose.MOD_PURPOSE_CONTENT;
|
||||
}
|
||||
}
|
||||
|
||||
if (purposeClass) {
|
||||
const element: HTMLElement = this.el.nativeElement;
|
||||
element.classList.add(purposeClass);
|
||||
}
|
||||
}
|
||||
this.setIsBranded();
|
||||
this.setPurposeClass();
|
||||
|
||||
await this.setIcon();
|
||||
}
|
||||
|
@ -109,12 +107,70 @@ export class CoreModIconComponent implements OnInit, OnChanges {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the isBranded property when undefined.
|
||||
*
|
||||
* @returns wether the icon does not need to be filtered.
|
||||
*/
|
||||
protected async setIsBranded(): Promise<void> {
|
||||
if (!this.colorize || this.isBranded !== undefined) {
|
||||
// It doesn't matter.
|
||||
return;
|
||||
}
|
||||
|
||||
// Earlier 4.0, icons were never colorized.
|
||||
if (this.iconVersion === IconVersion.LEGACY_VERSION) {
|
||||
this.colorize = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// No icon or local icon (not legacy), colorize it.
|
||||
if (!this.iconUrl || this.isLocalUrl) {
|
||||
this.isBranded = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.iconUrl = CoreTextUtils.decodeHTMLEntities(this.iconUrl);
|
||||
|
||||
// If it's an Moodle Theme icon, check if filtericon is set and use it.
|
||||
if (this.iconUrl && CoreUrlUtils.isThemeImageUrl(this.iconUrl)) {
|
||||
const iconParams = CoreUrlUtils.extractUrlParams(this.iconUrl);
|
||||
if (iconParams['filtericon'] === '1') {
|
||||
this.isBranded = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// filtericon was introduced in 4.2 and backported to 4.1.3 and 4.0.8.
|
||||
if (this.modname && !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan(['4.0.8', '4.1.3', '4.2'])) {
|
||||
// If version is prior to that, check if the url is a module icon and filter it.
|
||||
if (this.getComponentNameFromIconUrl(this.iconUrl) === this.modname) {
|
||||
this.isBranded = false;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External icons, or non monologo, do not filter.
|
||||
this.isBranded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set icon.
|
||||
*/
|
||||
async setIcon(): Promise<void> {
|
||||
this.icon = this.modicon || this.icon;
|
||||
this.isLocalUrl = this.icon.startsWith(assetsPath);
|
||||
this.iconUrl = this.modicon || this.iconUrl;
|
||||
|
||||
if (!this.iconUrl) {
|
||||
this.loadFallbackIcon();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLocalUrl = this.iconUrl.startsWith(assetsPath);
|
||||
|
||||
// Cache icon if the url is not the theme generic one.
|
||||
// If modname is not set icon won't be cached.
|
||||
|
@ -123,62 +179,31 @@ export class CoreModIconComponent implements OnInit, OnChanges {
|
|||
!!this.modname &&
|
||||
!!this.componentId &&
|
||||
!this.isLocalUrl &&
|
||||
this.getComponentNameFromIconUrl(this.icon) != this.modname;
|
||||
this.getComponentNameFromIconUrl(this.iconUrl) != this.modname;
|
||||
|
||||
this.noFilter = await this.getIconNoFilter();
|
||||
await this.setSVGIcon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon to load on error.
|
||||
*/
|
||||
async loadFallbackIcon(): Promise<void> {
|
||||
if (this.isLocalUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLocalUrl = true;
|
||||
this.linkIconWithComponent = false;
|
||||
|
||||
const moduleName = !this.modname || CoreCourse.CORE_MODULES.indexOf(this.modname) < 0
|
||||
? fallbackModName
|
||||
: this.modname;
|
||||
|
||||
const path = CoreCourse.getModuleIconsPath();
|
||||
|
||||
this.icon = path + moduleName + '.svg';
|
||||
this.noFilter = await this.getIconNoFilter();
|
||||
}
|
||||
this.iconUrl = path + moduleName + '.svg';
|
||||
|
||||
/**
|
||||
* Returns if the icon does not need to be filtered.
|
||||
*
|
||||
* @returns wether the icon does not need to be filtered.
|
||||
*/
|
||||
protected async getIconNoFilter(): Promise<boolean> {
|
||||
// Earlier 4.0, icons were never filtered.
|
||||
if (this.iconVersion === IconVersion.LEGACY_VERSION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// No icon or local icon (not legacy), filter it.
|
||||
if (!this.icon || this.isLocalUrl) {
|
||||
return await CoreCourseModuleDelegate.moduleIconIsBranded(this.modname);
|
||||
}
|
||||
|
||||
this.icon = CoreTextUtils.decodeHTMLEntities(this.icon);
|
||||
|
||||
// If it's an Moodle Theme icon, check if filtericon is set and use it.
|
||||
if (this.icon && CoreUrlUtils.isThemeImageUrl(this.icon)) {
|
||||
const iconParams = CoreUrlUtils.extractUrlParams(this.icon);
|
||||
if (iconParams['filtericon'] === '1') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// filtericon was introduced in 4.2 and backported to 4.1.3 and 4.0.8.
|
||||
if (this.modname && !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan(['4.0.8', '4.1.3', '4.2'])) {
|
||||
// If version is prior to that, check if the url is a module icon and filter it.
|
||||
if (this.getComponentNameFromIconUrl(this.icon) === this.modname) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External icons, or non monologo, do not filter.
|
||||
return true;
|
||||
await this.setSVGIcon();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -188,7 +213,7 @@ export class CoreModIconComponent implements OnInit, OnChanges {
|
|||
* @returns Guessed modname.
|
||||
*/
|
||||
protected getComponentNameFromIconUrl(iconUrl: string): string {
|
||||
if (!CoreUrlUtils.isThemeImageUrl(this.icon)) {
|
||||
if (!CoreUrlUtils.isThemeImageUrl(this.iconUrl)) {
|
||||
// Cannot be guessed.
|
||||
return '';
|
||||
}
|
||||
|
@ -213,6 +238,38 @@ export class CoreModIconComponent implements OnInit, OnChanges {
|
|||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the purpose class.
|
||||
*/
|
||||
protected setPurposeClass(): void {
|
||||
if (this.iconVersion === IconVersion.LEGACY_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.purposeClass =
|
||||
CoreCourseModuleDelegate.supportsFeature<ModPurpose>(
|
||||
this.modname || '',
|
||||
CoreConstants.FEATURE_MOD_PURPOSE,
|
||||
this.purpose,
|
||||
);
|
||||
|
||||
if (this.iconVersion === IconVersion.VERSION_4_0) {
|
||||
if (this.purposeClass === ModPurpose.MOD_PURPOSE_INTERACTIVECONTENT) {
|
||||
// Interactive content was introduced on 4.4, on previous versions CONTENT is used instead.
|
||||
this.purposeClass = ModPurpose.MOD_PURPOSE_CONTENT;
|
||||
}
|
||||
|
||||
if (this.modname === 'lti') {
|
||||
// LTI had content purpose with 4.0 icons.
|
||||
this.purposeClass = ModPurpose.MOD_PURPOSE_CONTENT;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.purposeClass) {
|
||||
this.element.classList.add(this.purposeClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon version depending on site version.
|
||||
*
|
||||
|
@ -232,4 +289,103 @@ 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 = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = 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) {
|
||||
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') {
|
||||
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]);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
this.element.replaceChildren(doc.documentElement);
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<ion-item class="ion-text-wrap" collapsible>
|
||||
<core-mod-icon slot="start" [modicon]="modicon" [modname]="module.modname" [componentId]="module.instance" [purpose]="module.purpose" />
|
||||
<ion-label>
|
||||
<core-mod-icon slot="start" [modicon]="modicon" [modname]="module.modname" [componentId]="module.instance" [purpose]="module.purpose"
|
||||
[isBranded]="module.branded" />
|
||||
<ion-label class="core-module-info-activity-title">
|
||||
<h1>
|
||||
<core-format-text [text]="module.name" contextLevel="module" [component]="component" [componentId]="componentId"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId" />
|
||||
|
|
|
@ -10,8 +10,16 @@
|
|||
|
||||
core-mod-icon {
|
||||
align-self: flex-start;
|
||||
--padding: 4px;
|
||||
@include margin-horizontal(null, 8px);
|
||||
|
||||
--module-icon-padding: 0px;
|
||||
--module-legacy-icon-padding: 4px;
|
||||
}
|
||||
|
||||
ion-label.core-module-info-activity-title {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h1 ion-icon {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<ion-label>
|
||||
<p *ngIf="moduleNameTranslated" class="core-modulename">
|
||||
<core-mod-icon slot="start" [modicon]="modicon" [modname]="module.modname" [componentId]="module.instance"
|
||||
[fallbackTranslation]="module.modplural" [purpose]="module.purpose" />
|
||||
[fallbackTranslation]="module.modplural" [purpose]="module.purpose" [isBranded]="module.branded" />
|
||||
{{moduleNameTranslated}}
|
||||
</p>
|
||||
<h1>
|
||||
|
|
|
@ -10,16 +10,20 @@ h1 {
|
|||
|
||||
.core-modulename {
|
||||
text-transform: uppercase;
|
||||
|
||||
core-mod-icon {
|
||||
--padding: 3px;
|
||||
--size: 10px;
|
||||
margin: 0;
|
||||
--module-icon-padding: 0px;
|
||||
--module-legacy-icon-padding: 2px;
|
||||
--module-icon-size: 16px;
|
||||
--module-legacy-icon-size: 12px;
|
||||
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ion-item ion-label ion-icon {
|
||||
@include margin-horizontal(0, 4px);
|
||||
@include margin-horizontal(0px, 4px);
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
<ion-label>
|
||||
<div class="activity-main">
|
||||
<core-mod-icon *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon" [modname]="module.modname"
|
||||
[componentId]="module.instance" [fallbackTranslation]="module.modplural" [purpose]="module.purpose" />
|
||||
[componentId]="module.instance" [fallbackTranslation]="module.modplural" [purpose]="module.purpose"
|
||||
[isBranded]="module.branded" />
|
||||
<div class="activity-title">
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="module.handlerData.title" contextLevel="module" [contextInstanceId]="module.id"
|
||||
|
|
|
@ -33,7 +33,8 @@
|
|||
core-mod-icon {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
--module-icon-padding: 4px;
|
||||
--module-icon-padding: 0px;
|
||||
--module-legacy-icon-padding: 4px;
|
||||
--module-icon-radius: var(--radius-xs);
|
||||
|
||||
@include margin-horizontal(null, 8px);
|
||||
|
|
|
@ -1751,6 +1751,7 @@ export type CoreCourseGetContentsWSModule = {
|
|||
modicon: string; // Activity icon url.
|
||||
modname: string; // Activity module type.
|
||||
purpose?: string; // @since 4.4 The module purpose.
|
||||
branded?: boolean; // @since 4.4 Whether the module is branded or not.
|
||||
modplural: string; // Activity module plural name.
|
||||
availability?: string; // Module availability settings.
|
||||
indent: number; // Number of identation in the site.
|
||||
|
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
@ -45,7 +45,7 @@
|
|||
<img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" class="core-module-icon"
|
||||
[alt]="row.iconAlt" />
|
||||
<core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start"
|
||||
[modname]="row.itemmodule" />
|
||||
[modname]="row.itemmodule" [colorize]="false" />
|
||||
<span [innerHTML]="row.gradeitem"></span>
|
||||
</th>
|
||||
<ng-container *ngIf="row.itemtype !== 'category'">
|
||||
|
|
|
@ -43,10 +43,10 @@ table.core-table.core-grades-table {
|
|||
}
|
||||
|
||||
core-mod-icon {
|
||||
--padding: 0px;
|
||||
--size: 16px;
|
||||
background: transparent;
|
||||
--filter: var(--module-icon-filter);
|
||||
--module-icon-padding: 0px;
|
||||
--module-legacy-icon-padding: 0px;
|
||||
--module-icon-size: 16px;
|
||||
--module-legacy-icon-size: 16px;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<ion-label>
|
||||
<h3 *ngIf="result.title">
|
||||
<ion-icon *ngIf="renderedIcon" [name]="renderedIcon" aria-hidden="true" />
|
||||
<core-mod-icon *ngIf="!renderedIcon && result.module" [modicon]="result.module.iconurl"
|
||||
[modname]="result.module.name" />
|
||||
<core-mod-icon *ngIf="!renderedIcon && result.module" [modicon]="result.module.iconurl" [modname]="result.module.name"
|
||||
[colorize]="false" />
|
||||
<img *ngIf="!renderedIcon && !result.module && result.component" [src]="result.component.iconurl" alt="" class="result-icon"
|
||||
core-external-content [component]="result.component.name">
|
||||
<core-format-text [text]="result.title" />
|
||||
|
|
|
@ -12,14 +12,14 @@
|
|||
color: var(--core-global-search-result-title-color);
|
||||
|
||||
core-mod-icon {
|
||||
--size: var(--core-global-search-result-icon-size);
|
||||
--filter: var(--module-icon-filter);
|
||||
--module-icon-padding: 0px;
|
||||
--module-legacy-icon-padding: 0px;
|
||||
--module-icon-size: var(--core-global-search-result-icon-size);
|
||||
--module-legacy-icon-size: var(--core-global-search-result-icon-size);
|
||||
|
||||
margin-inline-end: var(--spacing-2);
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
--padding: 0px;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<ion-item class="ion-text-wrap" [href]="item.url" core-link [capture]="true">
|
||||
<core-user-avatar *ngIf="item.avatarUrl" [profileUrl]="item.avatarUrl" slot="start" [linkProfile]="false" />
|
||||
|
||||
<core-mod-icon *ngIf="item.iconUrl" [modicon]="item.iconUrl" slot="start" [showAlt]="false" />
|
||||
<core-mod-icon *ngIf="item.iconUrl" [modicon]="item.iconUrl" slot="start" [showAlt]="false" [isBranded]="item.branded" />
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ item.heading }}</p>
|
||||
<p *ngFor="let text of item.details">{{ text }}</p>
|
||||
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
|
@ -135,15 +135,6 @@ $activity-icon-colors: (
|
|||
interactivecontent: #8d3d1b
|
||||
) !default;
|
||||
|
||||
$activity-icon-color-filters: (
|
||||
administration: invert(45%) sepia(46%) saturate(3819%) hue-rotate(260deg) brightness(101%) contrast(87%),
|
||||
assessment: invert(36%) sepia(98%) saturate(6969%) hue-rotate(315deg) brightness(90%) contrast(119%),
|
||||
collaboration: invert(25%) sepia(54%) saturate(6226%) hue-rotate(245deg) brightness(100%) contrast(102%),
|
||||
communication: invert(48%) sepia(74%) saturate(4887%) hue-rotate(11deg) brightness(102%) contrast(101%),
|
||||
content: invert(49%) sepia(52%) saturate(4675%) hue-rotate(156deg) brightness(89%) contrast(102%),
|
||||
interactivecontent: 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;
|
||||
|
|
|
@ -130,8 +130,6 @@ html.dark {
|
|||
--core-login-input-background: var(--core-login-background);
|
||||
--core-login-input-color: var(--core-login-text-color);
|
||||
|
||||
--module-icon-filter: brightness(0) invert(1);
|
||||
|
||||
--core-question-correct-color: var(--success-tint);
|
||||
--core-question-correct-color-bg: var(--success-shade);
|
||||
--core-question-incorrect-color: var(--danger);
|
||||
|
|
|
@ -354,8 +354,6 @@ html {
|
|||
--core-messages-discussion-badge: var(--primary);
|
||||
--core-messages-discussion-badge-text: var(--white);
|
||||
|
||||
--module-icon-filter: brightness(0);
|
||||
|
||||
--addon-forum-avatar-size: var(--core-avatar-size);
|
||||
--addon-forum-border-color: var(--stroke);
|
||||
--addon-forum-highlight-color: var(--light);
|
||||
|
@ -404,7 +402,7 @@ html {
|
|||
}
|
||||
|
||||
// Make activtity colours available for custom modules.
|
||||
@each $type, $value in $activity-icon-color-filters {
|
||||
@each $type, $value in $activity-icon-colors {
|
||||
--activity#{$type}: #{$value};
|
||||
}
|
||||
|
||||
|
|