commit
61cb0428f9
Binary file not shown.
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
@ -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,
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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 {}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue