Merge pull request #4180 from crazyserver/MOBILE-4442

Mobile 4442
main
Dani Palou 2024-09-19 15:57:27 +02:00 committed by GitHub
commit 61cb0428f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 773 additions and 268 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -33,6 +33,7 @@ import { AddonModPageModule } from './page/page.module';
import { AddonModQuizModule } from './quiz/quiz.module';
import { AddonModResourceModule } from './resource/resource.module';
import { AddonModScormModule } from './scorm/scorm.module';
import { AddonModSubsectionModule } from './subsection/subsection.module';
import { AddonModSurveyModule } from './survey/survey.module';
import { AddonModUrlModule } from './url/url.module';
import { AddonModWikiModule } from './wiki/wiki.module';
@ -59,6 +60,7 @@ import { AddonModWorkshopModule } from './workshop/workshop.module';
AddonModQuizModule,
AddonModResourceModule,
AddonModScormModule,
AddonModSubsectionModule,
AddonModSurveyModule,
AddonModUrlModule,
AddonModWikiModule,

View File

@ -0,0 +1,65 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourse } from '@features/course/services/course';
import { CoreLoadings } from '@services/loadings';
import { CoreDomUtils } from '@services/utils/dom';
import { makeSingleton } from '@singletons';
import { AddonModSubsection } from '../subsection';
/**
* Handler to treat links to subsection.
*/
@Injectable({ providedIn: 'root' })
export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
name = 'AddonModSubsectionLinkHandler';
constructor() {
super('AddonModSubsection', 'subsection', 'id');
}
/**
* @inheritdoc
*/
getActions(
siteIds: string[],
url: string,
params: Record<string, string>,
courseId?: number,
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: async(siteId) => {
const modal = await CoreLoadings.show();
const moduleId = Number(params.id);
try {
// Get the module.
const module = await CoreCourse.getModule(moduleId, courseId, undefined, true, false, siteId);
await AddonModSubsection.openSubsection(module.section, module.course, siteId);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error opening link.');
} finally {
modal.dismiss();
}
},
}];
}
}
export const AddonModSubsectionIndexLinkHandler = makeSingleton(AddonModSubsectionIndexLinkHandlerService);

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 { CoreConstants, ModPurpose } from '@/core/constants';
import { Injectable } from '@angular/core';
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
import { CoreCourseModuleData } from '@features/course/services/course-helper';
import {
CoreCourseModuleDelegate,
CoreCourseModuleHandler,
CoreCourseModuleHandlerData,
} from '@features/course/services/module-delegate';
import { CoreDomUtils } from '@services/utils/dom';
import { makeSingleton } from '@singletons';
import { AddonModSubsection } from '../subsection';
/**
* Handler to support subsection modules.
*
* This is merely to disable the siteplugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModSubsectionModuleHandlerService extends CoreModuleHandlerBase implements CoreCourseModuleHandler {
name = 'AddonModSubsection';
modName = 'subsection';
supportedFeatures = {
[CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE,
[CoreConstants.FEATURE_GROUPS]: false,
[CoreConstants.FEATURE_GROUPINGS]: false,
[CoreConstants.FEATURE_MOD_INTRO]: false,
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: false,
[CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: false,
[CoreConstants.FEATURE_MOD_PURPOSE]: ModPurpose.MOD_PURPOSE_CONTENT,
};
/**
* @inheritdoc
*/
getData(module: CoreCourseModuleData): CoreCourseModuleHandlerData {
return {
icon: CoreCourseModuleDelegate.getModuleIconSrc(module.modname, module.modicon),
title: module.name,
a11yTitle: '',
class: 'addon-mod-subsection-handler',
hasCustomCmListItem: true,
action: async(event, module) => {
try {
await AddonModSubsection.openSubsection(module.section, module.course);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error opening subsection.');
}
},
};
}
/**
* @inheritdoc
*/
async getMainComponent(): Promise<undefined> {
// There's no need to implement this because subsection cannot be used in singleactivity course format.
return;
}
/**
* @inheritdoc
*/
getIconSrc(): string {
return '';
}
}
export const AddonModSubsectionModuleHandler = makeSingleton(AddonModSubsectionModuleHandlerService);

View File

@ -0,0 +1,51 @@
// (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 { Injectable } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreSites } from '@services/sites';
import { makeSingleton } from '@singletons';
/**
* Service that provides some features for subsections.
*/
@Injectable({ providedIn: 'root' })
export class AddonModSubsectionProvider {
/**
* Open a subsection.
*
* @param sectionId Section ID.
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @returns Promise resolved when done.
*/
async openSubsection(sectionId: number, courseId: number, siteId?: string): Promise<void> {
const pageParams = {
sectionId,
};
if (
(!siteId || siteId === CoreSites.getCurrentSiteId()) &&
CoreCourse.currentViewIsCourse(courseId)
) {
CoreCourse.selectCourseTab('', pageParams);
} else {
await CoreCourseHelper.getAndOpenCourse(courseId, pageParams, siteId);
}
}
}
export const AddonModSubsection = makeSingleton(AddonModSubsectionProvider);

View File

@ -0,0 +1,33 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { AddonModSubsectionIndexLinkHandler } from './services/handlers/index-link';
import { AddonModSubsectionModuleHandler } from './services/handlers/module';
@NgModule({
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreCourseModuleDelegate.registerHandler(AddonModSubsectionModuleHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModSubsectionIndexLinkHandler.instance);
},
},
],
})
export class AddonModSubsectionModule {}

View File

@ -46,11 +46,9 @@ export class CoreMessageComponent implements OnInit {
protected deleted = false; // Needed to fix animation to void in Behat tests.
// @TODO Recover the animation using native css or wait for Angular 13.1
// where the bug https://github.com/angular/angular/issues/30693 is solved.
// @HostBinding('@coreSlideInOut') get animation(): string {
// return this.isMine ? '' : 'fromLeft';
// }
@HostBinding('@coreSlideInOut') get animation(): string {
return this.isMine ? '' : 'fromLeft';
}
@HostBinding('class.is-mine') isMine = false;

View File

@ -8,10 +8,13 @@
<core-loading [hideUntil]="loaded">
<!-- Single section. -->
<div *ngIf="selectedSection && selectedSection.id !== allSectionsId" class="single-section list-item-limited-width">
<div *ngIf="selectedSection && selectedSection.id !== allSectionsId" class="list-item-limited-width">
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
<ion-accordion-group [readonly]="true" value="single">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection, sectionId: 'single'}" />
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
[value]="accordionMultipleValue">
<core-course-section *ngIf="!selectedSection.hiddenbynumsections && selectedSection.id !== stealthModulesSectionId &&
!selectedSection.component" [course]="course" [section]="selectedSection" [lastModuleViewed]="lastModuleViewed"
[viewedModules]="viewedModules" [collapsible]="false" [subSections]="subSections" />
</ion-accordion-group>
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-table-cells-large"
[message]="'core.course.nocontentavailable' | translate" />
@ -19,13 +22,16 @@
</div>
<!-- Multiple sections. -->
<div *ngIf="selectedSection && selectedSection.id === allSectionsId" class="multiple-sections list-item-limited-width">
<div *ngIf="selectedSection && selectedSection.id === allSectionsId" class="list-item-limited-width">
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)" [value]="accordionMultipleValue"
#accordionMultiple>
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
[value]="accordionMultipleValue">
@for (section of sections; track section.id) {
@if ($index <= lastShownSectionIndex) {
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section, sectionId: section.id}" />
@if ($index <= lastShownSectionIndex && !section.hiddenbynumsections && section.id !== allSectionsId &&
section.id !== stealthModulesSectionId && !section.component) {
<core-course-section
[course]="course" [section]="section" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules"
[collapsible]="true" [subSections]="subSections" />
}
}
</ion-accordion-group>
@ -65,55 +71,3 @@
<ion-icon name="fas-list-ul" aria-hidden="true" />
</ion-fab-button>
</ion-fab>
<!-- Template to render a section. -->
<ng-template #sectionTemplate let-section="section" let-sectionId="sectionId">
<ion-accordion *ngIf="!section.hiddenbynumsections && section.id !== allSectionsId && section.id !== stealthModulesSectionId"
class="core-course-module-list-wrapper" [id]="section.id"
[attr.aria-labelledby]="section.name ? 'core-section-name-' + section.id : null" [value]="''+sectionId" toggleIconSlot="start">
<ion-item class="course-section divider" [class.item-dimmed]="section.visible === 0 || section.uservisible === false" slot="header">
<ion-label class="ion-text-wrap">
<h2 *ngIf="section.name" class="big" [id]="'core-section-name-' + section.id">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course.id" />
</h2>
<div *ngIf="section.visible === 0 && section.uservisible !== false">
<ion-badge color="warning">
{{ 'core.course.hiddenfromstudents' | translate }}
</ion-badge>
</div>
<div *ngIf="section.visible === 0 && section.uservisible === false">
<ion-badge color="warning">
{{ 'core.notavailable' | translate }}
</ion-badge>
</div>
<div *ngIf="section.availabilityinfo">
<ion-chip class="clickable">
<ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate" />
<ion-label>
<core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course.id" />
</ion-label>
</ion-chip>
</div>
</ion-label>
<ion-badge *ngIf="section.highlighted && highlighted" slot="end">{{highlighted}}</ion-badge>
</ion-item>
<div slot="content">
<ng-container *ngIf="section.expanded">
<ion-item class="ion-text-wrap section-summary" *ngIf="section.summary">
<ion-label>
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course.id" />
</ion-label>
</ion-item>
<ng-container *ngFor="let module of section.modules">
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
[showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
[isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id"
[class.core-course-module-not-viewed]="
!viewedModules[module.id] && (!module.completiondata || module.completiondata.state === completionStatusIncomplete)" />
</ng-container>
</ng-container>
</div>
</ion-accordion>
</ng-template>

View File

@ -10,30 +10,3 @@
margin-right: 4px;
}
}
.course-section {
--inner-padding-end: 12px;
}
.multiple-sections .core-course-module-list-wrapper {
border: var(--ion-card-border-width) solid var(--ion-card-border-color);
border-radius: var(--ion-card-radius);
margin: 8px 4px;
width: calc(100% - 8px);
ion-card {
--ion-card-background: transparent;
}
ion-item.divider.course-section {
--background: transparent;
}
}
.single-section ::ng-deep {
ion-item.divider.course-section {
ion-icon.ion-accordion-toggle-icon {
display: none;
}
}
}

View File

@ -11,7 +11,6 @@
// 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,
@ -31,7 +30,6 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import {
CoreCourse,
CoreCourseModuleCompletionStatus,
CoreCourseProvider,
} from '@features/course/services/course';
import {
@ -56,12 +54,12 @@ import { ContextLevel } from '@/core/constants';
import { CoreModals } from '@services/modals';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreBlockComponentsModule } from '@features/block/components/components.module';
import { CoreCourseComponentsModule } from '../components.module';
import { CoreSites } from '@services/sites';
import { COURSE_ALL_SECTIONS_PREFERRED_PREFIX, COURSE_EXPANDED_SECTIONS_PREFIX } from '@features/course/constants';
import { toBoolean } from '@/core/transforms/boolean';
import { CoreInfiniteLoadingComponent } from '@components/infinite-loading/infinite-loading';
import { CoreSite } from '@classes/sites/site';
import { CoreCourseSectionComponent, CoreCourseSectionToDisplay } from '../course-section/course-section';
/**
* Component to display course contents using a certain format. If the format isn't found, use default one.
@ -76,12 +74,12 @@ import { CoreSite } from '@classes/sites/site';
@Component({
selector: 'core-course-format',
templateUrl: 'course-format.html',
styleUrls: ['course-format.scss'],
styleUrl: 'course-format.scss',
standalone: true,
imports: [
CoreSharedModule,
CoreCourseSectionComponent,
CoreBlockComponentsModule,
CoreCourseComponentsModule,
],
})
export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
@ -126,16 +124,15 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
displayCourseIndex = false;
displayBlocks = false;
hasBlocks = false;
subSections: CoreCourseSectionToDisplay[] = []; // List of course subsections.
selectedSection?: CoreCourseSectionToDisplay;
previousSection?: CoreCourseSectionToDisplay;
nextSection?: CoreCourseSectionToDisplay;
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
allSectionsId = CoreCourseProvider.ALL_SECTIONS_ID;
stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
loaded = false;
highlighted?: string;
lastModuleViewed?: CoreCourseViewedModulesDBRecord;
viewedModules: Record<number, boolean> = {};
completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
communicationRoomUrl?: string;
@ -229,6 +226,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
if (changes.sections && this.sections) {
this.subSections = this.sections.filter((section) => section.component === 'mod_subsection');
this.sections = this.sections.filter((section) => section.component !== 'mod_subsection');
this.treatSections(this.sections);
}
this.changeDetectorRef.markForCheck();
@ -256,7 +256,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// Format has changed or it's the first time, load all the components.
this.lastCourseFormat = this.course.format;
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
currentSectionData.section.highlighted = true;
@ -301,8 +300,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @param sections Sections to treat.
*/
protected async treatSections(sections: CoreCourseSectionToDisplay[]): Promise<void> {
const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
const hasAllSections = sections[0].id === CoreCourseProvider.ALL_SECTIONS_ID;
const hasSeveralSections = sections.length > 2 || (sections.length === 2 && !hasAllSections);
await this.initializeViewedModules();
if (this.selectedSection) {
@ -327,6 +326,26 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.loaded = true;
this.sectionChanged(sections[0]);
} else if (this.initialSectionId || this.initialSectionNumber !== undefined) {
const subSection = this.subSections.find((section) => section.id === this.initialSectionId ||
(section.section !== undefined && section.section === this.initialSectionNumber));
if (subSection) {
// The section is a subsection, load the parent section.
this.sections.some((section) => {
const module = section.modules.find((module) =>
subSection.itemid === module.instance && module.modname === 'subsection');
if (module) {
this.initialSectionId = module.section;
this.initialSectionNumber = undefined;
return true;
}
return false;
});
this.setInputData();
}
// We have an input indicating the section ID to load. Search the section.
const section = sections.find((section) =>
section.id === this.initialSectionId ||
@ -469,6 +488,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
componentProps: {
course: this.course,
sections: this.sections,
subSections: this.subSections,
selectedId: selectedId,
},
});
@ -476,12 +496,25 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
if (!data) {
return;
}
const section = this.sections.find((section) => section.id === data.sectionId);
let section = this.sections.find((section) => section.id === data.sectionId);
if (!section) {
return;
}
this.sectionChanged(section);
if (data.subSectionId) {
section = this.subSections.find((section) => section.id === data.subSectionId);
if (!section) {
return;
}
// Use this section to find the module.
this.setSectionExpanded(section);
// Scroll to the subsection (later it may be scrolled to the module).
this.scrollInCourse(section.id, true);
}
if (!data.moduleId) {
return;
}
@ -496,7 +529,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
if (CoreCourseHelper.canUserViewModule(module, section)) {
this.scrollToModule(module.id);
this.scrollInCourse(module.id);
module.handlerData?.action?.(data.event, module, module.course);
}
@ -566,7 +599,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// Scroll to module if needed. Give more priority to the input.
const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
if (moduleIdToScroll) {
this.scrollToModule(moduleIdToScroll);
this.scrollInCourse(moduleIdToScroll);
}
if (!previousValue || previousValue.id !== newSection.id) {
@ -581,16 +614,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
/**
* Scroll to a certain module.
* Scroll to a certain module or section.
*
* @param moduleId Module ID.
* @param id ID of the module or section to scroll to.
* @param isSection Whether to scroll to a module or a subsection.
*/
protected scrollToModule(moduleId: number): void {
CoreDom.scrollToElement(
this.elementRef.nativeElement,
'#core-course-module-' + moduleId,
{ addYAxis: -10 },
);
protected scrollInCourse(id: number, isSection = false): void {
const elementId = isSection ? `#core-section-name-${id}` : `#core-course-module-${id}`;
CoreDom.scrollToElement(this.elementRef.nativeElement, elementId,{ addYAxis: -10 });
}
/**
@ -751,9 +782,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* Save expanded sections for the course.
*/
protected async saveExpandedSections(): Promise<void> {
const expandedSections = this.sections.filter((section) => section.expanded).map((section) => section.id).join(',');
let expandedSections = this.sections.filter((section) => section.expanded && section.id > 0).map((section) => section.id);
expandedSections =
expandedSections.concat(this.subSections.filter((section) => section.expanded).map((section) => section.id));
await this.currentSite?.setLocalSiteConfig(`${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`, expandedSections);
await this.currentSite?.setLocalSiteConfig(
`${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`,
expandedSections.join(','),
);
}
/**
@ -771,6 +807,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.accordionMultipleValue.push(section.id.toString());
});
this.subSections.forEach((section) => {
section.expanded = true;
this.accordionMultipleValue.push(section.id.toString());
});
return;
}
@ -779,6 +820,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.sections.forEach((section) => {
section.expanded = this.accordionMultipleValue.includes(section.id.toString());
});
this.subSections.forEach((section) => {
section.expanded = this.accordionMultipleValue.includes(section.id.toString());
});
}
/**
@ -792,9 +837,17 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
section.expanded = false;
});
this.subSections.forEach((section) => {
section.expanded = false;
});
sectionIds?.forEach((sectionId) => {
const sId = Number(sectionId);
const section = this.sections.find((section) => section.id === sId);
let section = this.sections.find((section) => section.id === sId);
if (!section) {
section = this.subSections.find((section) => section.id === sId);
}
if (section) {
section.expanded = true;
}
@ -820,8 +873,3 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
}
type CoreCourseSectionToDisplay = CoreCourseSection & {
highlighted?: boolean;
expanded?: boolean; // The aim of this property is to avoid DOM overloading.
};

View File

@ -24,59 +24,66 @@
</ion-label>
</ion-item>
<ng-container *ngIf="allSectionId !== section.id">
<ion-item class="divider section" (click)="selectSectionOrModule($event, section.id)" button
[class.item-current]="selectedId === section.id" [class.item-dimmed]="!section.visible"
[class.item-hightlighted]="section.highlighted" [detail]="false">
<ion-icon *ngIf="section.hasVisibleModules" name="fas-chevron-right" flip-rtl slot="start"
class="expandable-status-icon" (ariaButtonClick)="toggleExpand($event, section)"
[attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate"
[attr.aria-expanded]="section.expanded" [attr.aria-controls]="'core-course-index-section-' + section.id"
[class.expandable-status-icon-expanded]="section.expanded" />
<ion-icon *ngIf="!section.hasVisibleModules" name="" slot="start" aria-hidden="true"
class="expandable-status-icon" />
<ion-label>
<h2>
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id" />
</h2>
</ion-label>
<ion-badge *ngIf="section.highlighted && highlighted" slot="end">{{highlighted}}</ion-badge>
<ion-icon name="fas-lock" *ngIf="section.availabilityinfo" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate" />
<ion-icon name="fas-eye-slash" *ngIf="!section.visible && !section.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.notavailable' | translate" />
<ion-icon name="fas-eye-slash" *ngIf="!section.visible && section.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.course.hiddenfromstudents' | translate" />
</ion-item>
<div id="core-course-index-section-{{section.id}}">
<ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules">
<ion-item class="module" [class.item-dimmed]="!module.visible" [class.indented]="module.indented"
[class.item-hightlighted]="section.highlighted"
(click)="selectSectionOrModule($event, section.id, module.id)" button>
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
slot="start" aria-hidden="true" />
<ion-icon class="completioninfo completion_incomplete" name="far-circle"
*ngIf="module.completionStatus === 0" slot="start"
[attr.aria-label]="'core.course.todo' | translate" />
<ion-icon class="completioninfo completion_complete" name="fas-circle"
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
[attr.aria-label]="'core.course.done' | translate" />
<ion-icon class="completioninfo completion_fail" name="fas-xmark" *ngIf="module.completionStatus === 3"
color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate" />
<ion-label>
<p class="item-heading">
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="module.course" />
</p>
</ion-label>
<ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate" />
</ion-item>
</ng-container>
</ng-container>
</div>
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section}" />
</ng-container>
</ng-container>
</ion-list>
</core-loading>
</ion-content>
<ng-template #sectionTemplate let-section="section">
<ion-item class="divider section" (click)="selectSectionOrModule($event, section.id)" button
[class.item-current]="selectedId === section.id" [class.item-dimmed]="!section.visible"
[class.item-hightlighted]="section.highlighted" [detail]="false">
<ion-icon *ngIf="section.hasVisibleModules" name="fas-chevron-right" flip-rtl slot="start" class="expandable-status-icon"
(ariaButtonClick)="toggleExpand($event, section)"
[attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate" [attr.aria-expanded]="section.expanded"
[attr.aria-controls]="'core-course-index-section-' + section.id" [class.expandable-status-icon-expanded]="section.expanded" />
<ion-icon *ngIf="!section.hasVisibleModules" name="" slot="start" aria-hidden="true" class="expandable-status-icon" />
<ion-label>
<h2>
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id" />
</h2>
</ion-label>
<ion-badge *ngIf="section.highlighted && highlighted" slot="end">{{highlighted}}</ion-badge>
<ion-icon name="fas-lock" *ngIf="section.availabilityinfo" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate" />
<ion-icon name="fas-eye-slash" *ngIf="!section.visible && !section.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.notavailable' | translate" />
<ion-icon name="fas-eye-slash" *ngIf="!section.visible && section.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.course.hiddenfromstudents' | translate" />
</ion-item>
<div id="core-course-index-section-{{section.id}}" class="core-course-index-section-content">
<ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules">
@if (module.subSection) {
<div class="core-course-index-subsection">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: module.subSection}" />
</div>
} @else {
<ion-item class="module" [class.item-dimmed]="!module.visible" [class.indented]="module.indented"
[class.item-hightlighted]="section.highlighted" (click)="selectSectionOrModule($event, section.id, module.id)" button>
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined" slot="start"
aria-hidden="true" />
<ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="module.completionStatus === 0"
slot="start" [attr.aria-label]="'core.course.todo' | translate" />
<ion-icon class="completioninfo completion_complete" name="fas-circle"
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
[attr.aria-label]="'core.course.done' | translate" />
<ion-icon class="completioninfo completion_fail" name="fas-xmark" *ngIf="module.completionStatus === 3" color="danger"
slot="start" [attr.aria-label]="'core.course.failed' | translate" />
<ion-label>
<p class="item-heading">
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="module.course" />
</p>
</ion-label>
<ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate" />
</ion-item>
}
</ng-container>
</ng-container>
</div>
</ng-template>

View File

@ -1,6 +1,6 @@
@use "theme/globals" as *;
core-progress-bar {
--bar-margin: 8px 0 4px 0;
--bar-margin: 8px 0px 4px 0px;
--line-height: 20px;
--background: var(--contrast-background);
}
@ -19,7 +19,7 @@ ion-item.item {
&.item-current {
--background: var(--primary-tint);
--color: var(--gray-900);
border: 0;
border: 0px;
}
&.item-hightlighted {
@ -56,7 +56,7 @@ ion-item.item {
&.module {
&::part(native) {
--padding-start: 0;
--padding-start: 0px;
}
&.item-hightlighted ion-icon.completioninfo {
@ -70,7 +70,7 @@ ion-item.item {
}
ion-icon {
margin: 0;
margin: 0px;
padding: 12px 16px;
&.completioninfo {
@ -87,3 +87,7 @@ ion-item.item {
}
}
}
div.core-course-index-subsection {
@include padding-horizontal(16px, null);
}

View File

@ -20,7 +20,7 @@ import {
CoreCourseProvider,
} from '@features/course/services/course';
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
import { CoreSites } from '@services/sites';
@ -43,6 +43,7 @@ import { CoreDom } from '@singletons/dom';
export class CoreCourseCourseIndexComponent implements OnInit {
@Input() sections: CoreCourseSection[] = [];
@Input() subSections: CoreCourseSection[] = [];
@Input() selectedId?: number;
@Input() course?: CoreCourseAnyCourseData;
@ -87,38 +88,8 @@ export class CoreCourseCourseIndexComponent implements OnInit {
const enableIndentation = await CoreCourse.isCourseIndentationEnabled(site, this.course.id);
this.sectionsToRender = this.sections
.filter((section) => !CoreCourseHelper.isSectionStealth(section))
.map((section) => {
const modules = section.modules
.filter((module) => this.renderModule(section, module))
.map((module) => {
const completionStatus = completionEnabled
? CoreCourseHelper.getCompletionStatus(module.completiondata)
: undefined;
return {
id: module.id,
name: module.name,
course: module.course,
visible: !!module.visible,
uservisible: CoreCourseHelper.canUserViewModule(module, section),
indented: enableIndentation && module.indent > 0,
completionStatus,
};
});
return {
id: section.id,
name: section.name,
availabilityinfo: !!section.availabilityinfo,
visible: !!section.visible,
uservisible: CoreCourseHelper.canUserViewSection(section),
expanded: section.id === this.selectedId,
highlighted: currentSectionData.section.id === section.id,
hasVisibleModules: modules.length > 0,
modules: modules,
};
});
.filter((section) => section.component !== 'mod_subsection' && !CoreCourseHelper.isSectionStealth(section))
.map((section) => this.mapSectionToRender(section, completionEnabled, enableIndentation, currentSectionData));
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
@ -163,7 +134,26 @@ export class CoreCourseCourseIndexComponent implements OnInit {
* @param moduleId Selected module id, if any.
*/
selectSectionOrModule(event: Event, sectionId: number, moduleId?: number): void {
ModalController.dismiss({ event, sectionId, moduleId });
let subSectionId: number | undefined;
this.sectionsToRender.some((section) => {
if (section.id === sectionId) {
return true;
}
return section.modules.some((module) => {
if (module.subSection?.id === sectionId) {
// Always use the parent section.
subSectionId = sectionId;
sectionId = section.id;
return true;
}
return false;
});
});
ModalController.dismiss({ event, sectionId, subSectionId, moduleId });
}
/**
@ -187,6 +177,69 @@ export class CoreCourseCourseIndexComponent implements OnInit {
return !module.noviewlink;
}
/**
* Map a section to the format needed to render it.
*
* @param section Section to map.
* @param completionEnabled Whether completion is enabled.
* @param enableIndentation Whether indentation is enabled.
* @param currentSectionData Current section data.
* @returns Mapped section.
*/
protected mapSectionToRender(
section: CoreCourseSection,
completionEnabled: boolean,
enableIndentation: boolean,
currentSectionData?: CoreCourseFormatCurrentSectionData<CoreCourseSection>,
): CourseIndexSection {
const modules = section.modules
.filter((module) => module.modname === 'subsection' || this.renderModule(section, module))
.map((module) => {
if (module.modname === 'subsection') {
const subSectionFound = this.subSections.find((subSection) => subSection.itemid === module.instance);
const subSection = subSectionFound
? this.mapSectionToRender(subSectionFound, completionEnabled, enableIndentation)
: undefined;
return {
id: module.id,
name: module.name,
course: module.course,
visible: !!module.visible,
uservisible: CoreCourseHelper.canUserViewModule(module, section),
indented: true,
subSection,
};
}
const completionStatus = completionEnabled
? CoreCourseHelper.getCompletionStatus(module.completiondata)
: undefined;
return {
id: module.id,
name: module.name,
course: module.course,
visible: !!module.visible,
uservisible: CoreCourseHelper.canUserViewModule(module, section),
indented: enableIndentation && module.indent > 0,
completionStatus,
};
});
return {
id: section.id,
name: section.name,
availabilityinfo: !!section.availabilityinfo,
visible: !!section.visible,
uservisible: CoreCourseHelper.canUserViewSection(section),
expanded: section.id === this.selectedId,
highlighted: currentSectionData?.section.id === section.id,
hasVisibleModules: modules.length > 0,
modules,
};
}
}
type CourseIndexSection = {
@ -205,11 +258,13 @@ type CourseIndexSection = {
indented: boolean;
uservisible: boolean;
completionStatus?: CoreCourseModuleCompletionStatus;
subSection?: CourseIndexSection;
}[];
};
export type CoreCourseIndexSectionWithModule = {
event: Event;
sectionId: number;
subSectionId?: number;
moduleId?: number;
};

View File

@ -0,0 +1,68 @@
@if (collapsible) {
<ion-accordion class="core-course-module-list-wrapper" [id]="section.id"
[attr.aria-labelledby]="section.name ? 'core-section-name-' + section.id : null" [value]="section.id" toggleIconSlot="start">
<ng-container *ngTemplateOutlet="sectionHeader" />
<div slot="content">
<ng-container *ngIf="section.expanded">
<ng-container *ngTemplateOutlet="sectionContent" />
</ng-container>
</div>
</ion-accordion>
} @else {
<div class="core-course-module-list-wrapper" [id]="section.id"
[attr.aria-labelledby]="section.name ? 'core-section-name-' + section.id : null">
<ng-container *ngTemplateOutlet="sectionHeader" />
<ng-container *ngTemplateOutlet="sectionContent" />
</div>
}
<ng-template #sectionHeader>
<ion-item class="course-section divider" [class.item-dimmed]="section.visible === 0 || section.uservisible === false" slot="header">
<ion-label class="ion-text-wrap">
<h2 *ngIf="section.name" class="big" [id]="'core-section-name-' + section.id">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course.id" />
</h2>
<div *ngIf="section.visible === 0 && section.uservisible !== false">
<ion-badge color="warning">
{{ 'core.course.hiddenfromstudents' | translate }}
</ion-badge>
</div>
<div *ngIf="section.visible === 0 && section.uservisible === false">
<ion-badge color="warning">
{{ 'core.notavailable' | translate }}
</ion-badge>
</div>
<div *ngIf="section.availabilityinfo">
<ion-chip class="clickable">
<ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate" />
<ion-label>
<core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course.id" />
</ion-label>
</ion-chip>
</div>
</ion-label>
<ion-badge *ngIf="section.highlighted && highlightedName" slot="end">{{highlightedName}}</ion-badge>
</ion-item>
</ng-template>
<ng-template #sectionContent>
<ion-item class="ion-text-wrap section-summary" *ngIf="section.summary">
<ion-label>
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course.id" />
</ion-label>
</ion-item>
<ng-container *ngFor="let module of modules">
@if (module.subsection) {
<core-course-section [course]="course" [section]="module.subsection" [lastModuleViewed]="lastModuleViewed"
[viewedModules]="viewedModules" [collapsible]="true" />
} @else {
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
[showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
[isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id" [class.core-course-module-not-viewed]="
!viewedModules[module.id] &&
(!module.completiondata || module.completiondata.state === completionStatusIncomplete)" />
}
</ng-container>
</ng-template>

View File

@ -0,0 +1,36 @@
:host {
.course-section {
--inner-padding-end: var(--mdl-spacing-3);
}
&.collapsible {
.core-course-module-list-wrapper {
border: var(--ion-card-border-width) solid var(--ion-card-border-color);
border-radius: var(--ion-card-radius);
margin: var(--mdl-spacing-4);
width: calc(100% - var(--mdl-spacing-8));
ion-card {
--ion-card-background: transparent;
}
ion-item.divider.course-section {
--background: transparent;
}
}
}
core-course-module:has( + core-course-section) {
--activity-border: 0px;
--card-padding-bottom: 0px;
}
core-course-module:last-child {
--activity-border: 0px;
--card-padding-bottom: 0px;
}
core-course-module:first-child ::ng-deep ion-card {
margin-top: 0px;
}
}

View File

@ -0,0 +1,99 @@
// (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,
HostBinding,
Input,
OnChanges,
OnInit,
SimpleChange,
} from '@angular/core';
import {
CoreCourseModuleData,
CoreCourseSection,
} from '@features/course/services/course-helper';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseComponentsModule } from '../components.module';
import { toBoolean } from '@/core/transforms/boolean';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
import { CoreCourseModuleCompletionStatus } from '@features/course/services/course';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
/**
* Component to display course section.
*/
@Component({
selector: 'core-course-section',
templateUrl: 'course-section.html',
styleUrl: 'course-section.scss',
standalone: true,
imports: [
CoreSharedModule,
CoreCourseComponentsModule,
],
})
export class CoreCourseSectionComponent implements OnInit, OnChanges {
@Input({ required: true }) course!: CoreCourseAnyCourseData; // The course to render.
@Input({ required: true }) section!: CoreCourseSectionToDisplay;
@Input() subSections: CoreCourseSectionToDisplay[] = []; // List of subsections in the course.
@Input({ transform: toBoolean }) collapsible = true; // Whether the section can be collapsed.
@Input() lastModuleViewed?: CoreCourseViewedModulesDBRecord;
@Input() viewedModules: Record<number, boolean> = {};
@HostBinding('class')
get collapsibleClass(): string {
return this.collapsible ? 'collapsible' : 'non-collapsible';
}
modules: CoreCourseModuleToDisplay[] = [];
completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
highlightedName?: string; // Name to highlight.
/**
* @inheritdoc
*/
ngOnInit(): void {
this.highlightedName = this.section.highlighted && this.highlightedName === undefined
? CoreCourseFormatDelegate.getSectionHightlightedName(this.course)
: undefined;
}
/**
* @inheritdoc
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (changes.section && this.section) {
this.modules = this.section.modules;
this.modules.forEach((module) => {
if (module.modname === 'subsection') {
module.subSection = this.subSections.find((section) =>
section.component === 'mod_subsection' && section.itemid === module.instance);
}
});
}
}
}
type CoreCourseModuleToDisplay = CoreCourseModuleData & {
subSection?: CoreCourseSectionToDisplay;
};
export type CoreCourseSectionToDisplay = CoreCourseSection & {
highlighted?: boolean;
expanded?: boolean; // The aim of this property is to avoid DOM overloading.
};

View File

@ -81,7 +81,7 @@
contextLevel="module" [contextInstanceId]="module.id" [courseId]="module.course" />
<!-- Availability info -->
<div *ngIf="showAvailability && module.availabilityinfo" class="core-module-availabilityinfo">
<div *ngIf="showAvailability && module.availabilityinfo" class="core-module-availabilityinfo colored-box-with-icon">
<ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate" />
<core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="module.course" />
@ -94,7 +94,7 @@
</ion-item>
<div class="core-course-last-module-viewed" *ngIf="isLastViewed">
<div class="core-course-last-module-viewed colored-box-with-icon" *ngIf="isLastViewed">
<ion-icon name="fas-eye" aria-hidden="true" />
{{ 'core.course.lastaccessedactivity' | translate }}
</div>

View File

@ -1,29 +1,32 @@
@use "theme/globals" as *;
:host {
--horizontal-margin: 12px;
--vertical-margin: 12px;
--card-padding: 16px;
--horizontal-spacing: var(--mdl-spacing-4);
--vertical-spacing: var(--mdl-spacing-2);
--colored-box-padding: var(--mdl-spacing-2);
--card-border-width: 0px;
--card-radius: 0px;
--card-background: transparent;
--activity-border: 2px solid var(--stroke);
--card-padding-bottom: var(--vertical-spacing);
ion-card {
margin: var(--vertical-margin) var(--horizontal-margin);
margin: var(--vertical-spacing) var(--horizontal-spacing);
padding: 0px;
--ion-card-border-width: var(--card-border-width);
--ion-card-radius: var(--card-radius);
--ion-card-background: var(--card-background);
padding-bottom: var(--card-padding-bottom);
border-bottom: var(--activity-border);
}
ion-item {
--padding-start: var(--card-padding);
--inner-padding-end: var(--card-padding);
--padding-start: 0px;
--inner-padding-end: 0px;
--background: transparent;
ion-label {
margin-top: var(--card-padding);
margin-bottom: var(--card-padding);
&>:last-child {
margin-bottom: 0px;
}
margin-top: 0px;
margin-bottom: 0px;
}
}
@ -33,8 +36,6 @@
flex-direction: row;
core-mod-icon {
margin-top: 0px;
margin-bottom: 0px;
--module-icon-padding: 0px;
--module-legacy-icon-padding: 4px;
--module-icon-radius: var(--mdl-shape-borderRadius-xs);
@ -46,7 +47,7 @@
.activity-title {
flex-grow: 1;
align-self: center;
@include margin-horizontal(null, var(--card-padding));
@include margin-horizontal(null, var(--horizontal-spacing));
.item-heading ion-icon {
@include margin-horizontal(8px, null);
@ -56,7 +57,6 @@
.core-module-buttons {
align-self: self-start;
margin: 0;
display: flex;
flex-flow: row;
@ -86,6 +86,13 @@
}
}
core-mod-icon,
.activity-title,
.core-module-buttons {
margin-top: var(--vertical-spacing);
margin-bottom: var(--vertical-spacing);
}
.core-module-additional-info {
display: flex;
align-items: center;
@ -106,7 +113,7 @@
}
core-course-module-completion {
--margin: 8px 0px;
--margin: var(--vertical-spacing) 0px;
}
.activity-dates {
@ -121,9 +128,14 @@
.activity-description-availabilityinfo,
.activity-extrabadges {
margin-top: 8px;
padding-top: 8px;
margin-top: var(--vertical-spacing);
margin-bottom: var(--vertical-spacing);
}
.activity-extrabadges,
.core-module-description {
border-top: 1px solid var(--stroke);
display: block;
}
.activity-extrabadges {
@ -131,26 +143,16 @@
color: var(--medium);
}
.activity-description-availabilityinfo {
.core-module-availabilityinfo {
background: var(--gray-300);
border-radius: var(--mdl-shape-borderRadius-sm);
.activity-description-availabilityinfo .core-module-availabilityinfo {
background: var(--gray-300);
font: var(--mdl-typography-label-font-lg);
::ng-deep ul {
margin-top: 8px;
padding: 8px;
font-size: var(--mdl-typography-body-fontSize-md);
line-height: 120%;
margin-bottom: 0px;
::ng-deep ul {
margin-top: 8px;
margin-bottom: 0px;
li {
margin-bottom: 4px;
}
}
ion-icon {
@include margin-horizontal(null, 8px);
li {
margin-bottom: 4px;
}
}
}
@ -162,13 +164,25 @@
clear: both;
}
.core-course-last-module-viewed {
padding: 8px 12px;
color: var(--subdued-text-color);
border-top: 1px solid var(--stroke);
.colored-box-with-icon {
margin-top: var(--vertical-spacing);
margin-bottom: var(--vertical-spacing);
border: 0px;
padding: var(--colored-box-padding);
border-radius: var(--mdl-shape-borderRadius-sm);
ion-icon {
margin-right: 4px;
@include margin-horizontal(null, var(--mdl-spacing-2));
margin-top: auto;
margin-bottom: auto;
}
&.core-course-last-module-viewed {
display: flex;
background-color: var(--info-tint);
color: var(--info-shade);
font: var(--mdl-typography-label-font-md);
}
}
@ -185,17 +199,14 @@
}
&.indented ion-card {
@include margin-horizontal(calc(var(--horizontal-margin) + 1rem), null);
}
& + ::ng-deep core-course-module ion-card {
border-top: 1px solid var(--ion-card-border-color);
@include margin-horizontal(calc(var(--horizontal-spacing) + 1rem), null);
}
// Hide download folder icon meanwhile MOBILE-4147 is not solved
core-format-text.core-module-description ::ng-deep .description-inner .navitem {
display: none;
}
}

View File

@ -990,7 +990,7 @@ export class CoreCourseProvider {
map(sections => {
const siteHomeId = site.getSiteHomeId();
let showSections = true;
if (courseId == siteHomeId) {
if (courseId === siteHomeId) {
const storedNumSections = site.getStoredConfig('numsections');
showSections = storedNumSections !== undefined && !!storedNumSections;
}
@ -1770,6 +1770,8 @@ type CoreCourseGetContentsWSSection = {
uservisible?: boolean; // Is the section visible for the user?.
availabilityinfo?: string; // Availability information.
modules: CoreCourseGetContentsWSModule[]; // List of module.
component?: string; // @since 4.5 The delegate component of this section if any.
itemid?: number; // @since 4.5 The optional item id delegate component can use to identify its instance.
};
/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,5 +1,16 @@
ion-accordion {
.ion-accordion-toggle-icon[slot="start"] {
@include margin-horizontal(null, var(--mdl-spacing-4));
@include margin-horizontal(null, var(--mdl-spacing-2));
background-color: var(--gray-100);
border-radius: 50%;
padding: var(--mdl-spacing-1);
}
}
:root.dark {
ion-accordion {
.ion-accordion-toggle-icon[slot="start"] {
background-color: var(--gray-800);
}
}
}

View File

@ -310,15 +310,15 @@
display: block;
ion-card {
--vertical-margin: 10px;
--horizontal-margin: 10px;
--vertical-spacing: 10px;
--horizontal-spacing: 10px;
width: calc(100% - var(--horizontal-margin) - var(--horizontal-margin));
height: calc(100% - var(--vertical-margin) - var(--vertical-margin));
margin: var(--vertical-margin) var(--horizontal-margin);
width: calc(100% - var(--horizontal-spacing) - var(--horizontal-spacing));
height: calc(100% - var(--vertical-spacing) - var(--vertical-spacing));
margin: var(--vertical-spacing) var(--horizontal-spacing);
@media (max-width: 360px) {
--horizontal-margin: 6px;
--horizontal-spacing: 6px;
}
}
}