MOBILE-2748 mod: Create a new module icon component

main
Pau Ferrer Ocaña 2021-09-27 16:44:24 +02:00
parent aa8c6136de
commit 22bdbc1ddc
33 changed files with 251 additions and 147 deletions

View File

@ -6,7 +6,8 @@
<core-loading [hideUntil]="loaded" [fullscreen]="false" class="margin"> <core-loading [hideUntil]="loaded" [fullscreen]="false" class="margin">
<ion-item class="ion-text-wrap item-media" *ngFor="let entry of entries" detail="true" button <ion-item class="ion-text-wrap item-media" *ngFor="let entry of entries" detail="true" button
(click)="gotoCoureListModType(entry)"> (click)="gotoCoureListModType(entry)">
<img slot="start" [src]="entry.icon" alt="" role="presentation" class="core-module-icon"> <core-mod-icon slot="start" [modicon]="entry.icon" [modname]="entry.modName" [showAlt]="false">
</core-mod-icon>
<ion-label>{{ entry.name }}</ion-label> <ion-label>{{ entry.name }}</ion-label>
</ion-item> </ion-item>
</core-loading> </core-loading>

View File

@ -17,7 +17,9 @@
<ion-card> <ion-card>
<ion-item class="core-course-module-handler item-media ion-text-wrap" detail="false" (click)="action($event, item)" <ion-item class="core-course-module-handler item-media ion-text-wrap" detail="false" (click)="action($event, item)"
button> button>
<img slot="start" [src]="item.iconUrl" alt="" role="presentation" *ngIf="item.iconUrl" class="core-module-icon"> <core-mod-icon slot="start" *ngIf="item.iconUrl" [modicon]="item.iconUrl"
[modname]="item.modname" [componentId]="item.cmid" [showAlt]="false">
</core-mod-icon>
<ion-label> <ion-label>
<!-- Add the icon title so accessibility tools read it. --> <!-- Add the icon title so accessibility tools read it. -->
<span class="sr-only" *ngIf="item.iconTitle">{{ item.iconTitle }}</span> <span class="sr-only" *ngIf="item.iconTitle">{{ item.iconTitle }}</span>

View File

@ -5,10 +5,10 @@
<ng-container *ngFor="let event of dayEvents.events"> <ng-container *ngFor="let event of dayEvents.events">
<ion-item class="ion-text-wrap core-course-module-handler item-media" detail="false" (click)="action($event, event.url)" <ion-item class="ion-text-wrap core-course-module-handler item-media" detail="false" (click)="action($event, event.url)"
[attr.aria-label]="event.name" button> [attr.aria-label]="event.name" button>
<img slot="start" [src]="event.iconUrl" alt="" role="presentation" *ngIf="event.iconUrl" class="core-module-icon"> <core-mod-icon *ngIf="event.iconUrl" slot="start" [modicon]="event.iconUrl" [componentId]="event.instance"
[modname]="event.modulename">
</core-mod-icon>
<ion-label> <ion-label>
<!-- Add the icon title so accessibility tools read it. -->
<span class="sr-only" *ngIf="event.iconTitle">{{ event.iconTitle }}</span>
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="event.name" contextLevel="module" [contextInstanceId]="event.id" <core-format-text [text]="event.name" contextLevel="module" [contextInstanceId]="event.id"
[courseId]="event.course && event.course.id"> [courseId]="event.course && event.course.id">

View File

@ -104,7 +104,8 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
return start <= event.timesort; return start <= event.timesort;
}).map(async (event) => { }).map(async (event) => {
event.iconUrl = await CoreCourse.getModuleIconSrc(event.icon.component); event.iconUrl = await CoreCourse.getModuleIconSrc(event.icon.component);
event.iconTitle = event.modulename && CoreCourse.translateModuleName(event.modulename); event.modulename = event.modulename || event.icon.component;
event.iconTitle = CoreCourse.translateModuleName(event.modulename);
return event; return event;
})); }));

View File

@ -9,6 +9,10 @@
padding: 6px; padding: 6px;
} }
> core-mod-icon {
padding: 6px;
}
&.addon-calendar-eventtype-category > ion-icon { &.addon-calendar-eventtype-category > ion-icon {
background-color: var(--addon-calendar-event-category-color); background-color: var(--addon-calendar-event-category-color);
} }
@ -25,4 +29,5 @@
background-color: var(--addon-calendar-event-site-color); background-color: var(--addon-calendar-event-site-color);
} }
} }
}
}

View File

@ -88,8 +88,9 @@
<span class="addon-calendar-event-time"> <span class="addon-calendar-event-time">
{{ event.timestart * 1000 | coreFormatDate: timeFormat }} {{ event.timestart * 1000 | coreFormatDate: timeFormat }}
</span> </span>
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" alt="" role="presentation" <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false"
class="core-module-icon"> [modname]="event.modulename" [componentId]="event.instance">
</core-mod-icon>
<!-- Add the icon title so accessibility tools read it. --> <!-- Add the icon title so accessibility tools read it. -->
<span class="sr-only"> <span class="sr-only">
{{ 'addon.calendar.type' + event.formattedType | translate }} {{ 'addon.calendar.type' + event.formattedType | translate }}

View File

@ -144,15 +144,15 @@
} }
} }
.core-module-icon { core-mod-icon {
margin-right: 1px; margin-right: 1px;
margin-left: 1px; margin-left: 1px;
--size: 16px; --size: 16px;
display: inline-block; display: inline-block;
vertical-align: bottom; vertical-align: bottom;
} ::ng-deep img {
.core-module-icon[slot="start"] { display: block;
padding: 6px; }
} }
} }

View File

@ -6,8 +6,8 @@
<ng-container *ngFor="let event of filteredEvents"> <ng-container *ngFor="let event of filteredEvents">
<ion-item class="ion-text-wrap addon-calendar-event" [attr.aria-label]="event.name" (click)="eventClicked(event)" button <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="true"> [ngClass]="['addon-calendar-eventtype-'+event.eventtype]" detail="true">
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" slot="start" class="core-module-icon" alt="" <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [modname]="event.modulename"
role="presentation"> [componentId]="event.instance" [showAlt]="false"></core-mod-icon>
<ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" aria-hidden="true"> <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" aria-hidden="true">
</ion-icon> </ion-icon>
<ion-label> <ion-label>

View File

@ -61,8 +61,9 @@
<ng-container *ngFor="let event of filteredEvents"> <ng-container *ngFor="let event of filteredEvents">
<ion-item class="addon-calendar-event ion-text-wrap" [attr.aria-label]="event.name" (click)="gotoEvent(event.id)" <ion-item class="addon-calendar-event ion-text-wrap" [attr.aria-label]="event.name" (click)="gotoEvent(event.id)"
[class.item-dimmed]="event.ispast" [ngClass]="['addon-calendar-eventtype-'+event.eventtype]" button detail="true"> [class.item-dimmed]="event.ispast" [ngClass]="['addon-calendar-eventtype-'+event.eventtype]" button detail="true">
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" slot="start" class="core-module-icon" alt="" <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [showAlt]="false"
role="presentation"> [modname]="event.modname" [componentId]="event.instance">
</core-mod-icon>
<ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" aria-hidden="true"> <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" aria-hidden="true">
</ion-icon> </ion-icon>
<ion-label> <ion-label>

View File

@ -4,7 +4,8 @@
<ion-back-button [text]="'core.back' | translate"></ion-back-button> <ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons> </ion-buttons>
<h1 *ngIf="event"> <h1 *ngIf="event">
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" alt="" role="presentation" class="core-module-icon"> <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false"
[modname]="event.modulename" [componentId]="event.instance"></core-mod-icon>
<ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" aria-hidden="true"></ion-icon> <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" aria-hidden="true"></ion-icon>
<!-- Add the icon title so accessibility tools read it. --> <!-- Add the icon title so accessibility tools read it. -->
<span class="sr-only"> <span class="sr-only">

View File

@ -2,8 +2,9 @@
ion-card ion-note { ion-card ion-note {
font-size: 1.6rem; font-size: 1.6rem;
} }
h1 ion-icon, h1 img { h1 ion-icon, h1 img, h1 core-mod-icon {
margin-left: 10px; margin-left: 10px;
margin-right: 10px; margin-right: 10px;
display: inline-block;
} }
} }

View File

@ -77,8 +77,7 @@
</p> </p>
<ion-item class="ion-text-wrap" *ngFor="let activity of coursemodules" [href]="activity.url" <ion-item class="ion-text-wrap" *ngFor="let activity of coursemodules" [href]="activity.url"
[attr.aria-label]="activity.name" core-link capture="true"> [attr.aria-label]="activity.name" core-link capture="true">
<img slot="start" core-external-content [src]="activity.iconurl" alt="" *ngIf="activity.iconurl" <core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl"></core-mod-icon>
class="core-module-icon">
<ion-label> <ion-label>
<core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id" <core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id"
[courseId]="courseId"> [courseId]="courseId">

View File

@ -114,8 +114,8 @@
</p> </p>
<ion-item class="ion-text-wrap core-course-module-handler item-media" [attr.aria-label]="activity.name" <ion-item class="ion-text-wrap core-course-module-handler item-media" [attr.aria-label]="activity.name"
core-link *ngFor="let activity of competency.coursemodules" [href]="activity.url" capture="true"> core-link *ngFor="let activity of competency.coursemodules" [href]="activity.url" capture="true">
<img slot="start" [src]="activity.iconurl" core-external-content alt="" <core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl">
*ngIf="activity.iconurl" class="core-module-icon"> </core-mod-icon>
<ion-label> <ion-label>
<core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id" <core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id"
[courseId]="courseId"> [courseId]="courseId">

View File

@ -17,13 +17,8 @@ import { Injectable, Type } from '@angular/core';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreApp } from '@services/app'; import { makeSingleton } from '@singletons';
import { CoreFilepool } from '@services/filepool';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { DomSanitizer, makeSingleton } from '@singletons';
import { AddonModLtiHelper } from '../lti-helper'; import { AddonModLtiHelper } from '../lti-helper';
import { AddonModLti, AddonModLtiProvider } from '../lti';
import { AddonModLtiIndexComponent } from '../../components/index'; import { AddonModLtiIndexComponent } from '../../components/index';
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler'; import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
@ -62,6 +57,9 @@ export class AddonModLtiModuleHandlerService extends CoreModuleHandlerBase imple
const data = await super.getData(module, courseId, sectionId, forCoursePage); const data = await super.getData(module, courseId, sectionId, forCoursePage);
data.showDownloadButton = false; data.showDownloadButton = false;
// Handle custom icons.
data.icon = module.modicon;
data.buttons = [{ data.buttons = [{
icon: 'fas-external-link-alt', icon: 'fas-external-link-alt',
label: 'addon.mod_lti.launchactivity', label: 'addon.mod_lti.launchactivity',
@ -71,49 +69,9 @@ export class AddonModLtiModuleHandlerService extends CoreModuleHandlerBase imple
}, },
}]; }];
// Handle custom icons.
CoreUtils.ignoreErrors(this.loadCustomIcon(module, courseId, data));
return data; return data;
} }
/**
* Load the custom icon.
*
* @param module Module.
* @param courseId Course ID.
* @param data Handler data.
* @return Promise resolved when done.
*/
protected async loadCustomIcon(
module: CoreCourseModule,
courseId: number,
handlerData: CoreCourseModuleHandlerData,
): Promise<void> {
const lti = await AddonModLti.getLti(courseId, module.id);
const icon = lti.secureicon || lti.icon;
if (!icon) {
return;
}
const siteId = CoreSites.getCurrentSiteId();
try {
await CoreFilepool.downloadUrl(siteId, icon, false, AddonModLtiProvider.COMPONENT, module.id);
// Get the internal URL.
const url = await CoreFilepool.getSrcByUrl(siteId, icon, AddonModLtiProvider.COMPONENT, module.id);
handlerData.icon = DomSanitizer.bypassSecurityTrustUrl(url);
} catch {
// Error downloading. If we're online we'll set the online url.
if (CoreApp.isOnline()) {
handlerData.icon = DomSanitizer.bypassSecurityTrustUrl(icon);
}
}
}
/** /**
* @inheritdoc * @inheritdoc
*/ */

View File

@ -15,7 +15,7 @@
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { Injectable, Type } from '@angular/core'; import { Injectable, Type } from '@angular/core';
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler'; import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
import { CoreCourse, CoreCourseModuleContentFile } from '@features/course/services/course'; import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
@ -23,7 +23,6 @@ import { CoreFileHelper } from '@services/file-helper';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { CoreWSFile } from '@services/ws';
import { makeSingleton, Translate } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { AddonModResourceIndexComponent } from '../../components/index'; import { AddonModResourceIndexComponent } from '../../components/index';
import { AddonModResource, AddonModResourceCustomData } from '../resource'; import { AddonModResource, AddonModResourceCustomData } from '../resource';
@ -94,7 +93,7 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
}]; }];
this.getResourceData(module, courseId, handlerData).then((data) => { this.getResourceData(module, courseId, handlerData).then((data) => {
handlerData.icon = handlerData.icon || data.icon; handlerData.icon = data.icon;
handlerData.extraBadge = data.extra; handlerData.extraBadge = data.extra;
handlerData.extraBadgeColor = 'light'; handlerData.extraBadgeColor = 'light';
@ -136,7 +135,6 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
handlerData: CoreCourseModuleHandlerData, handlerData: CoreCourseModuleHandlerData,
): Promise<AddonResourceHandlerData> { ): Promise<AddonResourceHandlerData> {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
let infoFiles: CoreWSFile[] = [];
let options: AddonModResourceCustomData = {}; let options: AddonModResourceCustomData = {};
// Check if the button needs to be shown or not. // Check if the button needs to be shown or not.
@ -150,12 +148,11 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
return; return;
})); }));
if ('customdata' in module && typeof module.customdata != 'undefined') { if ('customdata' in module && module.customdata !== undefined) {
options = CoreTextUtils.unserialize(CoreTextUtils.parseJSON(module.customdata)); options = CoreTextUtils.unserialize(CoreTextUtils.parseJSON(module.customdata));
} else { } else {
// Get the resource data. // Get the resource data.
promises.push(AddonModResource.getResourceData(courseId, module.id).then((info) => { promises.push(AddonModResource.getResourceData(courseId, module.id).then((info) => {
infoFiles = info.contentfiles;
options = CoreTextUtils.unserialize(info.displayoptions); options = CoreTextUtils.unserialize(info.displayoptions);
return; return;
@ -164,28 +161,22 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
await Promise.all(promises); await Promise.all(promises);
const files: (CoreCourseModuleContentFile | CoreWSFile)[] = module.contents && module.contents.length let mimetypeIcon = '';
? module.contents
: infoFiles;
const resourceData: AddonResourceHandlerData = {
icon: '',
extra: '',
};
const extra: string[] = []; const extra: string[] = [];
if ('contentsinfo' in module && module.contentsinfo) { if ('contentsinfo' in module && module.contentsinfo) {
// No need to use the list of files. // No need to use the list of files.
const mimetype = module.contentsinfo.mimetypes[0]; const mimetype = module.contentsinfo.mimetypes[0];
if (mimetype) { if (mimetype) {
resourceData.icon = CoreMimetypeUtils.getMimetypeIcon(mimetype); mimetypeIcon = CoreMimetypeUtils.getMimetypeIcon(mimetype);
} }
resourceData.extra = CoreTextUtils.cleanTags(module.afterlink); extra.push(CoreTextUtils.cleanTags(module.afterlink));
} else if (files && files.length) { } else if (module.contents && module.contents[0]) {
const files = module.contents;
const file = files[0]; const file = files[0];
resourceData.icon = CoreMimetypeUtils.getFileIcon(file.filename || ''); mimetypeIcon = CoreMimetypeUtils.getFileIcon(file.filename || '');
if (options.showsize) { if (options.showsize) {
const size = options.filedetails const size = options.filedetails
@ -227,16 +218,12 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
)); ));
} }
} }
resourceData.extra += extra.join(' ');
} }
// No previously set, just set the icon. return {
if (resourceData.icon == '') { icon: await CoreCourse.getModuleIconSrc(module.modname, module.modicon, mimetypeIcon),
resourceData.icon = await CoreCourse.getModuleIconSrc(module.modname, module.modicon); extra: extra.join(' '),
} };
return resourceData;
} }
/** /**

View File

@ -119,9 +119,10 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
handlerData.buttons[0].hidden = hideButton; handlerData.buttons[0].hidden = hideButton;
if (module.contents && module.contents[0]) { if (module.contents && module.contents[0]) {
const icon = AddonModUrl.guessIcon(module.contents[0].fileurl);
// Calculate the icon to use. // Calculate the icon to use.
handlerData.icon = await CoreCourse.getModuleIconSrc(module.modname, module.modicon) || handlerData.icon = await CoreCourse.getModuleIconSrc(module.modname, module.modicon, icon);
AddonModUrl.guessIcon(module.contents[0].fileurl);
} }
return; return;

View File

@ -45,8 +45,9 @@
<ion-card-content> <ion-card-content>
<ng-container *ngFor="let module of section.modules"> <ng-container *ngFor="let module of section.modules">
<ion-item class="ion-no-padding" *ngIf="module.totalSize! > 0"> <ion-item class="ion-no-padding" *ngIf="module.totalSize! > 0">
<img *ngIf="module.handlerData!.icon" [src]="module.handlerData!.icon" [alt]="module.modNameTranslated" <core-mod-icon slot="start" *ngIf="module.handlerData!.icon"
class="core-module-icon" slot="start"> [modicon]="module.handlerData!.icon" [modname]="module.modname" [componentId]="module.instance">
</core-mod-icon>
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
<h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size"> <h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size">
{{ module.name }} {{ module.name }}

View File

@ -63,7 +63,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit {
section.modules.forEach((module) => { section.modules.forEach((module) => {
module.parentSection = section; module.parentSection = section;
module.totalSize = 0; module.totalSize = 0;
module.modNameTranslated = CoreCourse.translateModuleName(module.modname) || '';
// Note: This function only gets the size for modules which are downloadable. // Note: This function only gets the size for modules which are downloadable.
// For other modules it always returns 0, even if they have downloaded some files. // For other modules it always returns 0, even if they have downloaded some files.
@ -235,5 +234,4 @@ type AddonStorageManagerCourseSection = Omit<CoreCourseSection, 'modules'> & {
type AddonStorageManagerModule = CoreCourseModule & { type AddonStorageManagerModule = CoreCourseModule & {
parentSection?: AddonStorageManagerCourseSection; parentSection?: AddonStorageManagerCourseSection;
totalSize?: number; totalSize?: number;
modNameTranslated?: string;
}; };

View File

@ -40,6 +40,7 @@ import { CoreInputErrorsComponent } from './input-errors/input-errors';
import { CoreLoadingComponent } from './loading/loading'; import { CoreLoadingComponent } from './loading/loading';
import { CoreLocalFileComponent } from './local-file/local-file'; import { CoreLocalFileComponent } from './local-file/local-file';
import { CoreMarkRequiredComponent } from './mark-required/mark-required'; import { CoreMarkRequiredComponent } from './mark-required/mark-required';
import { CoreModIconComponent } from './mod-icon/mod-icon';
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons'; import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar'; import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
import { CoreProgressBarComponent } from './progress-bar/progress-bar'; import { CoreProgressBarComponent } from './progress-bar/progress-bar';
@ -81,6 +82,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit
CoreLoadingComponent, CoreLoadingComponent,
CoreLocalFileComponent, CoreLocalFileComponent,
CoreMarkRequiredComponent, CoreMarkRequiredComponent,
CoreModIconComponent,
CoreNavBarButtonsComponent, CoreNavBarButtonsComponent,
CoreNavigationBarComponent, CoreNavigationBarComponent,
CoreProgressBarComponent, CoreProgressBarComponent,
@ -128,6 +130,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit
CoreLoadingComponent, CoreLoadingComponent,
CoreLocalFileComponent, CoreLocalFileComponent,
CoreMarkRequiredComponent, CoreMarkRequiredComponent,
CoreModIconComponent,
CoreNavBarButtonsComponent, CoreNavBarButtonsComponent,
CoreNavigationBarComponent, CoreNavigationBarComponent,
CoreProgressBarComponent, CoreProgressBarComponent,

View File

@ -0,0 +1,19 @@
<img
*ngIf="!isLocalUrl"
[src]="icon"
[alt]="showAlt ? modNameTranslated : ''"
[attr.role]="!showAlt ? 'presentation' : null"
class="core-module-icon"
core-external-content
[component]="linkIconWithComponent ? modname : null"
[componentId]="linkIconWithComponent ? componentId : null"
(error)="loadFallbackIcon()"
>
<img
*ngIf="isLocalUrl"
[src]="icon"
[alt]="showAlt ? modNameTranslated : ''"
[attr.role]="!showAlt ? 'presentation' : null"
class="core-module-icon"
(error)="loadFallbackIcon()"
>

View File

@ -0,0 +1,37 @@
:host {
--size: var(--module-icon-size);
--margin-end: 0px;
--margin-vertical: 0px;
margin-top: var(--margin-vertical);
margin-bottom: var(--margin-vertical);
margin-right: var(--margin-end);
}
img {
width: var(--size);
height: var(--size);
max-width: var(--size);
max-height: var(--size);
&[alt] {
text-indent: -999999px;
white-space: nowrap;
overflow: hidden;
}
}
:host-context(ion-item) {
--margin-vertical: 12px;
--margin-end: 32px;
}
:host-context(ion-card ion-item) {
--margin-vertical: 12px;
--margin-end: 12px;
}
:host-context([dir=rtl]) {
margin-right: unset;
margin-left: var(--margin-end);
}

View File

@ -0,0 +1,88 @@
// (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 { Component, Input, OnChanges, OnInit, SimpleChange } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
const assetsPath = 'assets/img/mod/';
const fallbackModName = 'external-tool';
/**
* Component to handle a module icon.
*/
@Component({
selector: 'core-mod-icon',
templateUrl: 'mod-icon.html',
styleUrls: ['mod-icon.scss'],
})
export class CoreModIconComponent implements OnInit, OnChanges {
@Input() modname?; // The module name. Used also as component if set.
@Input() componentId?; // 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.
icon = '';
modNameTranslated = '';
isLocalUrl = true;
linkIconWithComponent = false;
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.modNameTranslated = this.modname ? CoreCourse.translateModuleName(this.modname) || '' : '';
this.setIcon();
}
/**
* @inheritdoc
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (changes && changes.modicon && changes.modicon.previousValue) {
this.setIcon();
}
}
/**
* Set icon.
*/
setIcon(): void {
this.icon = this.modicon || this.icon;
this.isLocalUrl = this.icon.startsWith(assetsPath);
// Cache icon if the url is not the theme generic one.
// If modname is not set icon won't be cached.
// Also if the url matches the regexp (the theme will manage the image so it's not cached).
this.linkIconWithComponent =
this.modname &&
this.componentId &&
!this.isLocalUrl &&
!this.icon.match('/theme/image.php/[^/]+/' + this.modname + '/[-0-9]*/');
}
/**
* Icon to load on error.
*/
loadFallbackIcon(): void {
this.isLocalUrl = true;
const moduleName = !this.modname || CoreCourse.CORE_MODULES.indexOf(this.modname) < 0
? fallbackModName
: this.modname;
this.icon = assetsPath + moduleName + '.svg';
}
}

View File

@ -79,3 +79,8 @@
left: 0; left: 0;
right: unset; right: unset;
} }
:host-context(ion-item) {
margin-top: 12px;
margin-bottom: 12px;
}

View File

@ -113,8 +113,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges {
* Get the URL that should be handled and, if valid, handle it. * Get the URL that should be handled and, if valid, handle it.
*/ */
protected async checkAndHandleExternalContent(): Promise<void> { protected async checkAndHandleExternalContent(): Promise<void> {
const currentSite = CoreSites.getCurrentSite(); const siteId = this.siteId || CoreSites.getRequiredCurrentSite().getId();
const siteId = this.siteId || currentSite?.getId();
const tagName = this.element.tagName.toUpperCase(); const tagName = this.element.tagName.toUpperCase();
let targetAttr; let targetAttr;
let url; let url;

View File

@ -13,8 +13,9 @@
[button]="module.handlerData.action && module.uservisible" [button]="module.handlerData.action && module.uservisible"
detail="false"> detail="false">
<img slot="start" *ngIf="module.handlerData.icon" [src]="module.handlerData.icon" [alt]="modNameTranslated" <core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon" [modname]="module.modname"
class="core-module-icon"> [componentId]="module.instance">
</core-mod-icon>
<ion-label class="core-module-title"> <ion-label class="core-module-title">
<p class="item-heading"> <p class="item-heading">

View File

@ -83,7 +83,7 @@ export class CoreCourseProvider {
static readonly COMPONENT = 'CoreCourse'; static readonly COMPONENT = 'CoreCourse';
protected readonly CORE_MODULES = [ readonly CORE_MODULES = [
'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'database', 'date', 'external-tool', 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'database', 'date', 'external-tool',
'feedback', 'file', 'folder', 'forum', 'glossary', 'ims', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', 'feedback', 'file', 'folder', 'forum', 'glossary', 'ims', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz',
'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop', 'h5pactivity', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop', 'h5pactivity',
@ -402,15 +402,16 @@ export class CoreCourseProvider {
): Promise<CoreCourseWSSection[]> => { ): Promise<CoreCourseWSSection[]> => {
const params: CoreCourseGetContentsParams = { const params: CoreCourseGetContentsParams = {
courseid: courseId!, courseid: courseId!,
options: [],
}; };
params.options = [];
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
omitExpires: preferCache, omitExpires: preferCache,
updateFrequency: CoreSite.FREQUENCY_RARELY, updateFrequency: CoreSite.FREQUENCY_RARELY,
}; };
if (includeStealth) { if (includeStealth) {
params.options!.push({ params.options.push({
name: 'includestealthmodules', name: 'includestealthmodules',
value: true, value: true,
}); });
@ -418,13 +419,13 @@ export class CoreCourseProvider {
// If modName is set, retrieve all modules of that type. Otherwise get only the module. // If modName is set, retrieve all modules of that type. Otherwise get only the module.
if (modName) { if (modName) {
params.options!.push({ params.options.push({
name: 'modname', name: 'modname',
value: modName, value: modName,
}); });
preSets.cacheKey = this.getModuleByModNameCacheKey(modName); preSets.cacheKey = this.getModuleByModNameCacheKey(modName);
} else { } else {
params.options!.push({ params.options.push({
name: 'cmid', name: 'cmid',
value: moduleId, value: moduleId,
}); });
@ -630,7 +631,11 @@ export class CoreCourseProvider {
* @param modicon The mod icon string to use in case we are not using a core activity. * @param modicon The mod icon string to use in case we are not using a core activity.
* @return The IMG src. * @return The IMG src.
*/ */
async getModuleIconSrc(moduleName: string, modicon?: string): Promise<string> { async getModuleIconSrc(moduleName: string, modicon?: string, mimetypeIcon = ''): Promise<string> {
if (mimetypeIcon) {
return mimetypeIcon;
}
if (this.CORE_MODULES.indexOf(moduleName) < 0) { if (this.CORE_MODULES.indexOf(moduleName) < 0) {
if (modicon) { if (modicon) {
return modicon; return modicon;
@ -1489,7 +1494,7 @@ export type CoreCourseWSModule = {
label: string; label: string;
timestamp: number; timestamp: number;
}[]; // @since 3.11. Activity dates. }[]; // @since 3.11. Activity dates.
contentsinfo?: { // Contents summary information. contentsinfo?: { // @since v3.7.6 Contents summary information.
filescount: number; // Total number of files. filescount: number; // Total number of files.
filessize: number; // Total files size. filessize: number; // Total files size.
lastmodified: number; // Last time files were modified. lastmodified: number; // Last time files were modified.

View File

@ -20,7 +20,7 @@ import { CoreSite } from '@classes/site';
import { CoreCourseModuleDefaultHandler } from './handlers/default-module'; import { CoreCourseModuleDefaultHandler } from './handlers/default-module';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from './course'; import { CoreCourse, CoreCourseWSModule } from './course';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreCourseModule } from './course-helper'; import { CoreCourseModule } from './course-helper';

View File

@ -52,8 +52,11 @@
> >
<ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt"> <ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt">
</ion-icon> </ion-icon>
<img *ngIf="row.image" [src]="row.image" slot="start" class="core-module-icon" <img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" class="core-module-icon"
[alt]="row.iconAlt"/> [alt]="row.iconAlt"/>
<core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start"
[modname]="row.itemmodule">
</core-mod-icon>
<span [innerHTML]="row.gradeitem"></span> <span [innerHTML]="row.gradeitem"></span>
</th> </th>
<ng-container *ngFor="let column of grades.columns"> <ng-container *ngFor="let column of grades.columns">

View File

@ -114,7 +114,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
* Update the table of grades. * Update the table of grades.
*/ */
private async fetchGrades(): Promise<void> { private async fetchGrades(): Promise<void> {
const table = await CoreGrades.getCourseGradesTable(this.grades.courseId!, this.grades.userId); const table = await CoreGrades.getCourseGradesTable(this.grades.courseId, this.grades.userId);
const formattedTable = await CoreGradesHelper.formatGradesTable(table); const formattedTable = await CoreGradesHelper.formatGradesTable(table);
this.grades.setTable(formattedTable); this.grades.setTable(formattedTable);
@ -192,7 +192,7 @@ class CoreGradesCourseManager extends CorePageItemsListManager<CoreGradesFormatt
* @inheritdoc * @inheritdoc
*/ */
protected async logActivity(): Promise<void> { protected async logActivity(): Promise<void> {
await CoreGrades.logCourseGradesView(this.courseId!, this.userId!); await CoreGrades.logCourseGradesView(this.courseId, this.userId);
} }
/** /**

View File

@ -82,6 +82,11 @@
height: 16px; height: 16px;
} }
core-mod-icon {
--size: 16px;
}
ion-icon { ion-icon {
color: var(--icon-color); color: var(--icon-color);
} }
@ -119,6 +124,11 @@
background-color: var(--cell-hover); background-color: var(--cell-hover);
} }
} }
th, td {
height: var(--a11y-min-target-size);
vertical-align: middle;
}
} }
} }

View File

@ -17,8 +17,9 @@
<ion-item *ngIf="grade.itemname && grade.link" class="ion-text-wrap" detail="true" [href]="grade.link" core-link <ion-item *ngIf="grade.itemname && grade.link" class="ion-text-wrap" detail="true" [href]="grade.link" core-link
capture="true"> capture="true">
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start" [attr.aria-label]="grade.iconAlt"></ion-icon> <ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start" [attr.aria-label]="grade.iconAlt"></ion-icon>
<img *ngIf="grade.image" [src]="grade.image" slot="start" class="core-module-icon" <img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image && grade.itemmodule" slot="start" [alt]="grade.iconAlt"/>
[alt]="grade.iconAlt"> <core-mod-icon *ngIf="grade.image && grade.itemmodule" [modicon]="grade.image" slot="start" [modname]="grade.itemmodule">
</core-mod-icon>
<ion-label> <ion-label>
<h2><core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId"> <h2><core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text></h2> </core-format-text></h2>
@ -27,7 +28,9 @@
<ion-item *ngIf="grade.itemname && !grade.link" class="ion-text-wrap" > <ion-item *ngIf="grade.itemname && !grade.link" class="ion-text-wrap" >
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start" [attr.aria-label]="grade.iconAlt"></ion-icon> <ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start" [attr.aria-label]="grade.iconAlt"></ion-icon>
<img *ngIf="grade.image" [src]="grade.image" slot="start" class="core-module-icon" [alt]="grade.iconAlt"/> <img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image" slot="start" [alt]="grade.iconAlt"/>
<core-mod-icon *ngIf="grade.image && grade.itemmodule" [modicon]="grade.image" slot="start" [modname]="grade.itemmodule">
</core-mod-icon>
<ion-label> <ion-label>
<h2><core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId"> <h2><core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text></h2> </core-format-text></h2>

View File

@ -3,8 +3,8 @@
<img [src]="item.avatarUrl" core-external-content alt="" role="presentation" <img [src]="item.avatarUrl" core-external-content alt="" role="presentation"
onError="this.src='assets/img/user-avatar.png'"> onError="this.src='assets/img/user-avatar.png'">
</ion-avatar> </ion-avatar>
<img slot="start" *ngIf="item.iconUrl" [src]="item.iconUrl" core-external-content alt="" role="presentation" <core-mod-icon *ngIf="item.iconUrl" [modicon]="item.iconUrl" slot="start" [showAlt]="false">
class="core-module-icon"> </core-mod-icon>
<ion-label> <ion-label>
<h2>{{ item.heading }}</h2> <h2>{{ item.heading }}</h2>
<p *ngFor="let text of item.details">{{ text }}</p> <p *ngFor="let text of item.details">{{ text }}</p>

View File

@ -527,36 +527,10 @@ img[core-external-content]:not([src]) {
visibility: hidden; visibility: hidden;
} }
// Activity modules
.core-module-icon {
--size: var(--module-icon-size);
width: var(--size);
height: var(--size);
max-width: var(--size);
max-height: var(--size);
}
ion-item img.core-module-icon[slot="start"] {
margin-top: 12px;
margin-bottom: 12px;
margin-right: 32px;
}
ion-card ion-item img.core-module-icon[slot="start"] {
margin-top: 12px;
margin-bottom: 12px;
margin-right: 12px;
}
ion-card ion-item:only-child { ion-card ion-item:only-child {
--inner-border-width: 0; --inner-border-width: 0;
} }
[dir=rtl] ion-item img.core-module-icon[slot="start"] {
margin-right: unset;
margin-left: 32px;
}
.core-course-module-handler:not(.addon-mod-label-handler) .item-heading .filter_mathjaxloader_equation div { .core-course-module-handler:not(.addon-mod-label-handler) .item-heading .filter_mathjaxloader_equation div {
display: inline !important; display: inline !important;
} }