MOBILE-3659 course: Implement format, module and completion components
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]>
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
</defs>
|
||||
<path style="fill:#999999;" d="M10,0v2H6V0H10z M11,2h1c1.1,0,2,0.9,2,2v1h2V2c0-1.1-0.9-2-2-2h-3V2z M16,6h-2v4h2V6z M14,11v1
|
||||
c0,1.1-0.9,2-2,2h-1v2h3c1.1,0,2-0.9,2-2v-3H14z M10,16v-2H6v2H10z M5,14H4c-1.1,0-2-0.9-2-2v-1H0v3c0,1.1,0.9,2,2,2h3V14z M0,10h2
|
||||
V6H0V10z M2,5V4c0-1.1,0.9-2,2-2h1V0H2C0.9,0,0,0.9,0,2v3H2z"/>
|
||||
<path style="fill:#FF403C;" d="M10.2,8l2.6-2.6c0.4-0.4,0.4-1,0-1.4L12,3.3c-0.4-0.4-1-0.4-1.4,0L8,5.9L5.4,3.3
|
||||
c-0.4-0.4-1-0.4-1.4,0L3.3,4c-0.4,0.4-0.4,1,0,1.4L5.9,8l-2.6,2.6C3,11,3,11.6,3.3,12l0.7,0.7c0.4,0.4,1,0.4,1.4,0L8,10.2l2.5,2.5
|
||||
c0.4,0.4,1,0.4,1.4,0l0.7-0.7c0.4-0.4,0.4-1,0-1.4L10.2,8z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c1.1 0 2 .9 2 2v1h2V2c0-1.1-.9-2-2-2h-3v2zm5 4h-2v4h2V6zm-2 5v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#FF2727"/></svg>
|
After Width: | Height: | Size: 579 B |
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]>
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
</defs>
|
||||
<path style="fill:#999999;" d="M10,0v2H6V0H10z M11,2h1c1.1,0,2,0.9,2,2v1h2V2c0-1.1-0.9-2-2-2h-3V2z M16,6h-2v4h2V6z M14,11v1
|
||||
c0,1.1-0.9,2-2,2h-1v2h3c1.1,0,2-0.9,2-2v-3H14z M10,16v-2H6v2H10z M5,14H4c-1.1,0-2-0.9-2-2v-1H0v3c0,1.1,0.9,2,2,2h3V14z M0,10h2
|
||||
V6H0V10z M2,5V4c0-1.1,0.9-2,2-2h1V0H2C0.9,0,0,0.9,0,2v3H2z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 955 B |
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]>
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
</defs>
|
||||
<path style="fill:#999999;" d="M10,0v2H6V0H10z M11,2h1c0.4,0,0.8,0.1,1.1,0.3c0.3-0.3,0.8-0.4,1.2-0.4c0.5,0,1,0.2,1.4,0.6L16,2.8
|
||||
V2c0-1.1-0.9-2-2-2h-3V2z M14,10h2V6.4l-2,2V10z M14,11v1c0,1.1-0.9,2-2,2h-1v2h3c1.1,0,2-0.9,2-2v-3H14z M10,16v-2H6v2H10z M5,14H4
|
||||
c-1.1,0-2-0.9-2-2v-1H0v3c0,1.1,0.9,2,2,2h3V14z M0,10h2V6H0V10z M2,5V4c0-1.1,0.9-2,2-2h1V0H2C0.9,0,0,0.9,0,2v3H2z"/>
|
||||
<path style="fill:#99CC33;" d="M15.7,3.9L15,3.2c-0.4-0.4-1-0.4-1.4,0l-6,6L5.4,7C5,6.7,4.4,6.7,4,7L3.3,7.7c-0.4,0.4-0.4,1,0,1.4
|
||||
l3.6,3.6c0.4,0.4,1,0.4,1.4,0l7.4-7.4C16.1,4.9,16.1,4.3,15.7,3.9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2h-3v2zm3 8h2V6.4l-2 2V10zm0 1v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#FF2727"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
|
After Width: | Height: | Size: 779 B |
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]>
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
</defs>
|
||||
<path style="fill:#999999;" d="M10,0v2H6V0H10z M11,2h1c0.4,0,0.8,0.1,1.1,0.3c0.3-0.3,0.8-0.4,1.2-0.4c0.5,0,1,0.2,1.4,0.6L16,2.8
|
||||
V2c0-1.1-0.9-2-2-2h-3V2z M14,10h2V6.4l-2,2V10z M14,11v1c0,1.1-0.9,2-2,2h-1v2h3c1.1,0,2-0.9,2-2v-3H14z M10,16v-2H6v2H10z M5,14H4
|
||||
c-1.1,0-2-0.9-2-2v-1H0v3c0,1.1,0.9,2,2,2h3V14z M0,10h2V6H0V10z M2,5V4c0-1.1,0.9-2,2-2h1V0H2C0.9,0,0,0.9,0,2v3H2z"/>
|
||||
<path style="fill:#76A1F0;" d="M15.7,3.9L15,3.2c-0.4-0.4-1-0.4-1.4,0l-6,6L5.4,7C5,6.7,4.4,6.7,4,7L3.3,7.7c-0.4,0.4-0.4,1,0,1.4
|
||||
l3.6,3.6c0.4,0.4,1,0.4,1.4,0l7.4-7.4C16.1,4.9,16.1,4.3,15.7,3.9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M14 0H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V2c0-1.1-.9-2-2-2zm0 12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8c1.1 0 2 .9 2 2v8z" fill="#FF2727"/></svg>
|
After Width: | Height: | Size: 476 B |
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]>
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
</defs>
|
||||
<path style="fill:#999999;" d="M14,0H2C0.9,0,0,0.9,0,2v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V2C16,0.9,15.1,0,14,0z M14,12
|
||||
c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V4c0-1.1,0.9-2,2-2h8c1.1,0,2,0.9,2,2V12z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 841 B |
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M14 8.4V12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V6.4l-2 2z" fill="#FF2727"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
|
After Width: | Height: | Size: 682 B |
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]>
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
</defs>
|
||||
<path style="fill:#999999;" d="M14,8.4V12c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V4c0-1.1,0.9-2,2-2h8c0.4,0,0.8,0.1,1.1,0.3
|
||||
c0.3-0.3,0.8-0.4,1.2-0.4c0.5,0,1,0.2,1.4,0.6L16,2.8V2c0-1.1-0.9-2-2-2H2C0.9,0,0,0.9,0,2v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2
|
||||
V6.4L14,8.4z"/>
|
||||
<path style="fill:#76A1F0;" d="M15.7,3.9L15,3.2c-0.4-0.4-1-0.4-1.4,0l-6,6L5.4,7C5,6.7,4.4,6.7,4,7L3.3,7.7c-0.4,0.4-0.4,1,0,1.4
|
||||
l3.6,3.6c0.4,0.4,1,0.4,1.4,0l7.4-7.4C16.1,4.9,16.1,4.3,15.7,3.9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -18,6 +18,7 @@ import { CoreDomUtils } from '@services/utils/dom';
|
|||
import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
|
||||
import { CoreBlockHelper } from '../../services/block-helper';
|
||||
import { CoreBlockComponent } from '../block/block';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
/**
|
||||
* Component that displays the list of course blocks.
|
||||
|
@ -108,4 +109,15 @@ export class CoreBlockCourseBlocksComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh data.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async doRefresh(): Promise<void> {
|
||||
await CoreUtils.instance.ignoreErrors(this.invalidateBlocks());
|
||||
|
||||
await this.loadContent();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,11 +19,17 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreBlockComponentsModule } from '@features/block/components/components.module';
|
||||
import { CoreCourseFormatComponent } from './format/format';
|
||||
import { CoreCourseModuleComponent } from './module/module';
|
||||
import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion';
|
||||
import { CoreCourseModuleDescriptionComponent } from './module-description/module-description';
|
||||
import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCourseFormatComponent,
|
||||
CoreCourseModuleComponent,
|
||||
CoreCourseModuleCompletionComponent,
|
||||
CoreCourseModuleDescriptionComponent,
|
||||
CoreCourseUnsupportedModuleComponent,
|
||||
],
|
||||
|
@ -35,6 +41,9 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup
|
|||
CoreSharedModule,
|
||||
],
|
||||
exports: [
|
||||
CoreCourseFormatComponent,
|
||||
CoreCourseModuleComponent,
|
||||
CoreCourseModuleCompletionComponent,
|
||||
CoreCourseModuleDescriptionComponent,
|
||||
CoreCourseUnsupportedModuleComponent,
|
||||
],
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
<!-- Buttons to add to the header. *ngIf is needed, otherwise the component is executed too soon and doesn't find the header. -->
|
||||
<core-navbar-buttons slot="end" *ngIf="loaded">
|
||||
<core-context-menu>
|
||||
<core-context-menu-item [hidden]="!displaySectionSelector || !sections || !sections.length" [priority]="500"
|
||||
[content]="'core.course.sections' | translate" (action)="showSectionSelector($event)" iconAction="menu">
|
||||
</core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
<core-block-course-blocks *ngIf="loaded" [courseId]="course!.id" [hideBlocks]="!displayBlocks" [downloadEnabled]="downloadEnabled"
|
||||
[hideBottomBlocks]="selectedSection && selectedSection.id == allSectionsId && canLoadMore">
|
||||
|
||||
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
|
||||
<!-- Default course format. -->
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<!-- Section selector. -->
|
||||
<core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
|
||||
|
||||
<div *ngIf="displaySectionSelector && sections && hasSeveralSections"
|
||||
class="ion-text-wrap clearfix ion-justify-content-between core-button-selector-row"
|
||||
[class.core-section-download]="downloadEnabled">
|
||||
<ion-button class="ion-float-start" (click)="showSectionSelector($event)" color="light" aria-haspopup="true"
|
||||
class="core-button-select button-no-uppercase" [attr.aria-expanded]="sectionSelectorExpanded"
|
||||
id="core-course-section-button" expand="block"> <!-- @todo: attr.aria-label? -->
|
||||
<core-icon name="fas-folder" slot="start"></core-icon>
|
||||
<span class="core-button-select-text">
|
||||
<core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
|
||||
[contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
|
||||
</core-format-text>
|
||||
<span *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</span>
|
||||
</span>
|
||||
<core-icon name="fas-chevron-down" slot="end"></core-icon>
|
||||
</ion-button>
|
||||
<!-- Section download. -->
|
||||
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
|
||||
</div>
|
||||
</core-dynamic-component>
|
||||
|
||||
<!-- Course summary. By default we only display the course progress. -->
|
||||
<core-dynamic-component [component]="courseSummaryComponent" [data]="data">
|
||||
<ion-list lines="none" class="core-format-progress-list"
|
||||
*ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) ||
|
||||
(selectedSection && selectedSection.id != allSectionsId &&
|
||||
(selectedSection.availabilityinfo || selectedSection.visible === 0))">
|
||||
<div *ngIf="imageThumb" class="core-course-thumb">
|
||||
<img [src]="imageThumb" core-external-content alt=""/>
|
||||
</div>
|
||||
<ng-container *ngIf="selectedSection">
|
||||
<ion-item class="core-course-progress"
|
||||
*ngIf="selectedSection?.id == allSectionsId && progress !== undefined">
|
||||
<core-progress-bar [progress]="progress"></core-progress-bar>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="selectedSection && selectedSection.id != allSectionsId &&
|
||||
(selectedSection.availabilityinfo || selectedSection.visible === 0)">
|
||||
<ion-badge color="secondary" class="ion-text-wrap"
|
||||
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible !== false">
|
||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="secondary" class="ion-text-wrap"
|
||||
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible === false">
|
||||
{{ 'core.notavailable' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="secondary" class="ion-text-wrap" *ngIf="selectedSection.availabilityinfo">
|
||||
<core-format-text [text]="selectedSection.availabilityinfo" contextLevel="course"
|
||||
[contextInstanceId]="course?.id">
|
||||
</core-format-text>
|
||||
</ion-badge>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</core-dynamic-component>
|
||||
|
||||
<!-- Single section. -->
|
||||
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
|
||||
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
|
||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
|
||||
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-th-large"
|
||||
[message]="'core.course.nocontentavailable' | translate">
|
||||
</core-empty-box>
|
||||
</core-dynamic-component>
|
||||
</div>
|
||||
|
||||
<!-- Multiple sections. -->
|
||||
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
|
||||
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
|
||||
<ng-container *ngFor="let section of sections; index as i">
|
||||
<ng-container *ngIf="i <= showSectionId">
|
||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</core-dynamic-component>
|
||||
|
||||
<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading>
|
||||
</div>
|
||||
|
||||
<ion-buttons class="ion-padding" class="core-course-section-nav-buttons safe-padding-horizontal"
|
||||
*ngIf="displaySectionSelector && sections?.length">
|
||||
<ion-button *ngIf="previousSection" color="light" (click)="sectionChanged(previousSection)"
|
||||
title="{{ 'core.previous' | translate }}">
|
||||
<core-icon name="fas-chevron-left" slot="icon-only"></core-icon>
|
||||
<core-format-text class="accesshide" [text]="previousSection.name" contextLevel="course"
|
||||
[contextInstanceId]="course?.id">
|
||||
</core-format-text>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" title="{{ 'core.next' | translate }}">
|
||||
<core-icon name="fas-chevron-right" slot="icon-only"></core-icon>
|
||||
<core-format-text class="accesshide" [text]="nextSection.name" contextLevel="course"
|
||||
[contextInstanceId]="course?.id">
|
||||
</core-format-text>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</core-loading>
|
||||
</core-dynamic-component>
|
||||
</core-block-course-blocks>
|
||||
|
||||
<!-- Template to render a section. -->
|
||||
<ng-template #sectionTemplate let-section="section">
|
||||
<section ion-list *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId">
|
||||
<!-- Title is only displayed when viewing all sections. -->
|
||||
<ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light"
|
||||
[class.core-section-download]="downloadEnabled"
|
||||
[class.item-dimmed]="section.visible === 0 || section.uservisible === false">
|
||||
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text>
|
||||
<!-- Section download. -->
|
||||
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: section}"></ng-container>
|
||||
<p *ngIf="section.visible === 0 || section.availabilityinfo">
|
||||
<ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap">
|
||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap">
|
||||
{{ 'core.notavailable' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="secondary" *ngIf="section.availabilityinfo" class="ion-text-wrap">
|
||||
<core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
</core-format-text>
|
||||
</ion-badge>
|
||||
</p>
|
||||
</ion-item-divider>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="section.summary">
|
||||
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [courseId]="course?.id"
|
||||
[downloadEnabled]="downloadEnabled" [section]="section" (completionChanged)="onCompletionChange($event)"
|
||||
(statusChanged)="onModuleStatusChange($event)">
|
||||
</core-course-module>
|
||||
</ng-container>
|
||||
</section>
|
||||
</ng-template>
|
||||
|
||||
<!-- Template to render a section download button/progress. -->
|
||||
<ng-template #sectionDownloadTemplate let-section="section">
|
||||
<div *ngIf="section && downloadEnabled" class="core-button-spinner ion-float-end">
|
||||
<!-- Download progress. -->
|
||||
<ion-badge class="core-course-download-section-progress"
|
||||
*ngIf="section.isDownloading && section.total > 0 && section.count < section.total">
|
||||
{{section.count}} / {{section.total}}
|
||||
</ion-badge>
|
||||
|
||||
<core-download-refresh [status]="section.downloadStatus" [enabled]="downloadEnabled" (action)="prefetch(section)"
|
||||
[loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="section.canCheckUpdates">
|
||||
</core-download-refresh>
|
||||
</div>
|
||||
</ng-template>
|
|
@ -0,0 +1,84 @@
|
|||
// ion-app.app-root ion-badge.core-course-download-section-progress {
|
||||
// display: block;
|
||||
// @include float(start);
|
||||
// @include margin(12px, 12px, null, 12px);
|
||||
// }
|
||||
|
||||
:host {
|
||||
|
||||
.core-format-progress-list {
|
||||
margin-bottom: 0;
|
||||
|
||||
.item {
|
||||
background: transparent;
|
||||
|
||||
.label {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
progress {
|
||||
.progress-bar-fallback,
|
||||
&[value]::-webkit-progress-bar {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-course-thumb {
|
||||
display: none;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
background: white;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// .item-divider {
|
||||
// .label {
|
||||
// margin-top: 0;
|
||||
// margin-bottom: 0;
|
||||
// }
|
||||
|
||||
// core-format-text {
|
||||
// line-height: 44px;
|
||||
// }
|
||||
|
||||
// ion-badge core-format-text {
|
||||
// line-height: normal;
|
||||
// margin-bottom: 9px;
|
||||
// }
|
||||
|
||||
// &.core-section-download .label{
|
||||
// @include margin(null, 0, null, null);
|
||||
// }
|
||||
// }
|
||||
|
||||
// div.core-section-download {
|
||||
// @include padding(null, 0, null, null);
|
||||
// }
|
||||
|
||||
// .core-button-selector-row {
|
||||
// @include safe-area-padding-start($content-padding !important, $content-padding);
|
||||
// }
|
||||
|
||||
// .core-course-section-nav-buttons {
|
||||
// .button-inner core-format-text {
|
||||
// white-space: nowrap;
|
||||
// text-overflow: ellipsis;
|
||||
// overflow: hidden;
|
||||
// text-transform: none;
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,617 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
SimpleChange,
|
||||
Output,
|
||||
EventEmitter,
|
||||
ViewChildren,
|
||||
QueryList,
|
||||
Type,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
import {
|
||||
CoreCourse,
|
||||
CoreCourseModuleCompletionData,
|
||||
CoreCourseModuleData,
|
||||
CoreCourseProvider,
|
||||
} from '@features/course/services/course';
|
||||
// import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||
import { CoreEventObserver, CoreEvents, CoreEventSectionStatusChangedData, CoreEventSelectCourseTabData } from '@singletons/events';
|
||||
import { IonContent, IonRefresher } from '@ionic/angular';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
// import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
|
||||
import { CoreCourseSectionFormatted } from '@features/course/services/course-helper';
|
||||
import { CoreCourseModuleStatusChangedData } from '../module/module';
|
||||
|
||||
/**
|
||||
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
||||
*
|
||||
* The inputs of this component will be shared with the course format components. Please use CoreCourseFormatDelegate
|
||||
* to register your handler for course formats.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-course-format [course]="course" [sections]="sections" (completionChanged)="onCompletionChange()"></core-course-format>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-format',
|
||||
templateUrl: 'core-course-format.html',
|
||||
})
|
||||
export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called.
|
||||
|
||||
@Input() course?: CoreCourseAnyCourseData; // The course to render.
|
||||
@Input() sections?: CoreCourseSectionFormatted[]; // List of course sections.
|
||||
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||
@Input() initialSectionId?: number; // The section to load first (by ID).
|
||||
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
||||
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when any module completion changes.
|
||||
|
||||
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>;
|
||||
@ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent;
|
||||
|
||||
// All the possible component classes.
|
||||
courseFormatComponent?: Type<unknown>;
|
||||
courseSummaryComponent?: Type<unknown>;
|
||||
sectionSelectorComponent?: Type<unknown>;
|
||||
singleSectionComponent?: Type<unknown>;
|
||||
allSectionsComponent?: Type<unknown>;
|
||||
|
||||
canLoadMore = false;
|
||||
showSectionId = 0;
|
||||
sectionSelectorExpanded = false;
|
||||
data: Record<string, unknown> = {}; // Data to pass to the components.
|
||||
|
||||
displaySectionSelector?: boolean;
|
||||
displayBlocks?: boolean;
|
||||
selectedSection?: CoreCourseSectionFormatted;
|
||||
previousSection?: CoreCourseSectionFormatted;
|
||||
nextSection?: CoreCourseSectionFormatted;
|
||||
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
|
||||
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||
loaded = false;
|
||||
hasSeveralSections?: boolean;
|
||||
imageThumb?: string;
|
||||
progress?: number;
|
||||
|
||||
protected sectionStatusObserver?: CoreEventObserver;
|
||||
protected selectTabObserver?: CoreEventObserver;
|
||||
protected lastCourseFormat?: string;
|
||||
|
||||
constructor(
|
||||
protected content: IonContent,
|
||||
) {
|
||||
// Pass this instance to all components so they can use its methods and properties.
|
||||
this.data.coreCourseFormatComponent = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Listen for section status changes.
|
||||
this.sectionStatusObserver = CoreEvents.on<CoreEventSectionStatusChangedData>(
|
||||
CoreEvents.SECTION_STATUS_CHANGED,
|
||||
async (data) => {
|
||||
if (!this.downloadEnabled || !this.sections?.length || !data.sectionId || data.courseId != this.course?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @todo Check if the affected section is being downloaded.
|
||||
// If so, we don't update section status because it'll already be updated when the download finishes.
|
||||
// const downloadId = CoreCourseHelper.instance.getSectionDownloadId({ id: data.sectionId });
|
||||
// if (prefetchDelegate.isBeingDownloaded(downloadId)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Get the affected section.
|
||||
// const section = this.sections.find(section => section.id == data.sectionId);
|
||||
// if (!section) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Recalculate the status.
|
||||
// await CoreCourseHelper.instance.calculateSectionStatus(section, this.course.id, false);
|
||||
|
||||
// if (section.isDownloading && !prefetchDelegate.isBeingDownloaded(downloadId)) {
|
||||
// // All the modules are now downloading, set a download all promise.
|
||||
// this.prefetch(section);
|
||||
// }
|
||||
},
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
|
||||
// Listen for select course tab events to select the right section if needed.
|
||||
this.selectTabObserver = CoreEvents.on<CoreEventSelectCourseTabData>(CoreEvents.SELECT_COURSE_TAB, (data) => {
|
||||
if (data.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
let section: CoreCourseSectionFormatted | undefined;
|
||||
|
||||
if (typeof data.sectionId != 'undefined' && data.sectionId != null && this.sections) {
|
||||
section = this.sections.find((section) => section.id == data.sectionId);
|
||||
} else if (typeof data.sectionNumber != 'undefined' && data.sectionNumber != null && this.sections) {
|
||||
section = this.sections.find((section) => section.section == data.sectionNumber);
|
||||
}
|
||||
|
||||
if (section) {
|
||||
this.sectionChanged(section);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
this.setInputData();
|
||||
|
||||
if (changes.course && this.course) {
|
||||
// Course has changed, try to get the components.
|
||||
this.getComponents();
|
||||
|
||||
this.displaySectionSelector = CoreCourseFormatDelegate.instance.displaySectionSelector(this.course);
|
||||
this.displayBlocks = CoreCourseFormatDelegate.instance.displayBlocks(this.course);
|
||||
this.progress = 'progress' in this.course && this.course.progress !== undefined && this.course.progress >= 0 &&
|
||||
this.course.completionusertracked !== false ? this.course.progress : undefined;
|
||||
if ('overviewfiles' in this.course) {
|
||||
this.imageThumb = this.course.overviewfiles?.[0]?.fileurl;
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.sections && this.sections) {
|
||||
this.treatSections(this.sections);
|
||||
}
|
||||
|
||||
if (this.downloadEnabled && (changes.downloadEnabled || changes.sections)) {
|
||||
this.calculateSectionsStatus(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the input data for components.
|
||||
*/
|
||||
protected setInputData(): void {
|
||||
this.data.course = this.course;
|
||||
this.data.sections = this.sections;
|
||||
this.data.initialSectionId = this.initialSectionId;
|
||||
this.data.initialSectionNumber = this.initialSectionNumber;
|
||||
this.data.downloadEnabled = this.downloadEnabled;
|
||||
this.data.moduleId = this.moduleId;
|
||||
this.data.completionChanged = this.completionChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the components classes.
|
||||
*/
|
||||
protected async getComponents(): Promise<void> {
|
||||
if (!this.course || this.course.format == this.lastCourseFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format has changed or it's the first time, load all the components.
|
||||
this.lastCourseFormat = this.course.format;
|
||||
|
||||
await Promise.all([
|
||||
this.loadCourseFormatComponent(),
|
||||
this.loadCourseSummaryComponent(),
|
||||
this.loadSectionSelectorComponent(),
|
||||
this.loadSingleSectionComponent(),
|
||||
this.loadAllSectionsComponent(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load course format component.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadCourseFormatComponent(): Promise<void> {
|
||||
this.courseFormatComponent = await CoreCourseFormatDelegate.instance.getCourseFormatComponent(this.course!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load course summary component.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadCourseSummaryComponent(): Promise<void> {
|
||||
this.courseSummaryComponent = await CoreCourseFormatDelegate.instance.getCourseSummaryComponent(this.course!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load section selector component.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadSectionSelectorComponent(): Promise<void> {
|
||||
this.sectionSelectorComponent = await CoreCourseFormatDelegate.instance.getSectionSelectorComponent(this.course!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load single section component.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadSingleSectionComponent(): Promise<void> {
|
||||
this.singleSectionComponent = await CoreCourseFormatDelegate.instance.getSingleSectionComponent(this.course!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all sections component.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadAllSectionsComponent(): Promise<void> {
|
||||
this.allSectionsComponent = await CoreCourseFormatDelegate.instance.getAllSectionsComponent(this.course!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat received sections.
|
||||
*
|
||||
* @param sections Sections to treat.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async treatSections(sections: CoreCourseSectionFormatted[]): Promise<void> {
|
||||
const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
|
||||
this.hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
|
||||
|
||||
if (this.selectedSection) {
|
||||
// We have a selected section, but the list has changed. Search the section in the list.
|
||||
let newSection = sections.find(section => this.compareSections(section, this.selectedSection!));
|
||||
|
||||
if (!newSection) {
|
||||
// Section not found, calculate which one to use.
|
||||
newSection = await CoreCourseFormatDelegate.instance.getCurrentSection(this.course!, sections);
|
||||
}
|
||||
|
||||
this.sectionChanged(newSection);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// There is no selected section yet, calculate which one to load.
|
||||
if (!this.hasSeveralSections) {
|
||||
// Always load "All sections" to display the section title. If it isn't there just load the section.
|
||||
this.loaded = true;
|
||||
this.sectionChanged(sections[0]);
|
||||
} else if (this.initialSectionId || this.initialSectionNumber) {
|
||||
// We have an input indicating the section ID to load. Search the section.
|
||||
const section = sections.find((section) => {
|
||||
if (section.id != this.initialSectionId && (!section.section || section.section != this.initialSectionNumber)) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Don't load the section if it cannot be viewed by the user.
|
||||
if (section && this.canViewSection(section)) {
|
||||
this.loaded = true;
|
||||
this.sectionChanged(section);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.loaded) {
|
||||
// No section specified, not found or not visible, get current section.
|
||||
const section = await CoreCourseFormatDelegate.instance.getCurrentSection(this.course!, sections);
|
||||
|
||||
this.loaded = true;
|
||||
this.sectionChanged(section);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the section selector modal.
|
||||
*
|
||||
* @param event Event.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
showSectionSelector(event?: MouseEvent): void {
|
||||
if (this.sectionSelectorExpanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @todo this.sectionSelectorExpanded = true;
|
||||
// const modal = this.modalCtrl.create('CoreCourseSectionSelectorPage',
|
||||
// {course: this.course, sections: this.sections, selected: this.selectedSection});
|
||||
// modal.onDidDismiss((newSection) => {
|
||||
// if (newSection) {
|
||||
// this.sectionChanged(newSection);
|
||||
// }
|
||||
|
||||
// this.sectionSelectorExpanded = false;
|
||||
// });
|
||||
|
||||
// modal.present({
|
||||
// ev: event
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when selected section changes.
|
||||
*
|
||||
* @param newSection The new selected section.
|
||||
*/
|
||||
sectionChanged(newSection: CoreCourseSectionFormatted): void {
|
||||
const previousValue = this.selectedSection;
|
||||
this.selectedSection = newSection;
|
||||
this.data.section = this.selectedSection;
|
||||
|
||||
if (newSection.id != this.allSectionsId) {
|
||||
// Select next and previous sections to show the arrows.
|
||||
const i = this.sections!.findIndex((value) => this.compareSections(value, this.selectedSection!));
|
||||
|
||||
let j: number;
|
||||
for (j = i - 1; j >= 1; j--) {
|
||||
if (this.canViewSection(this.sections![j])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.previousSection = j >= 1 ? this.sections![j] : undefined;
|
||||
|
||||
for (j = i + 1; j < this.sections!.length; j++) {
|
||||
if (this.canViewSection(this.sections![j])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.nextSection = j < this.sections!.length ? this.sections![j] : undefined;
|
||||
} else {
|
||||
this.previousSection = undefined;
|
||||
this.nextSection = undefined;
|
||||
this.canLoadMore = false;
|
||||
this.showSectionId = 0;
|
||||
this.showMoreActivities();
|
||||
// @todo CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false);
|
||||
}
|
||||
|
||||
if (this.moduleId && typeof previousValue == 'undefined') {
|
||||
setTimeout(() => {
|
||||
CoreDomUtils.instance.scrollToElementBySelector(this.content, '#core-course-module-' + this.moduleId);
|
||||
}, 200);
|
||||
} else {
|
||||
this.content.scrollToTop(0);
|
||||
}
|
||||
|
||||
if (!previousValue || previousValue.id != newSection.id) {
|
||||
// First load or section changed, add log in Moodle.
|
||||
CoreUtils.instance.ignoreErrors(
|
||||
CoreCourse.instance.logView(this.course!.id, newSection.section, undefined, this.course!.fullname),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare if two sections are equal.
|
||||
*
|
||||
* @param section1 First section.
|
||||
* @param section2 Second section.
|
||||
* @return Whether they're equal.
|
||||
*/
|
||||
compareSections(section1: CoreCourseSectionFormatted, section2: CoreCourseSectionFormatted): boolean {
|
||||
return section1 && section2 ? section1.id === section2.id : section1 === section2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the status of sections.
|
||||
*
|
||||
* @param refresh If refresh or not.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected calculateSectionsStatus(refresh?: boolean): void {
|
||||
// @todo CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, refresh).catch(() => {
|
||||
// // Ignore errors (shouldn't happen).
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm and prefetch a section. If the section is "all sections", prefetch all the sections.
|
||||
*
|
||||
* @param section Section to download.
|
||||
* @param refresh Refresh clicked (not used).
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
prefetch(section: CoreCourseSectionFormatted): void {
|
||||
// section.isCalculating = true;
|
||||
// @todo CoreCourseHelper.instance.confirmDownloadSizeSection(this.course.id, section, this.sections).then(() => {
|
||||
// this.prefetchSection(section, true);
|
||||
// }, (error) => {
|
||||
// // User cancelled or there was an error calculating the size.
|
||||
// if (error) {
|
||||
// CoreDomUtils.instance.showErrorModal(error);
|
||||
// }
|
||||
// }).finally(() => {
|
||||
// section.isCalculating = false;
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a section. @todo
|
||||
*
|
||||
* @param section The section to download.
|
||||
* @param manual Whether the prefetch was started manually or it was automatically started because all modules
|
||||
* are being downloaded.
|
||||
*/
|
||||
// protected prefetchSection(section: Section, manual?: boolean): void {
|
||||
// CoreCourseHelper.instance.prefetchSection(section, this.course.id, this.sections).catch((error) => {
|
||||
// // Don't show error message if it's an automatic download.
|
||||
// if (!manual) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
* @param done Function to call when done.
|
||||
* @param afterCompletionChange Whether the refresh is due to a completion change.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, afterCompletionChange?: boolean): Promise<void> {
|
||||
const promises = this.dynamicComponents?.map(async (component) => {
|
||||
await component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]);
|
||||
}) || [];
|
||||
|
||||
if (this.courseBlocksComponent) {
|
||||
promises.push(this.courseBlocksComponent.doRefresh());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show more activities (only used when showing all the sections at the same time).
|
||||
*
|
||||
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
|
||||
*/
|
||||
showMoreActivities(infiniteComplete?: () => void): void {
|
||||
this.canLoadMore = false;
|
||||
|
||||
const sections = this.sections || [];
|
||||
let modulesLoaded = 0;
|
||||
let i: number;
|
||||
for (i = this.showSectionId + 1; i < sections.length; i++) {
|
||||
if (!sections[i].hasContent || !sections[i].modules) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modulesLoaded += sections[i].modules.reduce((total, module) => module.visibleoncoursepage !== 0 ? total + 1 : total, 0);
|
||||
|
||||
if (modulesLoaded >= CoreCourseFormatComponent.LOAD_MORE_ACTIVITIES) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.showSectionId = i;
|
||||
this.canLoadMore = i < sections.length;
|
||||
|
||||
if (this.canLoadMore) {
|
||||
// Check if any of the following sections have any content.
|
||||
let thereAreMore = false;
|
||||
for (i++; i < sections.length; i++) {
|
||||
if (sections[i].hasContent && sections[i].modules && sections[i].modules?.length > 0) {
|
||||
thereAreMore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.canLoadMore = thereAreMore;
|
||||
}
|
||||
|
||||
infiniteComplete && infiniteComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.sectionStatusObserver && this.sectionStatusObserver.off();
|
||||
this.selectTabObserver && this.selectTabObserver.off();
|
||||
}
|
||||
|
||||
/**
|
||||
* User entered the page that contains the component.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
this.dynamicComponents?.forEach((component) => {
|
||||
component.callComponentFunction('ionViewDidEnter');
|
||||
});
|
||||
|
||||
// @todo if (this.downloadEnabled) {
|
||||
// // The download status of a section might have been changed from within a module page.
|
||||
// if (this.selectedSection && this.selectedSection.id !== CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
// CoreCourseHelper.instance.calculateSectionStatus(this.selectedSection, this.course.id, false, false);
|
||||
// } else {
|
||||
// CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* User left the page that contains the component.
|
||||
*/
|
||||
ionViewDidLeave(): void {
|
||||
this.dynamicComponents?.forEach((component) => {
|
||||
component.callComponentFunction('ionViewDidLeave');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a section can be viewed.
|
||||
*
|
||||
* @param section The section to check.
|
||||
* @return Whether the section can be viewed.
|
||||
*/
|
||||
canViewSection(section: CoreCourseSectionFormatted): boolean {
|
||||
return section.uservisible !== false && !section.hiddenbynumsections &&
|
||||
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* The completion of any of the modules have changed.
|
||||
*/
|
||||
onCompletionChange(completionData: CoreCourseModuleCompletionData): void {
|
||||
// Emit a new event for other components.
|
||||
this.completionChanged.emit(completionData);
|
||||
|
||||
if (completionData.valueused !== false || !this.course || !('progress' in this.course) ||
|
||||
typeof this.course.progress == 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the completion value is not used, the page won't be reloaded, so update the progress bar.
|
||||
const completionModules = (<CoreCourseModuleData[]> [])
|
||||
.concat(...this.sections!.map((section) => section.modules))
|
||||
.map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
|
||||
.reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0));
|
||||
|
||||
const moduleProgressPercent = 100 / (completionModules || 1);
|
||||
// Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar.
|
||||
if (completionData.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||
this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent);
|
||||
} else {
|
||||
this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate the download status of each section, in response to a module being downloaded.
|
||||
*
|
||||
* @param eventData
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onModuleStatusChange(eventData: CoreCourseModuleStatusChangedData): void {
|
||||
// @todo CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<button *ngIf="completion" (click)="completionClicked($event)">
|
||||
<img [src]="completionImage" [alt]="completionDescription">
|
||||
</button>
|
|
@ -0,0 +1,13 @@
|
|||
:host {
|
||||
button {
|
||||
display: block;
|
||||
background-color: transparent;
|
||||
|
||||
img {
|
||||
padding: 5px;
|
||||
width: 30px;
|
||||
vertical-align: middle;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||
import { CoreCourseModuleCompletionDataFormatted } from '@features/course/services/course-helper';
|
||||
import { Translate } from '@singletons';
|
||||
|
||||
/**
|
||||
* Component to handle activity completion. It shows a checkbox with the current status, and allows manually changing
|
||||
* the completion if it's allowed.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-course-module-completion [completion]="module.completiondata" [moduleName]="module.name"
|
||||
* (completionChanged)="completionChanged()"></core-course-module-completion>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-module-completion',
|
||||
templateUrl: 'core-course-module-completion.html',
|
||||
styleUrls: ['module-completion.scss'],
|
||||
})
|
||||
export class CoreCourseModuleCompletionComponent implements OnChanges {
|
||||
|
||||
@Input() completion?: CoreCourseModuleCompletionDataFormatted; // The completion status.
|
||||
@Input() moduleId?: number; // The name of the module this completion affects.
|
||||
@Input() moduleName?: string; // The name of the module this completion affects.
|
||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionDataFormatted>(); // Notify when completion changes.
|
||||
|
||||
completionImage?: string;
|
||||
completionDescription?: string;
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.completion && this.completion) {
|
||||
this.showStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion clicked.
|
||||
*
|
||||
* @param e The click event.
|
||||
*/
|
||||
async completionClicked(e: Event): Promise<void> {
|
||||
if (!this.completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.completion.cmid == 'undefined' || this.completion.tracking !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const modal = await CoreDomUtils.instance.showModalLoading();
|
||||
this.completion.state = this.completion.state === 1 ? 0 : 1;
|
||||
|
||||
try {
|
||||
const response = await CoreCourse.instance.markCompletedManually(
|
||||
this.completion.cmid,
|
||||
this.completion.state === 1,
|
||||
this.completion.courseId!,
|
||||
this.completion.courseName,
|
||||
);
|
||||
|
||||
if (this.completion.valueused === false) {
|
||||
this.showStatus();
|
||||
if (response.offline) {
|
||||
this.completion.offline = true;
|
||||
}
|
||||
}
|
||||
this.completionChanged.emit(this.completion);
|
||||
} catch (error) {
|
||||
this.completion.state = this.completion.state === 1 ? 0 : 1;
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorchangecompletion', true);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set image and description to show as completion icon.
|
||||
*/
|
||||
protected async showStatus(): Promise<void> {
|
||||
if (!this.completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleName = this.moduleName || '';
|
||||
let langKey: string | undefined;
|
||||
let image: string | undefined;
|
||||
|
||||
if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-manual-n';
|
||||
langKey = 'core.completion-alt-manual-n';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||
image = 'completion-manual-y';
|
||||
langKey = 'core.completion-alt-manual-y';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-auto-n';
|
||||
langKey = 'core.completion-alt-auto-n';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||
image = 'completion-auto-y';
|
||||
langKey = 'core.completion-alt-auto-y';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_PASS) {
|
||||
image = 'completion-auto-pass';
|
||||
langKey = 'core.completion-alt-auto-pass';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_FAIL) {
|
||||
image = 'completion-auto-fail';
|
||||
langKey = 'core.completion-alt-auto-fail';
|
||||
}
|
||||
|
||||
if (image) {
|
||||
if (this.completion.overrideby > 0) {
|
||||
image += '-override';
|
||||
}
|
||||
this.completionImage = 'assets/img/completion/' + image + '.svg';
|
||||
}
|
||||
|
||||
if (!moduleName || !this.moduleId || !langKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await CoreFilterHelper.instance.getFiltersAndFormatText(
|
||||
moduleName,
|
||||
'module',
|
||||
this.moduleId,
|
||||
{ clean: true, singleLine: true, shortenLength: 50, courseId: this.completion.courseId },
|
||||
);
|
||||
|
||||
let translateParams: Record<string, unknown> = {
|
||||
$a: result.text,
|
||||
};
|
||||
|
||||
if (this.completion.overrideby > 0) {
|
||||
langKey += '-override';
|
||||
|
||||
const profile = await CoreUser.instance.getProfile(this.completion.overrideby, this.completion.courseId, true);
|
||||
|
||||
translateParams = {
|
||||
$a: {
|
||||
overrideuser: profile.fullname,
|
||||
modname: result.text,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
this.completionDescription = Translate.instance.instant(langKey, translateParams);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
<ion-card *ngIf="description">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<core-format-text [text]="description" [component]="component" [componentId]="componentId"
|
||||
[maxHeight]="showFull && showFull !== 'false' ? 0 : 120" fullOnClick="true" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
<ion-label>
|
||||
<core-format-text [text]="description" [component]="component" [componentId]="componentId"
|
||||
[maxHeight]="showFull && showFull !== 'false' ? 0 : 120" fullOnClick="true" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="note">
|
||||
<ion-note slot="end">{{ note }}</ion-note>
|
||||
<ion-label>
|
||||
<ion-note slot="end">{{ note }}</ion-note>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
|
@ -0,0 +1,73 @@
|
|||
<ion-item *ngIf="module && module.handlerData && module.visibleoncoursepage !== 0 && !module.handlerData.loading"
|
||||
class="ion-text-wrap" id="core-course-module-{{module.id}}" class="core-course-module-handler {{module.handlerData.class}}"
|
||||
(click)="moduleClicked($event)" [title]="module.handlerData.a11yTitle" detail="false"
|
||||
[ngClass]="{'item-media': module.handlerData.icon, 'item-dimmed': module.visible === 0 || module.uservisible === false,
|
||||
'core-not-clickable': !module.handlerData.action || module.uservisible === false}">
|
||||
|
||||
<img slot="start" *ngIf="module.handlerData.icon" [src]="module.handlerData.icon" [alt]="modNameTranslated"
|
||||
[attr.aria-hidden]="true" class="core-module-icon">
|
||||
|
||||
<ion-label>
|
||||
<div class="core-module-title">
|
||||
<core-format-text [text]="module.handlerData.title" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated">
|
||||
</core-format-text>
|
||||
|
||||
<!-- Buttons. -->
|
||||
<div slot="end" *ngIf="module.uservisible !== false" class="buttons core-module-buttons"
|
||||
[ngClass]="{'core-button-completion': module.completiondata}">
|
||||
<!-- Module completion. -->
|
||||
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata"
|
||||
[moduleName]="module.name" [moduleId]="module.id" (completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion>
|
||||
|
||||
<div class="core-module-buttons-more">
|
||||
<!-- @todo <core-download-refresh [status]="downloadStatus" [enabled]="downloadEnabled" [canTrustDownload]="canCheckUpdates"
|
||||
[loading]="spinner || module.handlerData.spinner" (action)="download($event)">
|
||||
</core-download-refresh> -->
|
||||
|
||||
<!-- Buttons defined by the module handler. -->
|
||||
<ion-button fill="clear" *ngFor="let button of module.handlerData.buttons" color="dark"
|
||||
[hidden]="button.hidden || spinner || module.handlerData.spinner" class="core-animate-show-hide"
|
||||
(click)="buttonClicked($event, button)"
|
||||
[attr.aria-label]="button.label | translate:{$a: module.handlerData.title}">
|
||||
<core-icon [name]="button.icon" slot="icon-only"></core-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="core-module-more-info">
|
||||
<ion-badge slot="end" *ngIf="module.handlerData.extraBadge" [color]="module.handlerData.extraBadgeColor"
|
||||
class="ion-text-wrap ion-text-start">
|
||||
<span [innerHTML]="module.handlerData.extraBadge"></span>
|
||||
</ion-badge>
|
||||
<ion-badge slot="end" *ngIf="module.visible === 0 && (!section || section.visible)" class="ion-text-wrap">
|
||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge slot="end" *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap">
|
||||
{{ 'core.course.hiddenoncoursepage' | translate }}
|
||||
</ion-badge>
|
||||
<div class="core-module-availabilityinfo" *ngIf="module.availabilityinfo" slot="end">
|
||||
<ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge>
|
||||
<core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId" class="ion-text-wrap">
|
||||
</core-format-text>
|
||||
</div>
|
||||
<ion-badge slot="end" *ngIf="module.completiondata?.offline" color="warning" class="ion-text-wrap">
|
||||
{{ 'core.course.manualcompletionnotsynced' | translate }}
|
||||
</ion-badge>
|
||||
</div>
|
||||
|
||||
<core-format-text class="core-module-description" *ngIf="module.description" maxHeight="80" [text]="module.description"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Loading. -->
|
||||
<ion-item *ngIf="module && module.handlerData && module.visibleoncoursepage !== 0 && module.handlerData.loading" role="status"
|
||||
class="ion-text-wrap" id="core-course-module-{{module.id}}" [title]="module.handlerData.a11yTitle"
|
||||
[ngClass]="['core-course-module-handler', 'core-module-loading', module.handlerData.class]" detail="false">
|
||||
<ion-label><ion-spinner></ion-spinner></ion-label>
|
||||
</ion-item>
|
|
@ -0,0 +1,162 @@
|
|||
:host {
|
||||
background: white;
|
||||
display: block;
|
||||
|
||||
.item.core-course-module-handler {
|
||||
align-items: flex-start;
|
||||
min-height: 52px;
|
||||
cursor: pointer;
|
||||
|
||||
// &.item .item-inner {
|
||||
// @include safe-area-padding(null, 0px, null, null);
|
||||
// }
|
||||
// .label {
|
||||
// @include margin(0, 0, 0, null);
|
||||
// }
|
||||
.core-module-icon {
|
||||
align-items: flex-start;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
// &.item-ios:active,
|
||||
// &.item-ios.activated {
|
||||
// background-color: $list-ios-activated-background-color;
|
||||
// }
|
||||
// &.item-md:active,
|
||||
// &.item-md.activated {
|
||||
// background-color: $list-md-activated-background-color;
|
||||
// }
|
||||
}
|
||||
|
||||
.core-module-title {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: flex-start;
|
||||
|
||||
core-format-text {
|
||||
flex-grow: 2;
|
||||
}
|
||||
.core-module-buttons,
|
||||
.buttons.core-module-buttons {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.core-module-buttons,
|
||||
.core-module-buttons-more {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
justify-content: space-around;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.core-module-buttons core-course-module-completion,
|
||||
.core-module-buttons-more button {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.core-module-buttons core-course-module-completion {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.core-module-more-info {
|
||||
// ion-badge {
|
||||
// @include text-align('start');
|
||||
// }
|
||||
|
||||
.core-module-availabilityinfo {
|
||||
font-size: 90%;
|
||||
ul {
|
||||
margin-block-start: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-not-clickable {
|
||||
cursor: initial;
|
||||
|
||||
// &:active,
|
||||
// &.activated {
|
||||
// background-color: $list-background-color;
|
||||
// }
|
||||
}
|
||||
|
||||
.core-module-loading {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
clear: both;
|
||||
// @include darkmode() {
|
||||
// color: $core-dark-text-color;
|
||||
// }
|
||||
}
|
||||
|
||||
// @include darkmode() {
|
||||
// .item.core-course-module-handler {
|
||||
// background: $core-dark-item-bg-color;
|
||||
// &.item-ios:active,
|
||||
// &.item-ios.activated,
|
||||
// &.item-md:active,
|
||||
// &.item-md.activated {
|
||||
// background-color: $core-dark-background-color;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .core-not-clickable:active,
|
||||
// .core-not-clickable.activated {
|
||||
// background-color: $core-dark-item-bg-color;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// ion-app.app-root.md core-course-module {
|
||||
// .core-module-description {
|
||||
// @include padding(null, $label-md-margin-end, null, null);
|
||||
// margin-bottom: $label-md-margin-bottom;
|
||||
|
||||
// .core-show-more {
|
||||
// @include padding(null, $label-md-margin-end, null, null);
|
||||
// }
|
||||
// }
|
||||
|
||||
// .core-module-title core-format-text {
|
||||
// padding-top: $label-md-margin-top + 3;
|
||||
// }
|
||||
// .button-md {
|
||||
// margin-top: 8px;
|
||||
// margin-bottom: 8px;
|
||||
// }
|
||||
// .core-module-buttons-more {
|
||||
// min-height: 52px;
|
||||
// min-width: 53px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// ion-app.app-root.ios core-course-module {
|
||||
// .core-module-description {
|
||||
// @include padding(null, $label-ios-margin-end, null, null);
|
||||
// margin-bottom: $label-md-margin-bottom;
|
||||
|
||||
// .core-show-more {
|
||||
// @include padding(null, $label-ios-margin-end, null, null);
|
||||
// }
|
||||
// }
|
||||
|
||||
// .core-module-title core-format-text {
|
||||
// padding-top: $label-ios-margin-top + 3;
|
||||
// }
|
||||
|
||||
// .core-module-buttons-more {
|
||||
// min-height: 53px;
|
||||
// min-width: 58px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// ion-app.app-root .core-course-module-handler.item [item-start] + .item-inner {
|
||||
// @include margin-horizontal(4px, null);
|
||||
// }
|
|
@ -0,0 +1,206 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
|
||||
|
||||
// import { CoreSites } from '@services/sites';
|
||||
// import { CoreDomUtils } from '@services/utils/dom';
|
||||
// import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreCourseModuleDataFormatted, CoreCourseSectionFormatted } from '@features/course/services/course-helper';
|
||||
import { CoreCourse, CoreCourseModuleCompletionData } from '@features/course/services/course';
|
||||
import { CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
|
||||
// import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from '../../providers/module-prefetch-delegate';
|
||||
|
||||
/**
|
||||
* Component to display a module entry in a list of modules.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-course-module [module]="module" [courseId]="courseId" (completionChanged)="onCompletionChange()"></core-course-module>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-module',
|
||||
templateUrl: 'core-course-module.html',
|
||||
})
|
||||
export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() module?: CoreCourseModuleDataFormatted; // The module to render.
|
||||
@Input() courseId?: number; // The course the module belongs to.
|
||||
@Input() section?: CoreCourseSectionFormatted; // The section the module belongs to.
|
||||
// eslint-disable-next-line @angular-eslint/no-input-rename
|
||||
@Input('downloadEnabled') set enabled(value: boolean) {
|
||||
this.downloadEnabled = value;
|
||||
|
||||
if (!this.module?.handlerData?.showDownloadButton || !this.downloadEnabled || this.statusCalculated) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First time that the download is enabled. Initialize the data.
|
||||
this.statusCalculated = true;
|
||||
this.spinner = true; // Show spinner while calculating the status.
|
||||
|
||||
// Get current status to decide which icon should be shown.
|
||||
// @todo this.prefetchDelegate.getModuleStatus(this.module, this.courseId).then(this.showStatus.bind(this));
|
||||
};
|
||||
|
||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when module completion changes.
|
||||
@Output() statusChanged = new EventEmitter<CoreCourseModuleStatusChangedData>(); // Notify when the download status changes.
|
||||
|
||||
downloadStatus?: string;
|
||||
canCheckUpdates?: boolean;
|
||||
spinner?: boolean; // Whether to display a loading spinner.
|
||||
downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||
modNameTranslated = '';
|
||||
|
||||
// protected prefetchHandler: CoreCourseModulePrefetchHandler;
|
||||
// protected statusObserver?: CoreEventObserver;
|
||||
protected statusCalculated = false;
|
||||
protected isDestroyed = false;
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (!this.module) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.courseId = this.courseId || this.module.course;
|
||||
this.modNameTranslated = CoreCourse.instance.translateModuleName(this.module.modname) || '';
|
||||
|
||||
if (!this.module.handlerData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title;
|
||||
|
||||
if (this.module.handlerData.showDownloadButton) {
|
||||
// @todo Listen for changes on this module status, even if download isn't enabled.
|
||||
// this.prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(this.module);
|
||||
// this.canCheckUpdates = this.prefetchDelegate.canCheckUpdates();
|
||||
|
||||
// this.statusObserver = this.eventsProvider.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
|
||||
// if (data.componentId === this.module.id && this.prefetchHandler &&
|
||||
// data.component === this.prefetchHandler.component) {
|
||||
|
||||
// // Call determineModuleStatus to get the right status to display.
|
||||
// const status = this.prefetchDelegate.determineModuleStatus(this.module, data.status);
|
||||
|
||||
// if (this.downloadEnabled) {
|
||||
// // Download is enabled, show the status.
|
||||
// this.showStatus(status);
|
||||
// } else if (this.module.handlerData.updateStatus) {
|
||||
// // Download isn't enabled but the handler defines a updateStatus function, call it anyway.
|
||||
// this.module.handlerData.updateStatus(status);
|
||||
// }
|
||||
// }
|
||||
// }, this.sitesProvider.getCurrentSiteId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when the module is clicked.
|
||||
*
|
||||
* @param event Click event.
|
||||
*/
|
||||
moduleClicked(event: Event): void {
|
||||
if (this.module?.uservisible !== false && this.module?.handlerData?.action) {
|
||||
this.module.handlerData.action(event, this.module, this.courseId!);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when a button is clicked.
|
||||
*
|
||||
* @param event Click event.
|
||||
* @param button The clicked button.
|
||||
*/
|
||||
buttonClicked(event: Event, button: CoreCourseModuleHandlerButton): void {
|
||||
if (!button || !button.action) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
button.action(event, this.module!, this.courseId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Download the module.
|
||||
*
|
||||
* @param refresh Whether it's refreshing.
|
||||
*/
|
||||
// download(refresh: boolean): void {
|
||||
// if (!this.prefetchHandler) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Show spinner since this operation might take a while.
|
||||
// this.spinner = true;
|
||||
|
||||
// // Get download size to ask for confirm if it's high.
|
||||
// this.prefetchHandler.getDownloadSize(this.module, this.courseId, true).then((size) => {
|
||||
// return this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh);
|
||||
// }).then(() => {
|
||||
// const eventData = {
|
||||
// sectionId: this.section.id,
|
||||
// moduleId: this.module.id,
|
||||
// courseId: this.courseId
|
||||
// };
|
||||
// this.statusChanged.emit(eventData);
|
||||
// }).catch((error) => {
|
||||
// // Error, hide spinner.
|
||||
// this.spinner = false;
|
||||
// if (!this.isDestroyed) {
|
||||
// this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* Show download buttons according to module status.
|
||||
*
|
||||
* @param status Module status.
|
||||
*/
|
||||
protected showStatus(status: string): void {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.spinner = false;
|
||||
this.downloadStatus = status;
|
||||
|
||||
this.module?.handlerData?.updateStatus?.(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// this.statusObserver?.off();
|
||||
this.module?.handlerData?.onDestroy?.();
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data sent to the status changed output.
|
||||
*/
|
||||
export type CoreCourseModuleStatusChangedData = {
|
||||
moduleId: number;
|
||||
courseId: number;
|
||||
sectionId?: number;
|
||||
};
|
|
@ -21,9 +21,9 @@
|
|||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="dataLoaded">
|
||||
<!-- <core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId"
|
||||
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId"
|
||||
[initialSectionNumber]="sectionNumber" [downloadEnabled]="downloadEnabled" [moduleId]="moduleId"
|
||||
(completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}">
|
||||
</core-course-format> -->
|
||||
</core-course-format>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -35,7 +35,6 @@ import {
|
|||
} from '@features/course/services/course-options-delegate';
|
||||
// import { CoreCourseSyncProvider } from '../../providers/sync';
|
||||
// import { CoreCourseFormatComponent } from '../../components/format/format';
|
||||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||
import {
|
||||
CoreEvents,
|
||||
CoreEventObserver,
|
||||
|
@ -58,7 +57,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
// @ViewChild(CoreCourseFormatComponent) formatComponent: CoreCourseFormatComponent;
|
||||
|
||||
course!: CoreCourseAnyCourseData;
|
||||
sections?: Section[];
|
||||
sections?: CoreCourseSectionFormatted[];
|
||||
sectionId?: number;
|
||||
sectionNumber?: number;
|
||||
courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = [];
|
||||
|
@ -211,11 +210,6 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
this.course = result.course;
|
||||
}
|
||||
|
||||
// @todo: Get the overview files. Maybe move it to format component?
|
||||
// if ('overviewfiles' in this.course && this.course.overviewfiles) {
|
||||
// this.course.imageThumb = this.course.overviewfiles[0] && this.course.overviewfiles[0].fileurl;
|
||||
// }
|
||||
|
||||
if (sync) {
|
||||
// Try to synchronize the course data.
|
||||
// @todo return this.syncProvider.syncCourse(this.course.id).then((result) => {
|
||||
|
@ -281,19 +275,6 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
this.course.fullname,
|
||||
true,
|
||||
);
|
||||
|
||||
// Format the name of each section.
|
||||
result.sections.forEach(async (section: Section) => {
|
||||
const result = await CoreFilterHelper.instance.getFiltersAndFormatText(
|
||||
section.name.trim(),
|
||||
'course',
|
||||
this.course.id,
|
||||
{ clean: true, singleLine: true },
|
||||
);
|
||||
|
||||
section.formattedName = result.text;
|
||||
});
|
||||
|
||||
this.sections = result.sections;
|
||||
|
||||
if (CoreCourseFormatDelegate.instance.canViewAllSections(this.course)) {
|
||||
|
@ -527,7 +508,3 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
type Section = CoreCourseSectionFormatted & {
|
||||
formattedName?: string;
|
||||
};
|
||||
|
|
|
@ -593,7 +593,7 @@ export class CoreCourseHelperProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadOfflineCompletion(courseId: number, sections: CoreCourseSection[], siteId?: string): Promise<void> {
|
||||
async loadOfflineCompletion(courseId: number, sections: CoreCourseSectionFormatted[], siteId?: string): Promise<void> {
|
||||
const offlineCompletions = await CoreCourseOffline.instance.getCourseManualCompletions(courseId, siteId);
|
||||
|
||||
if (!offlineCompletions || !offlineCompletions.length) {
|
||||
|
@ -620,7 +620,7 @@ export class CoreCourseHelperProvider {
|
|||
offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) {
|
||||
// The module has offline completion. Load it.
|
||||
module.completiondata.state = offlineCompletion.completed;
|
||||
// @todo module.completiondata.offline = true;
|
||||
module.completiondata.offline = true;
|
||||
|
||||
// If all completions have been loaded, stop.
|
||||
loaded++;
|
||||
|
@ -776,7 +776,7 @@ export class CoreCourseHelperProvider {
|
|||
* @return Section download ID.
|
||||
* @todo section type.
|
||||
*/
|
||||
getSectionDownloadId(section: CoreCourseSection): string {
|
||||
getSectionDownloadId(section: {id: number}): string {
|
||||
return 'Section-' + section.id;
|
||||
}
|
||||
|
||||
|
@ -1097,4 +1097,5 @@ export type CoreCourseModuleCompletionDataFormatted = CoreCourseModuleCompletion
|
|||
courseName?: string;
|
||||
tracking?: number;
|
||||
cmid?: number;
|
||||
offline?: boolean;
|
||||
};
|
||||
|
|
|
@ -953,7 +953,20 @@ export class CoreCourseProvider {
|
|||
completed: completed,
|
||||
};
|
||||
|
||||
return site.write('core_completion_update_activity_completion_status_manually', params);
|
||||
const result = await site.write<CoreStatusWithWarningsWSResponse>(
|
||||
'core_completion_update_activity_completion_status_manually',
|
||||
params,
|
||||
);
|
||||
|
||||
if (!result.status) {
|
||||
if (result.warnings && result.warnings.length) {
|
||||
throw new CoreWSError(result.warnings[0]);
|
||||
} else {
|
||||
throw new CoreError('Cannot change completion.');
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -209,11 +209,15 @@ export interface CoreCourseModuleHandlerButton {
|
|||
|
||||
/**
|
||||
* The name of the button icon to use in iOS instead of "icon".
|
||||
*
|
||||
* @deprecated since 3.9.5. Now the icon must be the same for all platforms.
|
||||
*/
|
||||
iosIcon?: string;
|
||||
|
||||
/**
|
||||
* The name of the button icon to use in MaterialDesign instead of "icon".
|
||||
*
|
||||
* @deprecated since 3.9.5. Now the icon must be the same for all platforms.
|
||||
*/
|
||||
mdIcon?: string;
|
||||
|
||||
|
|
|
@ -286,3 +286,11 @@ export type CoreEventSelectCourseTabData = CoreEventSiteData & {
|
|||
export type CoreEventCompletionModuleViewedData = CoreEventSiteData & {
|
||||
courseId?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data passed to SECTION_STATUS_CHANGED event.
|
||||
*/
|
||||
export type CoreEventSectionStatusChangedData = CoreEventSiteData & {
|
||||
courseId: number;
|
||||
sectionId?: number;
|
||||
};
|
||||
|
|
|
@ -335,3 +335,11 @@ ion-select.core-button-select,
|
|||
overflow-x: scroll;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
// Text for accessibility, hidden from the view.
|
||||
.accesshide {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
|