commit
49afa105bc
|
@ -1536,19 +1536,23 @@
|
|||
"core.course.confirmpartialdownloadsize": "local_moodlemobileapp",
|
||||
"core.course.couldnotloadsectioncontent": "local_moodlemobileapp",
|
||||
"core.course.couldnotloadsections": "local_moodlemobileapp",
|
||||
"core.course.courseindex": "courseformat",
|
||||
"core.course.coursesummary": "moodle",
|
||||
"core.course.done": "completion",
|
||||
"core.course.downloadcourse": "tool_mobile",
|
||||
"core.course.downloadcoursesprogressdescription": "local_moodlemobileapp",
|
||||
"core.course.downloadsectionprogressdescription": "local_moodlemobileapp",
|
||||
"core.course.errordownloadingcourse": "local_moodlemobileapp",
|
||||
"core.course.errordownloadingsection": "local_moodlemobileapp",
|
||||
"core.course.errorgetmodule": "local_moodlemobileapp",
|
||||
"core.course.failed": "completion",
|
||||
"core.course.gotonextactivity": "local_moodlemobileapp",
|
||||
"core.course.gotonextactivitynotfound": "local_moodlemobileapp",
|
||||
"core.course.gotopreviousactivity": "local_moodlemobileapp",
|
||||
"core.course.gotopreviousactivitynotfound": "local_moodlemobileapp",
|
||||
"core.course.hiddenfromstudents": "moodle",
|
||||
"core.course.hiddenoncoursepage": "moodle",
|
||||
"core.course.highlighted": "moodle",
|
||||
"core.course.insufficientavailablequota": "local_moodlemobileapp",
|
||||
"core.course.insufficientavailablespace": "local_moodlemobileapp",
|
||||
"core.course.manualcompletionnotsynced": "local_moodlemobileapp",
|
||||
|
@ -1557,7 +1561,8 @@
|
|||
"core.course.overriddennotice": "grades",
|
||||
"core.course.refreshcourse": "local_moodlemobileapp",
|
||||
"core.course.section": "moodle",
|
||||
"core.course.sections": "moodle",
|
||||
"core.course.thisweek": "format_weeks/currentsection",
|
||||
"core.course.todo": "completion",
|
||||
"core.course.useactivityonbrowser": "local_moodlemobileapp",
|
||||
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
|
||||
"core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp",
|
||||
|
@ -2257,6 +2262,7 @@
|
|||
"core.strftimetime24": "langconfig",
|
||||
"core.submit": "moodle",
|
||||
"core.success": "moodle",
|
||||
"core.summary": "moodle",
|
||||
"core.tablet": "local_moodlemobileapp",
|
||||
"core.tag.defautltagcoll": "tag",
|
||||
"core.tag.errorareanotsupported": "local_moodlemobileapp",
|
||||
|
|
|
@ -95,7 +95,7 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
|
|||
|
||||
this.getResourceData(module, courseId, handlerData).then((extra) => {
|
||||
handlerData.extraBadge = extra;
|
||||
handlerData.extraBadgeColor = 'light';
|
||||
handlerData.extraBadgeColor = '';
|
||||
|
||||
return;
|
||||
}).catch(() => {
|
||||
|
|
|
@ -93,6 +93,7 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp
|
|||
}
|
||||
|
||||
rootAnimation.addAnimation(enteringContentAnimation);
|
||||
enteringContentAnimation.beforeAddClass('animating').afterRemoveClass('animating');
|
||||
|
||||
if (backDirection) {
|
||||
enteringContentAnimation
|
||||
|
@ -214,6 +215,8 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp
|
|||
// setup leaving view
|
||||
if (leavingEl) {
|
||||
const leavingContent = createAnimation();
|
||||
leavingContent.beforeAddClass('animating').afterRemoveClass('animating');
|
||||
|
||||
const leavingContentEl = leavingEl.querySelector(':scope > ion-content');
|
||||
const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar');
|
||||
const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');
|
||||
|
|
|
@ -62,7 +62,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
|
|||
@Input() layout: 'icon-top' | 'icon-start' | 'icon-end' | 'icon-bottom' | 'icon-hide' | 'label-hide' = 'icon-hide';
|
||||
@Input() tabs: CoreTabsOutletTab[] = [];
|
||||
|
||||
@ViewChild(IonTabs) protected ionTabs?: IonTabs;
|
||||
@ViewChild(IonTabs) protected ionTabs!: IonTabs;
|
||||
|
||||
protected stackEventsSubscription?: Subscription;
|
||||
protected outletActivatedSubscription?: Subscription;
|
||||
|
@ -96,11 +96,17 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
|
|||
}
|
||||
|
||||
this.tabsElement = this.element.nativeElement.querySelector('ion-tabs');
|
||||
this.stackEventsSubscription = this.ionTabs?.outlet.stackEvents.subscribe(async (stackEvent: StackEvent) => {
|
||||
this.stackEventsSubscription = this.ionTabs.outlet.stackEvents.subscribe(async (stackEvent: StackEvent) => {
|
||||
if (!this.isCurrentView) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add tabid to the tab content element.
|
||||
if (stackEvent.enteringView.element.id == '') {
|
||||
const tab = this.tabs.find((tab) => tab.page == stackEvent.enteringView.url);
|
||||
stackEvent.enteringView.element.id = tab?.id || '';
|
||||
}
|
||||
|
||||
this.showHideNavBarButtons(stackEvent.enteringView.element.tagName);
|
||||
|
||||
await this.listenContentScroll(stackEvent.enteringView.element, stackEvent.enteringView.id);
|
||||
|
@ -111,8 +117,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
|
|||
this.showHideTabs(scrollElement.scrollTop, scrollElement);
|
||||
}
|
||||
});
|
||||
this.outletActivatedSubscription = this.ionTabs?.outlet.activateEvents.subscribe(() => {
|
||||
this.lastActiveComponent = this.ionTabs?.outlet.component;
|
||||
this.outletActivatedSubscription = this.ionTabs.outlet.activateEvents.subscribe(() => {
|
||||
this.lastActiveComponent = this.ionTabs.outlet.component;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -140,8 +146,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
|
|||
// The `ionViewDidEnter` method is not called on nested outlets unless the parent page is leaving the navigation stack,
|
||||
// that's why we need to call it manually if the page that is entering already existed in the stack (meaning that it is
|
||||
// entering in response to a back navigation from the page on top).
|
||||
if (this.existsInNavigationStack && this.ionTabs?.outlet.isActivated) {
|
||||
(this.ionTabs?.outlet.component as Partial<ViewDidEnter>).ionViewDidEnter?.();
|
||||
if (this.existsInNavigationStack && this.ionTabs.outlet.isActivated) {
|
||||
(this.ionTabs.outlet.component as Partial<ViewDidEnter>).ionViewDidEnter?.();
|
||||
}
|
||||
|
||||
// After the view has entered for the first time, we can assume that it'll always be in the navigation stack
|
||||
|
@ -180,10 +186,9 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
|
|||
* @param activatedPageName Activated page name.
|
||||
*/
|
||||
protected showHideNavBarButtons(activatedPageName: string): void {
|
||||
const elements = this.ionTabs!.outlet.nativeEl.querySelectorAll('core-navbar-buttons');
|
||||
const domUtils = CoreDomUtils.instance;
|
||||
const elements = this.ionTabs.outlet.nativeEl.querySelectorAll('core-navbar-buttons');
|
||||
elements.forEach((element) => {
|
||||
const instance = domUtils.getInstanceByElement<CoreNavBarButtonsComponent>(element);
|
||||
const instance = CoreDomUtils.getInstanceByElement<CoreNavBarButtonsComponent>(element);
|
||||
|
||||
if (instance) {
|
||||
const pagetagName = element.closest('.ion-page')?.tagName;
|
||||
|
|
|
@ -43,6 +43,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
|||
protected headerSubHeadingFontSize = 0;
|
||||
protected contentSubHeadingFontSize = 0;
|
||||
protected subHeadingStartDifference = 0;
|
||||
protected inContent = true;
|
||||
protected title?: HTMLElement | null;
|
||||
protected titleHeight = 0;
|
||||
protected contentH1?: HTMLElement | null;
|
||||
|
||||
constructor(el: ElementRef<HTMLIonHeaderElement>) {
|
||||
this.header = el.nativeElement;
|
||||
|
@ -67,8 +71,6 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
|||
/**
|
||||
* Gets the loading content id to wait for the loading to finish.
|
||||
*
|
||||
* @TODO: If no core-loading is present, load directly. Take into account content needs to be initialized.
|
||||
*
|
||||
* @return Promise resolved with Loading Id, if any.
|
||||
*/
|
||||
protected async getLoadingId(): Promise<string | undefined> {
|
||||
|
@ -80,6 +82,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
|||
|
||||
return;
|
||||
}
|
||||
|
||||
const title = this.header.parentElement?.querySelector('.collapsible-title') || null;
|
||||
|
||||
if (title) {
|
||||
// Title already found, no need to wait for loading.
|
||||
this.loadingObserver.off();
|
||||
this.setupRealTitle();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return this.content.querySelector('core-loading.core-loading-loaded:not(.core-loading-inline) .core-loading-content')?.id;
|
||||
|
@ -89,6 +102,7 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
|||
* Call this function when header is not collapsible.
|
||||
*/
|
||||
protected cannotCollapse(): void {
|
||||
this.content = undefined;
|
||||
this.loadingObserver.off();
|
||||
this.header.classList.add('core-header-collapsed');
|
||||
}
|
||||
|
@ -112,16 +126,25 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
|||
await animation.finished;
|
||||
}));
|
||||
|
||||
const title = this.content.querySelector<HTMLElement>('.collapsible-title, h1');
|
||||
const contentH1 = this.content.querySelector<HTMLElement>('h1');
|
||||
let title = this.content.querySelector<HTMLElement>('.collapsible-title');
|
||||
if (!title) {
|
||||
// Title is outside the ion-content.
|
||||
title = this.header.parentElement?.querySelector('.collapsible-title') || null;
|
||||
this.inContent = false;
|
||||
}
|
||||
this.contentH1 = title?.querySelector<HTMLElement>('h1');
|
||||
const headerH1 = this.header.querySelector<HTMLElement>('h1');
|
||||
if (!title || !contentH1 || !headerH1) {
|
||||
if (!title || !this.contentH1 || !headerH1 || !this.contentH1.parentElement) {
|
||||
this.cannotCollapse();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.titleTopDifference = contentH1.getBoundingClientRect().top - headerH1.getBoundingClientRect().top;
|
||||
this.title = title;
|
||||
this.titleHeight = title.getBoundingClientRect().height;
|
||||
|
||||
this.titleTopDifference = this.contentH1.getBoundingClientRect().top - headerH1.getBoundingClientRect().top;
|
||||
|
||||
if (this.titleTopDifference <= 0) {
|
||||
this.cannotCollapse();
|
||||
|
||||
|
@ -141,17 +164,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
|||
}
|
||||
|
||||
const headerH1Styles = getComputedStyle(headerH1);
|
||||
const contentH1Styles = getComputedStyle(contentH1);
|
||||
const contentH1Styles = getComputedStyle(this.contentH1);
|
||||
|
||||
if (Platform.isRTL) {
|
||||
// Checking position over parent because transition may not be finished.
|
||||
const contentH1Position = contentH1.getBoundingClientRect().right - this.content.getBoundingClientRect().right;
|
||||
const contentH1Position = this.contentH1.getBoundingClientRect().right - this.content.getBoundingClientRect().right;
|
||||
const headerH1Position = headerH1.getBoundingClientRect().right - this.header.getBoundingClientRect().right;
|
||||
|
||||
this.h1StartDifference = Math.round(contentH1Position - (headerH1Position - parseFloat(headerH1Styles.paddingRight)));
|
||||
} else {
|
||||
// Checking position over parent because transition may not be finished.
|
||||
const contentH1Position = contentH1.getBoundingClientRect().left - this.content.getBoundingClientRect().left;
|
||||
const contentH1Position = this.contentH1.getBoundingClientRect().left - this.content.getBoundingClientRect().left;
|
||||
const headerH1Position = headerH1.getBoundingClientRect().left - this.header.getBoundingClientRect().left;
|
||||
|
||||
this.h1StartDifference = Math.round(contentH1Position - (headerH1Position + parseFloat(headerH1Styles.paddingLeft)));
|
||||
|
@ -165,10 +188,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
|||
if (styleName != 'font-size' &&
|
||||
styleName != 'font-family' &&
|
||||
(styleName.startsWith('font-') || styleName.startsWith('letter-'))) {
|
||||
contentH1.style.setProperty(styleName, headerH1Styles.getPropertyValue(styleName));
|
||||
this.contentH1?.style.setProperty(styleName, headerH1Styles.getPropertyValue(styleName));
|
||||
}
|
||||
});
|
||||
contentH1.style.setProperty(
|
||||
this.contentH1.style.setProperty(
|
||||
'--max-width',
|
||||
(parseFloat(headerH1Styles.width)
|
||||
-parseFloat(headerH1Styles.paddingLeft)
|
||||
|
@ -176,47 +199,70 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
|||
+'px'),
|
||||
);
|
||||
|
||||
contentH1.setAttribute('aria-hidden', 'true');
|
||||
this.contentH1.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Clone element to let the other elements be static.
|
||||
const contentH1Clone = this.contentH1.cloneNode(true) as HTMLElement;
|
||||
contentH1Clone.classList.add('cloned');
|
||||
this.contentH1.parentElement.insertBefore(contentH1Clone, this.contentH1);
|
||||
this.contentH1.style.setProperty(
|
||||
'top',
|
||||
(contentH1Clone.getBoundingClientRect().top -
|
||||
this.contentH1.parentElement.getBoundingClientRect().top +
|
||||
parseInt(getComputedStyle(this.contentH1.parentElement).marginTop || '0', 10)) + 'px',
|
||||
);
|
||||
this.contentH1.style.setProperty('position', 'absolute');
|
||||
|
||||
this.setupContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup content scroll.
|
||||
*
|
||||
* @param parentId Parent id to recalculate content
|
||||
* @param retries Retries to find content in case it's loading.
|
||||
*/
|
||||
async setupContent(parentId?: string, retries = 5): Promise<void> {
|
||||
if (parentId) {
|
||||
this.content = this.header.parentElement?.querySelector(`#${parentId} ion-content:not(.disable-scroll-y)`);
|
||||
this.inContent = false;
|
||||
if (!this.content && retries > 0) {
|
||||
await CoreUtils.nextTick();
|
||||
await this.setupContent(parentId, --retries);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.onScroll(this.content?.scrollTop || 0);
|
||||
}
|
||||
|
||||
if (!this.title || !this.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add something under the hood to change the page background.
|
||||
let color = getComputedStyle(title).getPropertyValue('backgroundColor').trim();
|
||||
let color = getComputedStyle(this.title).getPropertyValue('backgroundColor').trim();
|
||||
if (color == '') {
|
||||
color = getComputedStyle(title).getPropertyValue('--background').trim();
|
||||
color = getComputedStyle(this.title).getPropertyValue('--background').trim();
|
||||
}
|
||||
|
||||
const underHeader = document.createElement('div');
|
||||
underHeader.classList.add('core-underheader');
|
||||
underHeader.style.setProperty('height', this.header.clientHeight + 'px');
|
||||
underHeader.style.setProperty('background', color);
|
||||
this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader);
|
||||
|
||||
this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px');
|
||||
|
||||
// Subheading.
|
||||
const headerSubHeading = this.header.querySelector<HTMLElement>('h2,.subheading');
|
||||
const contentSubHeading = title.querySelector<HTMLElement>('h2,.subheading');
|
||||
if (headerSubHeading && contentSubHeading) {
|
||||
const headerSubHeadingStyles = getComputedStyle(headerSubHeading);
|
||||
this.headerSubHeadingFontSize = parseFloat(headerSubHeadingStyles.fontSize);
|
||||
|
||||
const contentSubHeadingStyles = getComputedStyle(contentSubHeading);
|
||||
this.contentSubHeadingFontSize = parseFloat(contentSubHeadingStyles.fontSize);
|
||||
|
||||
if (Platform.isRTL) {
|
||||
this.subHeadingStartDifference = contentSubHeading.getBoundingClientRect().right -
|
||||
(headerSubHeading.getBoundingClientRect().right - parseFloat(headerSubHeadingStyles.paddingRight));
|
||||
} else {
|
||||
this.subHeadingStartDifference = contentSubHeading.getBoundingClientRect().left -
|
||||
(headerSubHeading.getBoundingClientRect().left + parseFloat(headerSubHeadingStyles.paddingLeft));
|
||||
if (this.inContent) {
|
||||
this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader);
|
||||
this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px');
|
||||
} else {
|
||||
if (!this.header.closest('.ion-page')?.querySelector('.core-underheader')) {
|
||||
this.header.closest('.ion-page')?.insertBefore(underHeader, this.header);
|
||||
}
|
||||
|
||||
contentSubHeading.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
this.content.scrollEvents = true;
|
||||
this.content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => {
|
||||
if (e.target == this.content) {
|
||||
this.onScroll(title, contentH1, contentSubHeading, e.detail);
|
||||
this.onScroll(e.detail.scrollTop);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -224,54 +270,45 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
|||
/**
|
||||
* On scroll function.
|
||||
*
|
||||
* @param title Title on ion content.
|
||||
* @param contentH1 Heading 1 of title, if found.
|
||||
* @param scrollDetail Event details.
|
||||
* @param scrollTop Scroll top measure.
|
||||
*/
|
||||
protected onScroll(
|
||||
title: HTMLElement,
|
||||
contentH1: HTMLElement,
|
||||
contentSubheading: HTMLElement | null,
|
||||
scrollDetail: ScrollDetail,
|
||||
scrollTop: number,
|
||||
): void {
|
||||
const progress = CoreMath.clamp(scrollDetail.scrollTop / this.titleTopDifference, 0, 1);
|
||||
if (!this.title || !this.contentH1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = CoreMath.clamp(scrollTop / this.titleTopDifference, 0, 1);
|
||||
const collapsed = progress >= 1;
|
||||
|
||||
if (!this.inContent) {
|
||||
this.title.style.transform = 'translateY(-' + scrollTop + 'px)';
|
||||
const height = this.titleHeight - scrollTop;
|
||||
this.title.style.height = (height > 0 ? height : 0) + 'px';
|
||||
}
|
||||
|
||||
// Check total collapse.
|
||||
this.header.classList.toggle('core-header-collapsed', collapsed);
|
||||
title.classList.toggle('collapsible-title-collapsed', collapsed);
|
||||
title.classList.toggle('collapsible-title-collapse-started', scrollDetail.scrollTop > 0);
|
||||
title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5);
|
||||
title.style.setProperty('--collapse-opacity', (1 - progress) +'');
|
||||
this.title.classList.toggle('collapsible-title-collapsed', collapsed);
|
||||
this.title.classList.toggle('collapsible-title-collapse-started', scrollTop > 0);
|
||||
this.title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5);
|
||||
this.title.style.setProperty('--collapse-opacity', (1 - progress) +'');
|
||||
|
||||
if (collapsed) {
|
||||
contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)';
|
||||
contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px');
|
||||
|
||||
if (contentSubheading) {
|
||||
contentSubheading.style.transform = 'translateX(-' + this.subHeadingStartDifference + 'px)';
|
||||
contentSubheading.style.setProperty('font-size', this.headerSubHeadingFontSize + 'px');
|
||||
}
|
||||
this.contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)';
|
||||
this.contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Zoom font-size out.
|
||||
const newFontSize = this.contentH1FontSize - ((this.contentH1FontSize - this.headerH1FontSize) * progress);
|
||||
contentH1.style.setProperty('font-size', newFontSize + 'px');
|
||||
this.contentH1.style.setProperty('font-size', newFontSize + 'px');
|
||||
|
||||
// Move.
|
||||
const newStart = - this.h1StartDifference * progress;
|
||||
contentH1.style.transform = 'translateX(' + newStart + 'px)';
|
||||
|
||||
if (contentSubheading) {
|
||||
const newFontSize = this.contentSubHeadingFontSize -
|
||||
((this.contentSubHeadingFontSize - this.headerSubHeadingFontSize) * progress);
|
||||
contentSubheading.style.setProperty('font-size', newFontSize + 'px');
|
||||
|
||||
const newStart = - this.subHeadingStartDifference * progress;
|
||||
contentSubheading.style.transform = 'translateX(' + newStart + 'px)';
|
||||
}
|
||||
this.contentH1.style.transform = 'translateX(' + newStart + 'px)';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
@include position(50%, 0px, null, null);
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
transform: translateY(-50%);
|
||||
|
||||
ion-button {
|
||||
margin: 0;
|
||||
|
|
|
@ -20,7 +20,7 @@ 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 { CoreCourseSectionSelectorComponent } from './section-selector/section-selector';
|
||||
import { CoreCourseCourseIndexComponent } from './course-index/course-index';
|
||||
import { CoreCourseTagAreaComponent } from './tag-area/tag-area';
|
||||
import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
|
||||
import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy';
|
||||
|
@ -37,7 +37,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module-
|
|||
CoreCourseModuleDescriptionComponent,
|
||||
CoreCourseModuleInfoComponent,
|
||||
CoreCourseModuleManualCompletionComponent,
|
||||
CoreCourseSectionSelectorComponent,
|
||||
CoreCourseCourseIndexComponent,
|
||||
CoreCourseTagAreaComponent,
|
||||
CoreCourseUnsupportedModuleComponent,
|
||||
CoreCourseModuleNavigationComponent,
|
||||
|
@ -54,7 +54,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module-
|
|||
CoreCourseModuleDescriptionComponent,
|
||||
CoreCourseModuleInfoComponent,
|
||||
CoreCourseModuleManualCompletionComponent,
|
||||
CoreCourseSectionSelectorComponent,
|
||||
CoreCourseCourseIndexComponent,
|
||||
CoreCourseTagAreaComponent,
|
||||
CoreCourseUnsupportedModuleComponent,
|
||||
CoreCourseModuleNavigationComponent,
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h2 id="core-course-section-selector-label">{{ 'core.course.courseindex' | translate }}</h2>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label">
|
||||
<ng-container *ngFor="let section of sections">
|
||||
<ion-item *ngIf="allSectionId == section.id" class="ion-text-wrap divider" (click)="selectSection($event, section)" button
|
||||
[class.item-current]="selectedId === section.id" detail="false">
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-container *ngIf="allSectionId != section.id && !section.hiddenbynumsections &&
|
||||
section.id != stealthModulesSectionId && section.uservisible !== false">
|
||||
<ion-item class="ion-text-wrap divider section" (click)="selectSection($event, section)"
|
||||
[button]="section.visible !== 0 && section.uservisible !== false" [class.item-current]="selectedId === section.id"
|
||||
[class.item-dimmed]="section.visible === 0" detail="false" sticky="true">
|
||||
<ion-icon [name]="section.expanded ? 'fas-chevron-down' : 'fas-chevron-right'" flip-rtl slot="start"
|
||||
class="expandable-status-icon" (click)="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">
|
||||
</ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-badge *ngIf="section.highlighted && highlighted">{{highlighted}}</ion-badge>
|
||||
<ion-icon name="fas-lock" *ngIf="section.availabilityinfo" slot="end" class="restricted"
|
||||
[attr.aria-label]="'core.restricted' | translate"></ion-icon>
|
||||
</ion-item>
|
||||
<div [hidden]="!section.expanded" [id]="'core-course-index-section-' + section.id">
|
||||
<ng-container *ngIf="section.expanded">
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<ion-item [class.item-dimmed]="module.visible === 0"
|
||||
*ngIf="module.visibleoncoursepage !== 0 && !module.noviewlink"
|
||||
(click)="selectModule($event, section, module)" button>
|
||||
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
|
||||
slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-icon class="completioninfo completion_incomplete" name="far-circle"
|
||||
*ngIf="module.completionStatus === 0" slot="start" [attr.aria-label]="'core.course.todo' | translate">
|
||||
</ion-icon>
|
||||
<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>
|
||||
<ion-icon class="completioninfo completion_fail" name="fas-circle" *ngIf="module.completionStatus === 3"
|
||||
color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate">
|
||||
</ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="module.courseid">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-icon name="fas-lock" *ngIf="module.uservisible === false" slot="end" class="restricted"
|
||||
[attr.aria-label]="'core.restricted' | translate"></ion-icon>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</ion-content>
|
|
@ -0,0 +1,35 @@
|
|||
@import '~theme/globals.scss';
|
||||
core-progress-bar {
|
||||
--bar-margin: 8px 0 4px 0;
|
||||
--line-height: 20px;
|
||||
--background: var(--contrast-background);
|
||||
}
|
||||
|
||||
@if ($core-hide-progress-on-section-selector) {
|
||||
core-progress-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ion-icon.completioninfo {
|
||||
font-size: 10px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
ion-item.section::part(native) {
|
||||
--padding-start: 0;
|
||||
}
|
||||
|
||||
ion-icon.expandable-status-icon {
|
||||
margin: 0;
|
||||
@include padding(12px, 32px, 12px, 16px);
|
||||
}
|
||||
|
||||
ion-item.item-current ion-icon.expandable-status-icon {
|
||||
@include padding(null, null, null, 11px);
|
||||
|
||||
}
|
||||
|
||||
ion-icon.restricted {
|
||||
font-size: 14px;
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
// (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, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import {
|
||||
CoreCourseModuleCompletionStatus,
|
||||
CoreCourseModuleCompletionTracking,
|
||||
CoreCourseProvider,
|
||||
} from '@features/course/services/course';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ModalController } from '@singletons';
|
||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
|
||||
/**
|
||||
* Component to display course index modal.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-course-index',
|
||||
templateUrl: 'course-index.html',
|
||||
styleUrls: ['course-index.scss'],
|
||||
})
|
||||
export class CoreCourseCourseIndexComponent implements OnInit {
|
||||
|
||||
@ViewChild(IonContent) content?: IonContent;
|
||||
|
||||
@Input() sections?: CourseIndexSection[];
|
||||
@Input() selectedId?: number;
|
||||
@Input() course?: CoreCourseAnyCourseData;
|
||||
|
||||
stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||
allSectionId = CoreCourseProvider.ALL_SECTIONS_ID;
|
||||
highlighted?: string;
|
||||
|
||||
constructor(
|
||||
protected elementRef: ElementRef,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
|
||||
if (!this.course || !this.sections || !this.course.enablecompletion || !('courseformatoptions' in this.course) ||
|
||||
!this.course.courseformatoptions) {
|
||||
this.closeModal();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const formatOptions = CoreUtils.objectToKeyValueMap(this.course.courseformatoptions, 'name', 'value');
|
||||
|
||||
if (!formatOptions || formatOptions.completionusertracked === false) {
|
||||
return;
|
||||
}
|
||||
const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
|
||||
currentSection.highlighted = true;
|
||||
if (this.selectedId === undefined) {
|
||||
currentSection.expanded = true;
|
||||
this.selectedId = currentSection.id;
|
||||
} else {
|
||||
const selectedSection = this.sections.find((section) => section.id == this.selectedId);
|
||||
if (selectedSection) {
|
||||
selectedSection.expanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.sections.forEach((section) => {
|
||||
section.modules.forEach((module) => {
|
||||
module.completionStatus = module.completiondata === undefined ||
|
||||
module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
|
||||
? undefined
|
||||
: module.completiondata.state;
|
||||
});
|
||||
});
|
||||
|
||||
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
|
||||
|
||||
setTimeout(() => {
|
||||
CoreDomUtils.scrollToElementBySelector(
|
||||
this.elementRef.nativeElement,
|
||||
this.content,
|
||||
'.item.item-current',
|
||||
);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle expand status.
|
||||
*
|
||||
* @param event Event object.
|
||||
* @param section Section to expand / collapse.
|
||||
*/
|
||||
toggleExpand(event: Event, section: CourseIndexSection): void {
|
||||
section.expanded = !section.expanded;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a section.
|
||||
*
|
||||
* @param event Event.
|
||||
* @param section Selected section object.
|
||||
*/
|
||||
selectSection(event: Event, section: CoreCourseSection): void {
|
||||
if (section.uservisible !== false) {
|
||||
ModalController.dismiss({ event, section });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a section and open a module
|
||||
*
|
||||
* @param event Event.
|
||||
* @param section Selected section object.
|
||||
* @param module Selected module object.
|
||||
*/
|
||||
selectModule(event: Event,section: CoreCourseSection, module: CoreCourseModuleData): void {
|
||||
if (module.uservisible !== false) {
|
||||
ModalController.dismiss({ event, section, module });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type CourseIndexSection = Omit<CoreCourseSection, 'modules'> & {
|
||||
highlighted?: boolean;
|
||||
expanded?: boolean;
|
||||
modules: (CoreCourseModuleData & {
|
||||
completionStatus?: CoreCourseModuleCompletionStatus;
|
||||
})[];
|
||||
};
|
||||
|
||||
export type CoreCourseIndexSectionWithModule = {
|
||||
event: Event;
|
||||
section: CourseIndexSection;
|
||||
module?: CoreCourseModuleData;
|
||||
};
|
|
@ -1,64 +1,14 @@
|
|||
<!-- 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()" iconAction="menu">
|
||||
<core-context-menu-item [hidden]="!displayCourseIndex || !sections || !sections.length" [priority]="500"
|
||||
[content]="'core.course.courseindex' | translate" (action)="openCourseIndex()" iconAction="menu">
|
||||
</core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
<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 ion-justify-content-between ion-align-items-center core-button-selector-row">
|
||||
<core-combobox [modalOptions]="sectionSelectorModalOptions" interface="modal" listboxId="core-course-section-button"
|
||||
icon="fas-folder" [label]="'core.course.section' | translate"
|
||||
[selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate"
|
||||
(onChange)="sectionChanged($event)">
|
||||
<span slot="text">
|
||||
<core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
|
||||
[contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
|
||||
</core-format-text>
|
||||
<ng-container *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</ng-container>
|
||||
</span>
|
||||
</core-combobox>
|
||||
</div>
|
||||
</core-dynamic-component>
|
||||
|
||||
<!-- Course summary. By default we only display the course progress. -->
|
||||
<core-dynamic-component [component]="courseSummaryComponent" [data]="data">
|
||||
<ion-list *ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) ||
|
||||
(selectedSection && selectedSection.id != allSectionsId &&
|
||||
(selectedSection.availabilityinfo || selectedSection.visible === 0))" lines="none" class="core-format-progress-list">
|
||||
<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" a11yText="core.course.aria:sectionprogress">
|
||||
</core-progress-bar>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="selectedSection && selectedSection.id != allSectionsId &&
|
||||
(selectedSection.availabilityinfo || selectedSection.visible === 0)">
|
||||
<ion-badge color="info" class="ion-text-wrap"
|
||||
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible !== false">
|
||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="info" class="ion-text-wrap"
|
||||
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible === false">
|
||||
{{ 'core.notavailable' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="info" 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">
|
||||
|
@ -84,63 +34,75 @@
|
|||
</div>
|
||||
|
||||
<ion-buttons class="ion-padding core-course-section-nav-buttons safe-area-padding-horizontal"
|
||||
*ngIf="displaySectionSelector && sections?.length">
|
||||
*ngIf="displayCourseIndex && sections?.length">
|
||||
<ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary"
|
||||
[attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name">
|
||||
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
<core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
<core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary"
|
||||
[attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name">
|
||||
<core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
<core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
|
||||
<core-block-side-blocks-button *ngIf="course && displayBlocks && hasBlocks" [courseId]="course.id">
|
||||
</core-block-side-blocks-button>
|
||||
</core-loading>
|
||||
</core-dynamic-component>
|
||||
|
||||
|
||||
<core-block-side-blocks-button slot="fixed" *ngIf="loaded && course && displayBlocks && hasBlocks" [courseId]="course.id">
|
||||
</core-block-side-blocks-button>
|
||||
|
||||
<!-- Course Index button. -->
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && displayCourseIndex">
|
||||
<ion-fab-button (click)="openCourseIndex()" [attr.aria-label]="'core.course.courseindex' | translate">
|
||||
<ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon>
|
||||
<span class="sr-only">{{'core.course.courseindex' | translate }}</span>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
|
||||
<!-- Template to render a section. -->
|
||||
<ng-template #sectionTemplate let-section="section">
|
||||
<section *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"
|
||||
<ion-item-divider class="course-section ion-text-wrap" color="light"
|
||||
[class.item-dimmed]="section.visible === 0 || section.uservisible === false">
|
||||
<ion-icon name="fas-folder" aria-label="hidden" slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
<h2 *ngIf="section.name">
|
||||
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="section.visible === 0 || section.availabilityinfo">
|
||||
<ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap">
|
||||
<p *ngIf="section.visible === 0 || section.availabilityinfo || (section.highlighted && highlighted)">
|
||||
<ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap block">
|
||||
<ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap">
|
||||
<ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap block">
|
||||
<ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.notavailable' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="info" *ngIf="section.availabilityinfo" class="ion-text-wrap">
|
||||
<core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
<ion-badge color="info" *ngIf="section.availabilityinfo" class="ion-text-wrap block">
|
||||
<ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate"></ion-icon>
|
||||
<core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</ion-badge>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-badge class="ion-text-wrap block" *ngIf="section.highlighted && highlighted" slot="end">{{highlighted}}</ion-badge>
|
||||
</ion-item-divider>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="section.summary">
|
||||
<ion-label>
|
||||
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
|
||||
(completionChanged)="onCompletionChange($event)" [showActivityDates]="course?.showactivitydates"
|
||||
[showCompletionConditions]="course?.showcompletionconditions">
|
||||
[showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions">
|
||||
</core-course-module>
|
||||
</ng-container>
|
||||
</section>
|
||||
|
|
|
@ -1,70 +1,17 @@
|
|||
@import '~theme/globals.scss';
|
||||
.core-course-section-nav-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
:host {
|
||||
|
||||
.core-format-progress-list {
|
||||
margin-bottom: 0;
|
||||
|
||||
.item {
|
||||
background: transparent;
|
||||
|
||||
.label {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-course-thumb {
|
||||
display: none;
|
||||
height: var(--core-courseimage-on-course-height);
|
||||
width: 100%;
|
||||
core-format-text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
background: var(--ion-item-background);
|
||||
border-bottom: 1px solid var(--stroke);
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
}
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.course-section {
|
||||
ion-badge {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
@if ($core-show-courseimage-on-course) {
|
||||
.core-course-thumb {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@if ($core-hide-progress-on-course) {
|
||||
.core-course-progress {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.core-button-selector-row {
|
||||
display: flex;
|
||||
core-combobox {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.core-course-section-nav-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
core-format-text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
Type,
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
import { ModalOptions } from '@ionic/core';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
|
@ -44,8 +43,10 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg
|
|||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { IonContent, IonRefresher } from '@ionic/angular';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector';
|
||||
import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from '../course-index/course-index';
|
||||
import { CoreBlockHelper } from '@features/block/services/block-helper';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
|
||||
/**
|
||||
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
||||
|
@ -66,8 +67,8 @@ 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?: CoreCourseSection[]; // List of course sections.
|
||||
@Input() course!: CoreCourseAnyCourseData; // The course to render.
|
||||
@Input() sections: CoreCourseSectionToDisplay[] = []; // List of course sections.
|
||||
@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.
|
||||
|
@ -78,7 +79,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
// All the possible component classes.
|
||||
courseFormatComponent?: Type<unknown>;
|
||||
courseSummaryComponent?: Type<unknown>;
|
||||
sectionSelectorComponent?: Type<unknown>;
|
||||
singleSectionComponent?: Type<unknown>;
|
||||
allSectionsComponent?: Type<unknown>;
|
||||
|
||||
|
@ -86,7 +86,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
showSectionId = 0;
|
||||
data: Record<string, unknown> = {}; // Data to pass to the components.
|
||||
|
||||
displaySectionSelector = false;
|
||||
displayCourseIndex = false;
|
||||
displayBlocks = false;
|
||||
hasBlocks = false;
|
||||
selectedSection?: CoreCourseSection;
|
||||
|
@ -95,17 +95,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
|
||||
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||
loaded = false;
|
||||
hasSeveralSections?: boolean;
|
||||
imageThumb?: string;
|
||||
progress?: number;
|
||||
sectionSelectorModalOptions: ModalOptions = {
|
||||
component: CoreCourseSectionSelectorComponent,
|
||||
componentProps: {},
|
||||
};
|
||||
highlighted?: string;
|
||||
|
||||
protected selectTabObserver?: CoreEventObserver;
|
||||
protected completionObserver?: CoreEventObserver;
|
||||
protected lastCourseFormat?: string;
|
||||
protected sectionSelectorExpanded = false;
|
||||
|
||||
constructor(
|
||||
protected content: IonContent,
|
||||
|
@ -116,9 +111,17 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (this.course === undefined) {
|
||||
CoreDomUtils.showErrorModal('Course not set');
|
||||
|
||||
CoreNavigator.back();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for select course tab events to select the right section if needed.
|
||||
this.selectTabObserver = CoreEvents.on(CoreEvents.SELECT_COURSE_TAB, (data) => {
|
||||
if (data.name) {
|
||||
|
@ -137,6 +140,37 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.sectionChanged(section);
|
||||
}
|
||||
});
|
||||
|
||||
// The completion of any of the modules have changed.
|
||||
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => {
|
||||
if (data.completion.courseId != this.course.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit a new event for other components.
|
||||
this.completionChanged.emit(data.completion);
|
||||
|
||||
if (data.completion.valueused !== false || !this.course || !('progress' in this.course) ||
|
||||
typeof this.course.progress != 'number') {
|
||||
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), 0);
|
||||
|
||||
const moduleProgressPercent = 100 / (completionModules || 1);
|
||||
// Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar.
|
||||
if (data.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
|
||||
this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent);
|
||||
} else {
|
||||
this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent);
|
||||
}
|
||||
|
||||
this.updateProgress();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -144,27 +178,20 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
*/
|
||||
async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise<void> {
|
||||
this.setInputData();
|
||||
this.sectionSelectorModalOptions.componentProps!.course = this.course;
|
||||
this.sectionSelectorModalOptions.componentProps!.sections = this.sections;
|
||||
|
||||
if (changes.course && this.course) {
|
||||
// Course has changed, try to get the components.
|
||||
this.getComponents();
|
||||
|
||||
this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course);
|
||||
this.displayCourseIndex = CoreCourseFormatDelegate.displayCourseIndex(this.course);
|
||||
this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course);
|
||||
|
||||
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
|
||||
|
||||
this.updateProgress();
|
||||
|
||||
if ('overviewfiles' in this.course) {
|
||||
this.imageThumb = this.course.overviewfiles?.[0]?.fileurl;
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.sections && this.sections) {
|
||||
this.sectionSelectorModalOptions.componentProps!.sections = this.sections;
|
||||
this.treatSections(this.sections);
|
||||
}
|
||||
}
|
||||
|
@ -192,10 +219,13 @@ 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 currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
|
||||
currentSection.highlighted = true;
|
||||
|
||||
await Promise.all([
|
||||
this.loadCourseFormatComponent(),
|
||||
this.loadCourseSummaryComponent(),
|
||||
this.loadSectionSelectorComponent(),
|
||||
this.loadSingleSectionComponent(),
|
||||
this.loadAllSectionsComponent(),
|
||||
]);
|
||||
|
@ -207,7 +237,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadCourseFormatComponent(): Promise<void> {
|
||||
this.courseFormatComponent = await CoreCourseFormatDelegate.getCourseFormatComponent(this.course!);
|
||||
this.courseFormatComponent = await CoreCourseFormatDelegate.getCourseFormatComponent(this.course);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -216,16 +246,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadCourseSummaryComponent(): Promise<void> {
|
||||
this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load section selector component.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadSectionSelectorComponent(): Promise<void> {
|
||||
this.sectionSelectorComponent = await CoreCourseFormatDelegate.getSectionSelectorComponent(this.course!);
|
||||
this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -234,7 +255,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadSingleSectionComponent(): Promise<void> {
|
||||
this.singleSectionComponent = await CoreCourseFormatDelegate.getSingleSectionComponent(this.course!);
|
||||
this.singleSectionComponent = await CoreCourseFormatDelegate.getSingleSectionComponent(this.course);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -243,7 +264,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadAllSectionsComponent(): Promise<void> {
|
||||
this.allSectionsComponent = await CoreCourseFormatDelegate.getAllSectionsComponent(this.course!);
|
||||
this.allSectionsComponent = await CoreCourseFormatDelegate.getAllSectionsComponent(this.course);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -254,15 +275,16 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
*/
|
||||
protected async treatSections(sections: CoreCourseSection[]): Promise<void> {
|
||||
const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
|
||||
this.hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
|
||||
const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
|
||||
|
||||
if (this.selectedSection) {
|
||||
const selectedSection = 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!));
|
||||
let newSection = sections.find(section => this.compareSections(section, selectedSection));
|
||||
|
||||
if (!newSection) {
|
||||
// Section not found, calculate which one to use.
|
||||
newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course!, sections);
|
||||
newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
|
||||
}
|
||||
|
||||
this.sectionChanged(newSection);
|
||||
|
@ -271,7 +293,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
// There is no selected section yet, calculate which one to load.
|
||||
if (!this.hasSeveralSections) {
|
||||
if (!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]);
|
||||
|
@ -289,7 +311,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
|
||||
if (!this.loaded) {
|
||||
// No section specified, not found or not visible, get current section.
|
||||
const section = await CoreCourseFormatDelegate.getCurrentSection(this.course!, sections);
|
||||
const section = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
|
||||
|
||||
this.loaded = true;
|
||||
this.sectionChanged(section);
|
||||
|
@ -299,20 +321,31 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Display the section selector modal.
|
||||
* Display the course index modal.
|
||||
*/
|
||||
async showSectionSelector(): Promise<void> {
|
||||
if (this.sectionSelectorExpanded) {
|
||||
return;
|
||||
}
|
||||
async openCourseIndex(): Promise<void> {
|
||||
const data = await CoreDomUtils.openModal<CoreCourseIndexSectionWithModule>({
|
||||
component: CoreCourseCourseIndexComponent,
|
||||
componentProps: {
|
||||
course: this.course,
|
||||
sections: this.sections,
|
||||
selectedId: this.selectedSection?.id,
|
||||
},
|
||||
});
|
||||
|
||||
this.sectionSelectorExpanded = true;
|
||||
|
||||
const data = await CoreDomUtils.openModal<CoreCourseSection>(this.sectionSelectorModalOptions);
|
||||
|
||||
this.sectionSelectorExpanded = false;
|
||||
if (data) {
|
||||
this.sectionChanged(data);
|
||||
this.sectionChanged(data.section);
|
||||
if (data.module) {
|
||||
if (!data.module.handlerData) {
|
||||
data.module.handlerData =
|
||||
await CoreCourseModuleDelegate.getModuleDataFor(data.module.modname, data.module, this.course.id);
|
||||
}
|
||||
|
||||
if (data.module.uservisible !== false && data.module.handlerData?.action) {
|
||||
data.module.handlerData.action(data.event, data.module, data.module.course);
|
||||
}
|
||||
this.moduleId = data.module.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,27 +357,26 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
sectionChanged(newSection: CoreCourseSection): void {
|
||||
const previousValue = this.selectedSection;
|
||||
this.selectedSection = newSection;
|
||||
this.sectionSelectorModalOptions.componentProps!.selected = this.selectedSection;
|
||||
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!));
|
||||
const i = this.sections.findIndex((value) => this.compareSections(value, newSection));
|
||||
|
||||
let j: number;
|
||||
for (j = i - 1; j >= 1; j--) {
|
||||
if (this.canViewSection(this.sections![j])) {
|
||||
if (this.canViewSection(this.sections[j])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.previousSection = j >= 1 ? this.sections![j] : undefined;
|
||||
this.previousSection = j >= 1 ? this.sections[j] : undefined;
|
||||
|
||||
for (j = i + 1; j < this.sections!.length; j++) {
|
||||
if (this.canViewSection(this.sections![j])) {
|
||||
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;
|
||||
this.nextSection = j < this.sections.length ? this.sections[j] : undefined;
|
||||
} else {
|
||||
this.previousSection = undefined;
|
||||
this.nextSection = undefined;
|
||||
|
@ -368,7 +400,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
if (!previousValue || previousValue.id != newSection.id) {
|
||||
// First load or section changed, add log in Moodle.
|
||||
CoreUtils.ignoreErrors(
|
||||
CoreCourse.logView(this.course!.id, newSection.section, undefined, this.course!.fullname),
|
||||
CoreCourse.logView(this.course.id, newSection.section, undefined, this.course.fullname),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -475,7 +507,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.selectTabObserver && this.selectTabObserver.off();
|
||||
|
@ -510,35 +542,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
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 != 'number') {
|
||||
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), 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 === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
|
||||
this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent);
|
||||
} else {
|
||||
this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent);
|
||||
}
|
||||
|
||||
this.updateProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update course progress.
|
||||
*/
|
||||
|
@ -559,3 +562,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
type CoreCourseSectionToDisplay = CoreCourseSection & {
|
||||
highlighted?: boolean;
|
||||
};
|
||||
|
|
|
@ -12,14 +12,19 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course';
|
||||
import {
|
||||
CoreCourseCompletionType,
|
||||
CoreCourseModuleCompletionStatus,
|
||||
CoreCourseModuleCompletionTracking,
|
||||
} from '@features/course/services/course';
|
||||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
|
||||
/**
|
||||
* Component to handle activity completion in sites previous to 3.11.
|
||||
|
@ -35,11 +40,29 @@ import { CoreCourseHelper } from '@features/course/services/course-helper';
|
|||
templateUrl: 'core-course-module-completion-legacy.html',
|
||||
styleUrls: ['module-completion-legacy.scss'],
|
||||
})
|
||||
export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent {
|
||||
export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent
|
||||
implements OnInit, OnDestroy {
|
||||
|
||||
completionImage?: string;
|
||||
completionDescription?: string;
|
||||
|
||||
protected completionObserver?: CoreEventObserver;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => {
|
||||
if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.completion = data.completion;
|
||||
this.calculateData();
|
||||
this.completionChanged.emit(this.completion);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -52,30 +75,28 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC
|
|||
let langKey: string | undefined;
|
||||
let image: string | undefined;
|
||||
|
||||
if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL &&
|
||||
this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-manual-n';
|
||||
langKey = 'core.completion-alt-manual-n';
|
||||
} else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL &&
|
||||
this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
|
||||
image = 'completion-manual-y';
|
||||
langKey = 'core.completion-alt-manual-y';
|
||||
} else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-auto-n';
|
||||
langKey = 'core.completion-alt-auto-n';
|
||||
} else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
|
||||
image = 'completion-auto-y';
|
||||
langKey = 'core.completion-alt-auto-y';
|
||||
} else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) {
|
||||
image = 'completion-auto-pass';
|
||||
langKey = 'core.completion-alt-auto-pass';
|
||||
} else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) {
|
||||
image = 'completion-auto-fail';
|
||||
langKey = 'core.completion-alt-auto-fail';
|
||||
if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL) {
|
||||
if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-manual-n';
|
||||
langKey = 'core.completion-alt-manual-n';
|
||||
} else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
|
||||
image = 'completion-manual-y';
|
||||
langKey = 'core.completion-alt-manual-y';
|
||||
}
|
||||
} else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC) {
|
||||
if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-auto-n';
|
||||
langKey = 'core.completion-alt-auto-n';
|
||||
} else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
|
||||
image = 'completion-auto-y';
|
||||
langKey = 'core.completion-alt-auto-y';
|
||||
} else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) {
|
||||
image = 'completion-auto-pass';
|
||||
langKey = 'core.completion-alt-auto-pass';
|
||||
} else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) {
|
||||
image = 'completion-auto-fail';
|
||||
langKey = 'core.completion-alt-auto-fail';
|
||||
}
|
||||
}
|
||||
|
||||
if (image) {
|
||||
|
@ -128,9 +149,16 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC
|
|||
|
||||
await CoreCourseHelper.changeManualCompletion(this.completion, event);
|
||||
|
||||
this.calculateData();
|
||||
// @deprecated MANUAL_COMPLETION_CHANGED is deprecated since 4.0 use COMPLETION_CHANGED instead.
|
||||
CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
|
||||
CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL });
|
||||
}
|
||||
|
||||
this.completionChanged.emit(this.completion);
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.completionObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,16 +4,19 @@
|
|||
<ng-container *ngIf="completion.istrackeduser">
|
||||
<ng-container *ngFor="let rule of details">
|
||||
<ion-chip *ngIf="rule.statuscomplete" color="success" role="listitem" [attr.aria-label]="rule.accessibleDescription">
|
||||
<ion-icon name="fas-check" aria-hidden="true"></ion-icon>
|
||||
<ion-label><strong>{{ 'core.course.completion_automatic:done' | translate }}</strong> {{ rule.rulevalue.description }}
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
|
||||
<ion-chip *ngIf="rule.statuscompletefail" color="danger" role="listitem" [attr.aria-label]="rule.accessibleDescription">
|
||||
<ion-icon name="fas-times" aria-hidden="true"></ion-icon>
|
||||
<ion-label><strong>{{ 'core.course.completion_automatic:failed' | translate }}</strong> {{ rule.rulevalue.description }}
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
|
||||
<ion-chip *ngIf="rule.statusincomplete" color="medium" role="listitem" [attr.aria-label]="rule.accessibleDescription">
|
||||
<ion-chip *ngIf="rule.statusincomplete" role="listitem" [attr.aria-label]="rule.accessibleDescription">
|
||||
<ion-icon name="fas-edit" aria-hidden="true"></ion-icon>
|
||||
<ion-label><strong>{{ 'core.course.completion_automatic:todo' | translate }}</strong> {{ rule.rulevalue.description }}
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
|
|
|
@ -19,12 +19,21 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-content select="[description]"></ng-content>
|
||||
<!-- Activity dates. -->
|
||||
<ion-item class="ion-text-wrap core-module-dates" lines="none" *ngIf="showCompletion && module.dates?.length">
|
||||
|
||||
<ion-item class="ion-text-wrap core-module-dates" lines="none" *ngIf="showCompletion && (module.dates?.length ||
|
||||
(module.completiondata && module.completiondata.isautomatic && module.uservisible))">
|
||||
<ion-label>
|
||||
<p *ngFor="let date of module.dates">
|
||||
<strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
|
||||
</p>
|
||||
<!-- Activity dates. -->
|
||||
<div *ngIf="module.dates?.length" class="core-module-dates">
|
||||
<p *ngFor="let date of module.dates">
|
||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong> {{ date.timestamp * 1000 |
|
||||
coreFormatDate:'strftimedatetime' }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Module completion. -->
|
||||
<core-course-module-completion [completion]="module.completiondata" [moduleName]="module.name" [moduleId]="module.id"
|
||||
[showCompletionConditions]="true">
|
||||
</core-course-module-completion>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-content></ng-content>
|
||||
|
|
|
@ -16,4 +16,9 @@
|
|||
core-mod-icon {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.core-module-dates ion-icon {
|
||||
@include margin-horizontal(null, 8px);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,22 +2,20 @@
|
|||
|
||||
<ng-container *ngIf="completion.istrackeduser">
|
||||
<ng-container *ngIf="completion.state">
|
||||
<ion-button color="success" expand="block" fill="outline" [attr.aria-label]="accessibleDescription"
|
||||
(click)="completionClicked($event)">
|
||||
<ion-button color="success" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)">
|
||||
<ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.course.completion_manual:done' | translate }}
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!completion.state">
|
||||
<ion-button color="dark" expand="block" fill="outline" [attr.aria-label]="accessibleDescription"
|
||||
(click)="completionClicked($event)">
|
||||
<ion-button color="dark" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)">
|
||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!completion.istrackeduser">
|
||||
<ion-button disabled="true" color="dark" expand="block" fill="outline">
|
||||
<ion-button disabled="true" color="dark" fill="outline">
|
||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core';
|
||||
import { CoreCourseCompletionType } from '@features/course/services/course';
|
||||
|
||||
import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
|
@ -34,14 +35,14 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
|
|||
|
||||
accessibleDescription: string | null = null;
|
||||
|
||||
protected manualChangedObserver?: CoreEventObserver;
|
||||
protected completionObserver?: CoreEventObserver;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => {
|
||||
if (!this.completion || this.completion.cmid != data.completion.cmid) {
|
||||
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => {
|
||||
if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -98,14 +99,16 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
|
|||
|
||||
await CoreCourseHelper.changeManualCompletion(this.completion, event);
|
||||
|
||||
// @deprecated MANUAL_COMPLETION_CHANGED is deprecated since 4.0 use COMPLETION_CHANGED instead.
|
||||
CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
|
||||
CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL });
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.manualChangedObserver?.off();
|
||||
this.completionObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,12 +6,10 @@
|
|||
<ion-icon name="fas-arrow-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="showCompletion && (currentModule.completiondata &&
|
||||
(currentModule.completiondata.isautomatic || showManualCompletion) && currentModule.uservisible)">
|
||||
<ion-col *ngIf="showCompletion && (currentModule.completiondata && showManualCompletion && currentModule.uservisible)">
|
||||
<!-- Module completion. -->
|
||||
<core-course-module-completion [completion]="currentModule.completiondata" [moduleName]="currentModule.name"
|
||||
[moduleId]="currentModule.id" [showCompletionConditions]="true" [showManualCompletion]="showManualCompletion"
|
||||
(completionChanged)="completionChanged.emit($event)">
|
||||
[moduleId]="currentModule.id" [showManualCompletion]="true" (completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion>
|
||||
</ion-col>
|
||||
<ion-col size="auto">
|
||||
|
|
|
@ -18,25 +18,26 @@
|
|||
[courseId]="module.course" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<ion-badge *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 *ngIf="module.visible === 0 && (!section || section.visible)" class="ion-text-wrap">
|
||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap">
|
||||
{{ 'core.course.hiddenoncoursepage' | translate }}
|
||||
</ion-badge>
|
||||
<div class="core-module-availabilityinfo" *ngIf="module.availabilityinfo">
|
||||
<ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge>
|
||||
<core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="module.course" class="ion-text-wrap">
|
||||
</core-format-text>
|
||||
</div>
|
||||
<ion-badge *ngIf="module.completiondata?.offline" color="warning" class="ion-text-wrap">
|
||||
{{ 'core.course.manualcompletionnotsynced' | translate }}
|
||||
</ion-badge>
|
||||
<ion-chip class="completioninfo completion_incomplete" *ngIf="completionStatus === 0">
|
||||
<ion-label>
|
||||
<ion-icon name="fas-edit" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.course.todo' | translate }}
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip class="completioninfo completion_complete" *ngIf="completionStatus === 1 || completionStatus === 2"
|
||||
color="success">
|
||||
<ion-icon name="fas-check" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{'core.course.done' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip class="completioninfo completion_fail" *ngIf="completionStatus === 3" color="danger">
|
||||
<ion-icon name="fas-times" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{'core.course.failed' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
|
||||
<ion-chip *ngIf="module.handlerData.extraBadge" [color]="module.handlerData.extraBadgeColor"
|
||||
class="ion-text-wrap ion-text-start" [outline]="true">
|
||||
<ion-label><span [innerHTML]="module.handlerData.extraBadge"></span></ion-label>
|
||||
</ion-chip>
|
||||
</ion-label>
|
||||
<!-- Buttons. -->
|
||||
<div slot="end" *ngIf="module.uservisible !== false" class="buttons core-module-buttons"
|
||||
|
@ -63,10 +64,15 @@
|
|||
'item-dimmed': module.visible === 0 || module.uservisible === false
|
||||
}" detail="false">
|
||||
<ion-label>
|
||||
<core-format-text class="core-module-description" *ngIf="module.description" [maxHeight]="80" [text]="module.description"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="module.course">
|
||||
</core-format-text>
|
||||
|
||||
<!-- Activity dates. -->
|
||||
<div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates">
|
||||
<p *ngFor="let date of module.dates">
|
||||
<strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
|
||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong> {{ date.timestamp *
|
||||
1000 | coreFormatDate:'strftimedatetime' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -76,9 +82,31 @@
|
|||
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion>
|
||||
|
||||
<core-format-text class="core-module-description" *ngIf="module.description" [maxHeight]="80" [text]="module.description"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="module.course">
|
||||
</core-format-text>
|
||||
<ion-chip *ngIf="module.completiondata?.offline" color="warning" class="ion-text-wrap block">
|
||||
<ion-icon name="fas-sync" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.course.manualcompletionnotsynced' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
|
||||
<!-- Availability -->
|
||||
<ion-chip *ngIf="module.visible === 0 && (!section || section.visible)" class="ion-text-wrap block">
|
||||
<ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.course.hiddenfromstudents' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
|
||||
<ion-chip *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap block">
|
||||
<ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.course.hiddenoncoursepage' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
|
||||
<ion-chip class="core-module-availabilityinfo ion-text-wrap block" *ngIf="module.availabilityinfo">
|
||||
<ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate"></ion-icon>
|
||||
<ion-label>
|
||||
<core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="module.course" class="ion-text-wrap">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
|
||||
.item.core-module-main-item {
|
||||
|
@ -59,4 +61,9 @@
|
|||
--inner-border-width: 0px;
|
||||
}
|
||||
|
||||
.core-module-availabilityinfo ion-icon,
|
||||
.core-module-dates ion-icon {
|
||||
@include margin-horizontal(null, 8px);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
CoreCourseModuleCompletionData,
|
||||
CoreCourseSection,
|
||||
} from '@features/course/services/course-helper';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course';
|
||||
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
|
||||
|
||||
/**
|
||||
|
@ -47,6 +47,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
hasInfo = false;
|
||||
showLegacyCompletion = false; // Whether to show module completion in the old format.
|
||||
showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled.
|
||||
completionStatus?: CoreCourseModuleCompletionStatus;
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
|
@ -61,13 +62,22 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title;
|
||||
this.completionStatus = this.module.completiondata === undefined ||
|
||||
this.module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
|
||||
? undefined
|
||||
: this.module.completiondata.state;
|
||||
|
||||
this.hasInfo = !!(
|
||||
this.module.description ||
|
||||
(this.showActivityDates && this.module.dates && this.module.dates.length) ||
|
||||
(this.module.completiondata &&
|
||||
((this.showManualCompletion && !this.module.completiondata.isautomatic) ||
|
||||
(this.showCompletionConditions && this.module.completiondata.isautomatic))
|
||||
)
|
||||
) ||
|
||||
this.module.completiondata?.offline ||
|
||||
(this.module.visible === 0 && (!this.section || this.section.visible)) ||
|
||||
(this.module.visible !== 0 && this.module.isStealth) ||
|
||||
(this.module.availabilityinfo)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h2 id="core-course-section-selector-label">{{ 'core.course.sections' | translate }}</h2>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label">
|
||||
<ng-container *ngFor="let section of sections">
|
||||
<ion-item *ngIf="!section.hiddenbynumsections && section.id != stealthModulesSectionId" class="ion-text-wrap"
|
||||
(click)="selectSection(section)" [attr.aria-current]="selected?.id == section.id ? 'page' : 'false'"
|
||||
[class.item-dimmed]="section.visible === 0 || section.uservisible === false" detail="false"
|
||||
[attr.aria-hidden]="section.uservisible === false" button>
|
||||
|
||||
<ion-icon name="fas-folder" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<core-progress-bar *ngIf="section.progress >= 0" [progress]="section.progress"
|
||||
a11yText="core.course.aria:sectionprogress">
|
||||
</core-progress-bar>
|
||||
|
||||
<ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap">
|
||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap">
|
||||
{{ 'core.notavailable' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="info" *ngIf="section.availabilityinfo" class="ion-text-wrap">
|
||||
<core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course?.id">
|
||||
</core-format-text>
|
||||
</ion-badge>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</ion-content>
|
|
@ -1,15 +0,0 @@
|
|||
@import '~theme/globals.scss';
|
||||
core-progress-bar {
|
||||
--bar-margin: 8px 0 4px 0;
|
||||
--line-height: 20px;
|
||||
}
|
||||
|
||||
@if ($core-hide-progress-on-section-selector) {
|
||||
core-progress-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ion-badge {
|
||||
text-align: start;
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
// (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 } from '@angular/core';
|
||||
|
||||
import { CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import {
|
||||
CoreCourseModuleCompletionStatus,
|
||||
CoreCourseModuleCompletionTracking,
|
||||
CoreCourseProvider,
|
||||
} from '@features/course/services/course';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ModalController } from '@singletons';
|
||||
|
||||
/**
|
||||
* Component to display course section selector in a modal.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-section-selector',
|
||||
templateUrl: 'section-selector.html',
|
||||
styleUrls: ['section-selector.scss'],
|
||||
})
|
||||
export class CoreCourseSectionSelectorComponent implements OnInit {
|
||||
|
||||
@Input() sections?: SectionWithProgress[];
|
||||
@Input() selected?: CoreCourseSection;
|
||||
@Input() course?: CoreCourseAnyCourseData;
|
||||
|
||||
stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
|
||||
if (!this.course || !this.sections || !this.course.enablecompletion || !('courseformatoptions' in this.course) ||
|
||||
!this.course.courseformatoptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formatOptions = CoreUtils.objectToKeyValueMap(this.course.courseformatoptions, 'name', 'value');
|
||||
|
||||
if (!formatOptions || formatOptions.coursedisplay != 1 || formatOptions.completionusertracked === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sections.forEach((section) => {
|
||||
let complete = 0;
|
||||
let total = 0;
|
||||
section.modules.forEach((module) => {
|
||||
if (!module.uservisible || module.completiondata === undefined ||
|
||||
module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
total++;
|
||||
if (module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE ||
|
||||
module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) {
|
||||
complete++;
|
||||
}
|
||||
});
|
||||
|
||||
if (total > 0) {
|
||||
section.progress = complete / total * 100;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a section.
|
||||
*
|
||||
* @param section Selected section object.
|
||||
*/
|
||||
selectSection(section: SectionWithProgress): void {
|
||||
if (section.uservisible !== false) {
|
||||
ModalController.dismiss(section);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type SectionWithProgress = CoreCourseSection & {
|
||||
progress?: number;
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
:host ::ng-deep .collapsible-title {
|
||||
display: none;
|
||||
}
|
|
@ -19,7 +19,7 @@ import { CoreCourseUnsupportedModuleComponent } from '@features/course/component
|
|||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper';
|
||||
import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import { CoreBlockHelper } from '@features/block/services/block-helper';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
|
||||
|
@ -31,11 +31,12 @@ import { CoreCourse } from '@features/course/services/course';
|
|||
@Component({
|
||||
selector: 'core-course-format-single-activity',
|
||||
templateUrl: 'core-course-format-single-activity.html',
|
||||
styleUrls: ['single-activity.scss'],
|
||||
})
|
||||
export class CoreCourseFormatSingleActivityComponent implements OnChanges {
|
||||
|
||||
@Input() course?: CoreCourseAnyCourseData; // The course to render.
|
||||
@Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections.
|
||||
@Input() sections?: CoreCourseSection[]; // List of course sections.
|
||||
@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.
|
||||
|
|
|
@ -71,7 +71,7 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
displaySectionSelector(): boolean {
|
||||
displayCourseIndex(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
import { CoreCourseWSSection } from '@features/course/services/course';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
|
@ -32,20 +32,14 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
|
|||
format = 'weeks';
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return True or promise resolved with true if enabled.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of sections, get the "current" section that should be displayed first.
|
||||
*
|
||||
* @param course The course to get the title.
|
||||
* @param sections List of sections.
|
||||
* @return Current section (or promise resolved with current section).
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
|
||||
const now = CoreTimeUtils.timestamp();
|
||||
|
@ -71,6 +65,13 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
|
|||
return sections[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getSectionHightlightedName(): string {
|
||||
return Translate.instant('core.course.thisweek');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the start and end date of a section.
|
||||
*
|
||||
|
@ -83,7 +84,7 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
|
|||
startDate = startDate + 7200;
|
||||
|
||||
const dates = {
|
||||
start: startDate + (CoreConstants.SECONDS_WEEK * (section.section! - 1)),
|
||||
start: startDate + (CoreConstants.SECONDS_WEEK * ((section.section || 0) - 1)),
|
||||
end: 0,
|
||||
};
|
||||
dates.end = dates.start + CoreConstants.SECONDS_WEEK;
|
||||
|
|
|
@ -26,21 +26,25 @@
|
|||
"confirmdownloadzerosize": "You are about to start downloading.{{availableSpace}} Are you sure you want to continue?",
|
||||
"confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?",
|
||||
"confirmlimiteddownload": "You are not currently connected to Wi-Fi. ",
|
||||
"gotonextactivity": "Continue to next activity",
|
||||
"gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.",
|
||||
"gotopreviousactivity": "Continue to previous activity",
|
||||
"gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.",
|
||||
"courseindex": "Course index",
|
||||
"couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
|
||||
"couldnotloadsections": "Could not load the sections. Please try again later.",
|
||||
"coursesummary": "Course summary",
|
||||
"done": "Done",
|
||||
"downloadcourse": "Download course",
|
||||
"downloadcoursesprogressdescription": "Downloading courses: downloaded {{count}} out of {{total}}.",
|
||||
"downloadsectionprogressdescription": "Downloading section: downloaded {{count}} out of {{total}}.",
|
||||
"errordownloadingcourse": "Error downloading course.",
|
||||
"errordownloadingsection": "Error downloading section.",
|
||||
"errorgetmodule": "Error getting activity data.",
|
||||
"failed": "Failed",
|
||||
"gotonextactivity": "Continue to next activity",
|
||||
"gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.",
|
||||
"gotopreviousactivity": "Continue to previous activity",
|
||||
"gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.",
|
||||
"hiddenfromstudents": "Hidden from students",
|
||||
"hiddenoncoursepage": "Available but not shown on course page",
|
||||
"highlighted": "Highlighted",
|
||||
"insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
|
||||
"insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
|
||||
"manualcompletionnotsynced": "Manual completion not synchronised.",
|
||||
|
@ -49,7 +53,8 @@
|
|||
"overriddennotice": "Your final grade from this activity was manually adjusted.",
|
||||
"refreshcourse": "Refresh course",
|
||||
"section": "Section",
|
||||
"sections": "Sections",
|
||||
"thisweek": "This week",
|
||||
"todo": "To do",
|
||||
"useactivityonbrowser": "You can still use it using your device's web browser.",
|
||||
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
|
||||
"warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}"
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
|
||||
<core-loading [hideUntil]="dataLoaded">
|
||||
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber"
|
||||
[moduleId]="moduleId" (completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}">
|
||||
[moduleId]="moduleId" (completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}"
|
||||
*ngIf="dataLoaded">
|
||||
</core-course-format>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
|
|
@ -384,8 +384,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
*/
|
||||
openCourseSummary(): void {
|
||||
CoreNavigator.navigateToSitePath(
|
||||
'/course/' + this.course.id + '/preview',
|
||||
{ params: { course: this.course } },
|
||||
`/course/${this.course.id}/preview`,
|
||||
{ params: { course: this.course, avoidOpenCourse: true } },
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<ion-header>
|
||||
<ion-header collapsible>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
|
@ -12,4 +12,31 @@
|
|||
<ion-buttons slot="end"></ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<core-tabs-outlet [tabs]="tabs" [hideUntil]="loaded" (ionChange)="tabSelected()"></core-tabs-outlet>
|
||||
<ion-item lines="full" class="core-format-progress-list ion-text-wrap collapsible-title">
|
||||
<ion-avatar slot="start" class="core-course-thumb" *ngIf="imageThumb">
|
||||
<img [src]="imageThumb" core-external-content alt="" />
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<p *ngIf="category">
|
||||
<core-format-text [text]="category" contextLevel="coursecat" [contextInstanceId]="course!.categoryid">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<h1>{{ title }}</h1>
|
||||
</ion-col>
|
||||
<ion-col size="auto" class="ion-align-self-center">
|
||||
<ion-button fill="clear" (click)="openCourseSummary()" [attr.aria-label]="'core.course.coursesummary' | translate"
|
||||
color="dark">
|
||||
<ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<div class="core-course-progress" *ngIf="progress !== undefined">
|
||||
<core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
|
||||
</core-progress-bar>
|
||||
</div>
|
||||
</ion-label>
|
||||
|
||||
</ion-item>
|
||||
<core-tabs-outlet [tabs]="tabs" [hideUntil]="loaded" (ionChange)="tabSelected($event)"></core-tabs-outlet>
|
||||
|
|
|
@ -17,7 +17,7 @@ import { RouterModule, ROUTES, Routes } from '@angular/router';
|
|||
|
||||
import { resolveModuleRoutes } from '@/app/app-routing.module';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreCourseIndexPage } from './index.page';
|
||||
import { CoreCourseIndexPage } from '.';
|
||||
import { COURSE_INDEX_ROUTES } from './index-routing.module';
|
||||
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
@import '~theme/globals.scss';
|
||||
|
||||
:host {
|
||||
.core-course-thumb {
|
||||
height: var(--core-courseimage-on-course-size);
|
||||
min-height: var(--core-courseimage-on-course-size);
|
||||
width: var(--core-courseimage-on-course-size);
|
||||
min-width: var(--core-courseimage-on-course-size);
|
||||
}
|
||||
|
||||
@if ($core-show-courseimage-on-course) {
|
||||
.core-course-thumb {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@if ($core-hide-progress-on-course) {
|
||||
.core-course-progress {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
|
@ -26,6 +26,8 @@ import { CoreUtils } from '@services/utils/utils';
|
|||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CONTENTS_PAGE_NAME } from '@features/course/course.module';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreCollapsibleHeaderDirective } from '@directives/collapsible-header';
|
||||
|
||||
/**
|
||||
* Page that displays the list of courses the user is enrolled in.
|
||||
|
@ -33,23 +35,28 @@ import { CONTENTS_PAGE_NAME } from '@features/course/course.module';
|
|||
@Component({
|
||||
selector: 'page-core-course-index',
|
||||
templateUrl: 'index.html',
|
||||
styleUrls: ['index.scss'],
|
||||
})
|
||||
export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent;
|
||||
@ViewChild(CoreCollapsibleHeaderDirective) ionCollapsibleHeader?: CoreCollapsibleHeaderDirective;
|
||||
|
||||
title?: string;
|
||||
title = '';
|
||||
category = '';
|
||||
course?: CoreCourseAnyCourseData;
|
||||
tabs: CourseTab[] = [];
|
||||
loaded = false;
|
||||
imageThumb?: string;
|
||||
progress?: number;
|
||||
|
||||
protected currentPagePath = '';
|
||||
protected selectTabObserver: CoreEventObserver;
|
||||
protected firstTabName?: string;
|
||||
protected module?: CoreCourseModuleData;
|
||||
protected modParams?: Params;
|
||||
protected isGuest?: boolean;
|
||||
protected contentsTab: CoreTabsOutletTab = {
|
||||
protected isGuest = false;
|
||||
protected contentsTab: CoreTabsOutletTab & { pageParams: Params } = {
|
||||
page: CONTENTS_PAGE_NAME,
|
||||
title: 'core.course',
|
||||
pageParams: {},
|
||||
|
@ -60,10 +67,10 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
if (!data.name) {
|
||||
// If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
|
||||
if (data.sectionId) {
|
||||
this.contentsTab.pageParams!.sectionId = data.sectionId;
|
||||
this.contentsTab.pageParams.sectionId = data.sectionId;
|
||||
}
|
||||
if (data.sectionNumber) {
|
||||
this.contentsTab.pageParams!.sectionNumber = data.sectionNumber;
|
||||
this.contentsTab.pageParams.sectionNumber = data.sectionNumber;
|
||||
}
|
||||
|
||||
// Select course contents.
|
||||
|
@ -79,7 +86,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
// Increase route depth.
|
||||
|
@ -87,12 +94,19 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
|
||||
CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, ''));
|
||||
|
||||
// Get params.
|
||||
this.course = CoreNavigator.getRouteParam('course');
|
||||
try {
|
||||
this.course = CoreNavigator.getRequiredRouteParam('course');
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
CoreNavigator.back();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.firstTabName = CoreNavigator.getRouteParam('selectedTab');
|
||||
this.module = CoreNavigator.getRouteParam<CoreCourseModuleData>('module');
|
||||
this.modParams = CoreNavigator.getRouteParam<Params>('modParams');
|
||||
this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest');
|
||||
this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
|
||||
|
||||
this.currentPagePath = CoreNavigator.getCurrentPath();
|
||||
this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page);
|
||||
|
@ -104,7 +118,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
if (this.module) {
|
||||
this.contentsTab.pageParams!.moduleId = this.module.id;
|
||||
this.contentsTab.pageParams.moduleId = this.module.id;
|
||||
}
|
||||
|
||||
this.tabs.push(this.contentsTab);
|
||||
|
@ -112,20 +126,23 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
|
||||
await Promise.all([
|
||||
this.loadCourseHandlers(),
|
||||
this.loadTitle(),
|
||||
this.loadBasinInfo(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A tab was selected.
|
||||
*/
|
||||
tabSelected(): void {
|
||||
if (this.module) {
|
||||
// Now that the first tab has been selected we can load the module.
|
||||
CoreCourseHelper.openModule(this.module, this.course!.id, this.contentsTab.pageParams!.sectionId, this.modParams);
|
||||
tabSelected(tabToSelect: CoreTabsOutletTab): void {
|
||||
this.ionCollapsibleHeader?.setupContent(tabToSelect.id);
|
||||
|
||||
delete this.module;
|
||||
if (!this.module || !this.course) {
|
||||
return;
|
||||
}
|
||||
// Now that the first tab has been selected we can load the module.
|
||||
CoreCourseHelper.openModule(this.module, this.course.id, this.contentsTab.pageParams.sectionId, this.modParams);
|
||||
|
||||
delete this.module;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,8 +151,12 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadCourseHandlers(): Promise<void> {
|
||||
if (!this.course) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the course handlers.
|
||||
const handlers = await CoreCourseOptionsDelegate.getHandlersToDisplay(this.course!, false, this.isGuest);
|
||||
const handlers = await CoreCourseOptionsDelegate.getHandlersToDisplay(this.course, false, this.isGuest);
|
||||
|
||||
let tabToLoad: number | undefined;
|
||||
|
||||
|
@ -169,23 +190,34 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadTitle(): Promise<void> {
|
||||
protected async loadBasinInfo(): Promise<void> {
|
||||
if (!this.course) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the title to display initially.
|
||||
this.title = CoreCourseFormatDelegate.getCourseTitle(this.course!);
|
||||
this.title = CoreCourseFormatDelegate.getCourseTitle(this.course);
|
||||
this.category = 'categoryname' in this.course ? this.course.categoryname : '';
|
||||
|
||||
if ('overviewfiles' in this.course) {
|
||||
this.imageThumb = this.course.overviewfiles?.[0]?.fileurl;
|
||||
}
|
||||
|
||||
this.updateProgress();
|
||||
|
||||
// Load sections.
|
||||
const sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course!.id, false, true));
|
||||
const sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true));
|
||||
|
||||
if (!sections) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the title again now that we have sections.
|
||||
this.title = CoreCourseFormatDelegate.getCourseTitle(this.course!, sections);
|
||||
this.title = CoreCourseFormatDelegate.getCourseTitle(this.course, sections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
const path = CoreNavigator.getRouteFullPath(this.route.snapshot);
|
||||
|
@ -208,6 +240,39 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
this.tabsComponent?.ionViewDidLeave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the course summary
|
||||
*/
|
||||
openCourseSummary(): void {
|
||||
if (!this.course) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoreNavigator.navigateToSitePath(
|
||||
`/course/${this.course.id}/preview`,
|
||||
{ params: { course: this.course, avoidOpenCourse: true } },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update course progress.
|
||||
*/
|
||||
protected updateProgress(): void {
|
||||
if (
|
||||
!this.course ||
|
||||
!('progress' in this.course) ||
|
||||
typeof this.course.progress !== 'number' ||
|
||||
this.course.progress < 0 ||
|
||||
this.course.completionusertracked === false
|
||||
) {
|
||||
this.progress = undefined;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.progress = this.course.progress;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type CourseTab = CoreTabsOutletTab & {
|
|
@ -27,33 +27,35 @@
|
|||
<core-course-module-info [module]="module" [courseId]="courseId" [description]="module.description" [component]="module.modname"
|
||||
[componentId]="module.id" [expandDescription]="true">
|
||||
|
||||
<div class="safe-area-padding-horizontal ion-padding" *ngIf="module.handlerData?.extraBadge">
|
||||
<ion-badge class="ion-text-wrap ion-text-start" [color]="module.handlerData?.extraBadgeColor">
|
||||
<span [innerHTML]="module.handlerData?.extraBadge"></span>
|
||||
</ion-badge>
|
||||
<div class="ion-padding" *ngIf="module.handlerData?.extraBadge">
|
||||
<ion-chip class="ion-text-wrap ion-text-start" [color]="module.handlerData?.extraBadgeColor">
|
||||
<ion-label><span [innerHTML]="module.handlerData?.extraBadge"></span></ion-label>
|
||||
</ion-chip>
|
||||
</div>
|
||||
<div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible === 0 && (!section || section.visible)">
|
||||
<ion-badge class="ion-text-wrap">
|
||||
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||
</ion-badge>
|
||||
<div class="ion-padding" *ngIf="module.visible === 0 && (!section || section.visible)">
|
||||
<ion-chip class="ion-text-wrap">
|
||||
<ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.course.hiddenfromstudents' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
</div>
|
||||
<div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible !== 0 && module.isStealth">
|
||||
<ion-badge class="ion-text-wrap">
|
||||
{{ 'core.course.hiddenoncoursepage' | translate }}
|
||||
</ion-badge>
|
||||
<div class="ion-padding" *ngIf="module.visible !== 0 && module.isStealth">
|
||||
<ion-chip class="ion-text-wrap">
|
||||
<ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.course.hiddenoncoursepage' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
</div>
|
||||
<div class="safe-area-padding-horizontal ion-padding core-module-availabilityinfo" *ngIf="module.availabilityinfo">
|
||||
<ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge>
|
||||
<div>
|
||||
<div class="ion-padding core-module-availabilityinfo" *ngIf="module.availabilityinfo">
|
||||
<ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate"></ion-icon>
|
||||
<ion-label>
|
||||
<core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId" class="ion-text-wrap">
|
||||
</core-format-text>
|
||||
</div>
|
||||
</ion-label>
|
||||
</div>
|
||||
<div class="safe-area-padding-horizontal ion-padding" *ngIf="module.completiondata?.offline">
|
||||
<ion-badge color="warning" class="ion-text-wrap">
|
||||
{{ 'core.course.manualcompletionnotsynced' | translate }}
|
||||
</ion-badge>
|
||||
<div class="ion-padding" *ngIf="module.completiondata?.offline">
|
||||
<ion-chip color="warning" class="ion-text-wrap">
|
||||
<ion-label>{{ 'core.course.manualcompletionnotsynced' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
</div>
|
||||
|
||||
<core-course-unsupported-module *ngIf="unsupported" [module]="module" [courseId]="courseId"></core-course-unsupported-module>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</ion-buttons>
|
||||
<ion-title>
|
||||
<h1>
|
||||
<core-format-text [text]="course?.fullname" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text>
|
||||
{{'core.course.coursesummary' | translate}}
|
||||
</h1>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
|
@ -16,44 +16,49 @@
|
|||
</ion-refresher>
|
||||
<core-loading [hideUntil]="dataLoaded">
|
||||
<div *ngIf="courseImageUrl" class="core-course-thumb-parallax">
|
||||
<div (click)="openCourse()" class="core-course-thumb">
|
||||
<div class="core-course-thumb">
|
||||
<img [src]="courseImageUrl" core-external-content alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="core-course-thumb-parallax-content" *ngIf="course">
|
||||
<ion-item class="ion-text-wrap" (click)="openCourse()" [attr.aria-label]="course.fullname" [detail]="canAccessCourse"
|
||||
[button]="canAccessCourse">
|
||||
<ion-icon name="fas-graduation-cap" fixed-width slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="course.categoryname">
|
||||
<core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<h2>
|
||||
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="course.startdate">
|
||||
{{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }}
|
||||
<span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}</span>
|
||||
</p>
|
||||
<div class="core-course-progress" *ngIf="progress !== undefined">
|
||||
<core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
|
||||
</core-progress-bar>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false">
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
{{'core.summary' | translate}}
|
||||
</p>
|
||||
<core-format-text [text]="course.summary" [maxHeight]="120" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container class="ion-text-wrap" *ngIf="course.contacts && course.contacts.length">
|
||||
<ion-item-divider>
|
||||
<ion-list *ngIf="course.contacts && course.contacts.length">
|
||||
<ion-item-divider class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.teachers' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id"
|
||||
<ion-item button class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id"
|
||||
[courseId]="isEnrolled ? course.id : null" [attr.aria-label]="'core.viewprofile' | translate" detail="true">
|
||||
<core-user-avatar [user]="contact" slot="start" [userId]="contact.id" [courseId]="isEnrolled ? course.id : null">
|
||||
</core-user-avatar>
|
||||
|
@ -62,7 +67,7 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
<core-spacer></core-spacer>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="course.customfields">
|
||||
<ion-label>
|
||||
|
@ -83,7 +88,8 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<div *ngIf="!isEnrolled" detail="false">
|
||||
<!-- Enrol -->
|
||||
<ng-container *ngIf="!isEnrolled">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let instance of selfEnrolInstances">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ instance.name }}</p>
|
||||
|
@ -92,23 +98,24 @@
|
|||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
<ion-item class="ion-text-wrap" *ngIf="!isEnrolled && paypalEnabled">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.courses.paypalaccepted' | translate }}</h2>
|
||||
<p>{{ 'core.paymentinstant' | translate }}</p>
|
||||
<ion-button expand="block" class="ion-margin-top" (click)="paypalEnrol()" *ngIf="isMobile">
|
||||
{{ 'core.courses.sendpaymentbutton' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled">
|
||||
<ion-label>
|
||||
<p>{{ 'core.courses.notenrollable' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false"
|
||||
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate" button>
|
||||
<ion-item class="ion-text-wrap" *ngIf="paypalEnabled">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.courses.paypalaccepted' | translate }}</p>
|
||||
<p *ngIf="isMobile">{{ 'core.paymentinstant' | translate }}</p>
|
||||
<ion-button *ngIf="isMobile" expand="block" class="ion-margin-top" (click)="paypalEnrol()">
|
||||
{{ 'core.courses.sendpaymentbutton' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="!selfEnrolInstances.length && !paypalEnabled">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.courses.notenrollable' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ion-button class="ion-margin" *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" expand="block"
|
||||
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate">
|
||||
<ion-icon *ngIf="(prefetchCourseData.status != statusDownloaded) && !prefetchCourseData.loading"
|
||||
[name]="prefetchCourseData.icon" slot="start" aria-hidden="true">
|
||||
</ion-icon>
|
||||
|
@ -116,23 +123,24 @@
|
|||
[name]="prefetchCourseData.icon" color="success" aria-hidden="true" role="status">
|
||||
</ion-icon>
|
||||
<ion-spinner *ngIf="prefetchCourseData.loading" slot="start" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
<ion-label>
|
||||
<h2 *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</h2>
|
||||
<h2 *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="openCourse()" [attr.aria-label]="course.fullname" *ngIf="canAccessCourse" detail="true">
|
||||
<ion-label *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</ion-label>
|
||||
<ion-label *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</ion-label>
|
||||
</ion-button>
|
||||
|
||||
<ion-button class="ion-margin" (click)="openCourse()" *ngIf="!avoidOpenCourse && canAccessCourse" expand="block">
|
||||
<ion-icon name="fas-briefcase" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.course' | translate }}</h2>
|
||||
{{ 'core.course' | translate }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item [href]="courseUrl" core-link [attr.aria-label]="course.fullname" button detail="false" [showBrowserWarning]="false">
|
||||
</ion-button>
|
||||
|
||||
<ion-button class="ion-margin" [href]="courseUrl" core-link [showBrowserWarning]="false" expand="block">
|
||||
<ion-icon name="fas-external-link-alt" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.openinbrowser' | translate }}</h2>
|
||||
{{ 'core.openinbrowser' | translate }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-button>
|
||||
|
||||
</div>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
|
|
@ -20,8 +20,8 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import {
|
||||
CoreCourseCustomField,
|
||||
CoreCourseEnrolmentMethod,
|
||||
CoreCourseGetCoursesData,
|
||||
CoreCourses,
|
||||
CoreCourseSearchedData,
|
||||
CoreCoursesProvider,
|
||||
|
@ -34,6 +34,8 @@ import { Translate } from '@singletons';
|
|||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreCoursesSelfEnrolPasswordComponent } from '../../../courses/components/self-enrol-password/self-enrol-password';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourseWithImageAndColor } from '@features/courses/services/courses-helper';
|
||||
|
||||
/**
|
||||
* Page that allows "previewing" a course and enrolling in it if enabled and not enrolled.
|
||||
|
@ -45,12 +47,13 @@ import { CoreNavigator } from '@services/navigator';
|
|||
})
|
||||
export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
||||
|
||||
course?: CoreCourseSearchedData;
|
||||
course?: CoreCourseSummaryData;
|
||||
isEnrolled = false;
|
||||
canAccessCourse = true;
|
||||
selfEnrolInstances: CoreCourseEnrolmentMethod[] = [];
|
||||
paypalEnabled = false;
|
||||
dataLoaded = false;
|
||||
avoidOpenCourse = false;
|
||||
prefetchCourseData: CorePrefetchStatusInfo = {
|
||||
icon: '',
|
||||
statusTranslatable: 'core.loading',
|
||||
|
@ -64,6 +67,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
courseUrl = '';
|
||||
courseImageUrl?: string;
|
||||
isMobile: boolean;
|
||||
progress?: number;
|
||||
|
||||
protected isGuestEnabled = false;
|
||||
protected useGuestAccess = false;
|
||||
|
@ -74,6 +78,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
protected paypalReturnUrl = '';
|
||||
protected pageDestroyed = false;
|
||||
protected courseStatusObserver?: CoreEventObserver;
|
||||
protected courseId!: number;
|
||||
|
||||
constructor(
|
||||
protected zone: NgZone,
|
||||
|
@ -84,7 +89,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
if (this.downloadCourseEnabled) {
|
||||
// Listen for status change in course.
|
||||
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
|
||||
if (data.courseId == this.course!.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
|
||||
if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
|
||||
this.updateCourseStatus(data.status);
|
||||
}
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
|
@ -92,27 +97,25 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.course = CoreNavigator.getRouteParam('course');
|
||||
|
||||
if (!this.course) {
|
||||
try {
|
||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
CoreNavigator.back();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSite = CoreSites.getCurrentSite();
|
||||
const currentSiteUrl = currentSite && currentSite.getURL();
|
||||
this.avoidOpenCourse = !!CoreNavigator.getRouteBooleanParam('avoidOpenCourse');
|
||||
this.course = CoreNavigator.getRouteParam('course');
|
||||
|
||||
this.paypalEnabled = this.course!.enrollmentmethods?.indexOf('paypal') > -1;
|
||||
this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/index.php?id=' + this.course!.id);
|
||||
this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'course/view.php?id=' + this.course!.id);
|
||||
this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/paypal/return.php');
|
||||
if (this.course.overviewfiles.length > 0) {
|
||||
this.courseImageUrl = this.course.overviewfiles[0].fileurl;
|
||||
}
|
||||
const currentSiteUrl = CoreSites.getRequiredCurrentSite().getURL();
|
||||
this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/index.php?id=' + this.courseId);
|
||||
this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'course/view.php?id=' + this.courseId);
|
||||
this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/paypal/return.php');
|
||||
|
||||
try {
|
||||
await this.getCourse();
|
||||
|
@ -120,11 +123,11 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
if (this.downloadCourseEnabled) {
|
||||
|
||||
// Determine course prefetch icon.
|
||||
this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.course!.id);
|
||||
this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId);
|
||||
|
||||
if (this.prefetchCourseData.loading) {
|
||||
// Course is being downloaded. Get the download promise.
|
||||
const promise = CoreCourseHelper.getCourseDownloadPromise(this.course!.id);
|
||||
const promise = CoreCourseHelper.getCourseDownloadPromise(this.courseId);
|
||||
if (promise) {
|
||||
// There is a download promise. If it fails, show an error.
|
||||
promise.catch((error) => {
|
||||
|
@ -134,7 +137,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
});
|
||||
} else {
|
||||
// No download, this probably means that the app was closed while downloading. Set previous status.
|
||||
CoreCourse.setCoursePreviousStatus(this.course!.id);
|
||||
CoreCourse.setCoursePreviousStatus(this.courseId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -177,13 +180,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
this.selfEnrolInstances = [];
|
||||
|
||||
try {
|
||||
this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.course!.id);
|
||||
this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.courseId);
|
||||
|
||||
this.enrolmentMethods.forEach((method) => {
|
||||
if (method.type === 'self') {
|
||||
this.selfEnrolInstances.push(method);
|
||||
} else if (method.type === 'guest') {
|
||||
this.isGuestEnabled = true;
|
||||
} else if (method.type === 'paypal') {
|
||||
this.paypalEnabled = true;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -191,22 +196,17 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
try {
|
||||
let course: CoreEnrolledCourseData | CoreCourseGetCoursesData;
|
||||
|
||||
// Check if user is enrolled in the course.
|
||||
try {
|
||||
course = await CoreCourses.getUserCourse(this.course!.id);
|
||||
this.course = await CoreCourses.getUserCourse(this.courseId);
|
||||
this.isEnrolled = true;
|
||||
} catch {
|
||||
// The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course.
|
||||
this.isEnrolled = false;
|
||||
|
||||
course = await CoreCourses.getCourse(this.course!.id);
|
||||
this.course = await CoreCourses.getCourse(this.courseId);
|
||||
}
|
||||
|
||||
// Success retrieving the course, we can assume the user has permissions to view it.
|
||||
this.course!.fullname = course.fullname || this.course!.fullname;
|
||||
this.course!.summary = course.summary || this.course!.summary;
|
||||
this.canAccessCourse = true;
|
||||
this.useGuestAccess = false;
|
||||
} catch {
|
||||
|
@ -219,14 +219,37 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
if (!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) {
|
||||
try {
|
||||
const course = await CoreCourses.getCourseByField('id', this.course!.id);
|
||||
if (this.course && 'overviewfiles' in this.course && this.course.overviewfiles?.length) {
|
||||
this.courseImageUrl = this.course.overviewfiles[0].fileurl;
|
||||
}
|
||||
|
||||
this.course!.customfields = course.customfields;
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
try {
|
||||
const courseByField = await CoreCourses.getCourseByField('id', this.courseId);
|
||||
if (this.course) {
|
||||
this.course.customfields = courseByField.customfields;
|
||||
this.course.contacts = courseByField.contacts;
|
||||
this.course.displayname = courseByField.displayname;
|
||||
this.course.categoryname = courseByField.categoryname;
|
||||
this.course.overviewfiles = courseByField.overviewfiles;
|
||||
} else {
|
||||
this.course = courseByField;
|
||||
}
|
||||
|
||||
this.paypalEnabled = !this.isEnrolled && courseByField.enrollmentmethods?.indexOf('paypal') > -1;
|
||||
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
if (!this.course ||
|
||||
!('progress' in this.course) ||
|
||||
typeof this.course.progress !== 'number' ||
|
||||
this.course.progress < 0 ||
|
||||
this.course.completionusertracked === false
|
||||
) {
|
||||
this.progress = undefined;
|
||||
} else {
|
||||
this.progress = this.course.progress;
|
||||
}
|
||||
|
||||
this.dataLoaded = true;
|
||||
|
@ -234,13 +257,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
|
||||
/**
|
||||
* Open the course.
|
||||
*
|
||||
* @param replaceCurrentPage If current place should be replaced in the navigation stack.
|
||||
*/
|
||||
openCourse(): void {
|
||||
if (!this.canAccessCourse) {
|
||||
openCourse(replaceCurrentPage = false): void {
|
||||
if (!this.canAccessCourse || !this.course || this.avoidOpenCourse) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoreCourseHelper.openCourse(this.course!, { isGuest: this.useGuestAccess });
|
||||
CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,7 +304,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
// Open the enrolment page in InAppBrowser.
|
||||
const window = await CoreSites.getCurrentSite()!.openInAppWithAutoLogin(this.enrolUrl);
|
||||
const window = await CoreSites.getRequiredCurrentSite().openInAppWithAutoLogin(this.enrolUrl);
|
||||
|
||||
// Observe loaded pages in the InAppBrowser to check if the enrol process has ended.
|
||||
const inAppLoadSubscription = window.on('loadstart').subscribe((event) => {
|
||||
|
@ -319,7 +344,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
const modal = await CoreDomUtils.showModalLoading('core.loading', true);
|
||||
|
||||
try {
|
||||
await CoreCourses.selfEnrol(this.course!.id, password, instanceId);
|
||||
await CoreCourses.selfEnrol(this.courseId, password, instanceId);
|
||||
|
||||
// Close modal and refresh data.
|
||||
this.isEnrolled = true;
|
||||
|
@ -331,13 +356,13 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
await this.refreshData().finally(() => {
|
||||
// My courses have been updated, trigger event.
|
||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
|
||||
courseId: this.course!.id,
|
||||
courseId: this.courseId,
|
||||
course: this.course,
|
||||
action: CoreCoursesProvider.ACTION_ENROL,
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
});
|
||||
|
||||
this.openCourse();
|
||||
this.openCourse(true);
|
||||
|
||||
modal?.dismiss();
|
||||
} catch (error) {
|
||||
|
@ -378,12 +403,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(CoreCourses.invalidateUserCourses());
|
||||
promises.push(CoreCourses.invalidateCourse(this.course!.id));
|
||||
promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.course!.id));
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.course!.id));
|
||||
if (CoreSites.getCurrentSite() && !CoreSites.getCurrentSite()!.isVersionGreaterEqualThan('3.7')) {
|
||||
promises.push(CoreCourses.invalidateCoursesByField('id', this.course!.id));
|
||||
}
|
||||
promises.push(CoreCourses.invalidateCourse(this.courseId));
|
||||
promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId));
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.courseId));
|
||||
promises.push(CoreCourses.invalidateCoursesByField('id', this.courseId));
|
||||
if (this.guestInstanceId) {
|
||||
promises.push(CoreCourses.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId));
|
||||
}
|
||||
|
@ -419,14 +442,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
// Check if user is enrolled in the course.
|
||||
try {
|
||||
CoreCourses.invalidateUserCourses();
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
await CoreUtils.ignoreErrors(CoreCourses.invalidateUserCourses());
|
||||
|
||||
try {
|
||||
await CoreCourses.getUserCourse(this.course!.id);
|
||||
await CoreCourses.getUserCourse(this.courseId);
|
||||
} catch {
|
||||
// Not enrolled, wait a bit and try again.
|
||||
if (this.pageDestroyed || (Date.now() - this.waitStart > 60000)) {
|
||||
|
@ -451,7 +470,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
*/
|
||||
async prefetchCourse(): Promise<void> {
|
||||
try {
|
||||
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course!, {
|
||||
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course as CoreEnrolledCourseData, {
|
||||
isGuest: this.useGuestAccess,
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -462,14 +481,20 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.pageDestroyed = true;
|
||||
|
||||
if (this.courseStatusObserver) {
|
||||
this.courseStatusObserver.off();
|
||||
}
|
||||
this.courseStatusObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type CoreCourseSummaryData = CoreCourseWithImageAndColor & (CoreEnrolledCourseData | CoreCourseSearchedData) & {
|
||||
contacts?: { // Contact users.
|
||||
id: number; // Contact user id.
|
||||
fullname: string; // Contact user fullname.
|
||||
}[];
|
||||
customfields?: CoreCourseCustomField[]; // Custom fields and associated values.
|
||||
categoryname?: string; // Category name.
|
||||
};
|
||||
|
|
|
@ -1,41 +1,43 @@
|
|||
:host {
|
||||
--scroll-factor: 0.5;
|
||||
--translate-z: calc(-2 * var(--scroll-factor))px;
|
||||
--scale: calc(1 + var(--scroll-factor) * 2);
|
||||
ion-content:not(.animating) {
|
||||
&::part(scroll) {
|
||||
perspective: 1px;
|
||||
perspective-origin: center top;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
perspective: 1px;
|
||||
perspective-origin: center top;
|
||||
transform-style: preserve-3d;
|
||||
.core-course-thumb {
|
||||
transform-origin: center top;
|
||||
|
||||
// @todo This parallax effect caused the image to be scaled during page transitions,
|
||||
// and in some devices it seems like the problem persisted even after the transition.
|
||||
// We should decide whether we want to keep this parallax or not, and if we do fix
|
||||
// the problem or find an alternative implementation. For now, it's disabled.
|
||||
--scroll-factor: 0.5;
|
||||
--translate-z: calc(-2 * var(--scroll-factor))px;
|
||||
--scale: calc(1 + var(--scroll-factor) * 2);
|
||||
|
||||
/**
|
||||
* Calculated with scroll-factor: 0.5;
|
||||
* translate-z: -2 * $scroll-factor px;
|
||||
* scale: 1 + $scroll-factor * 2;
|
||||
*/
|
||||
transform: translateZ(-1px) scale(2);
|
||||
}
|
||||
}
|
||||
|
||||
.core-course-thumb-parallax-content {
|
||||
transform: translateZ(0);
|
||||
-webkit-filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
|
||||
filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
|
||||
}
|
||||
|
||||
.core-course-thumb-parallax {
|
||||
height: 40vw;
|
||||
max-height: 35vh;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// .core-course-thumb-parallax-content {
|
||||
// transform: translateZ(0);
|
||||
// -webkit-filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
|
||||
// filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
|
||||
// }
|
||||
// .core-course-thumb-parallax {
|
||||
// height: 40vw;
|
||||
// max-height: 35vh;
|
||||
// z-index: -1;
|
||||
// overflow: hidden;
|
||||
// }
|
||||
.core-course-thumb {
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transform-origin: center top;
|
||||
|
||||
/**
|
||||
* Calculated with scroll-factor: 0.5;
|
||||
* translate-z: -2 * $scroll-factor px;
|
||||
* scale: 1 + $scroll-factor * 2;
|
||||
*/
|
||||
// transform: translateZ(-1px) scale(2);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
|||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||
import { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home';
|
||||
import { CoreStatusWithWarningsWSResponse } from '@services/ws';
|
||||
|
||||
|
@ -1178,7 +1178,7 @@ export class CoreCourseHelperProvider {
|
|||
|
||||
modal?.dismiss();
|
||||
|
||||
return this.openCourse(course, params, siteId);
|
||||
return this.openCourse(course, { params , siteId });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2020,20 +2020,25 @@ export class CoreCourseHelperProvider {
|
|||
* they will see the result immediately.
|
||||
*
|
||||
* @param course Course to open
|
||||
* @param params Params to pass to the course page.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param navOptions Navigation options that includes params to pass to the page.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params, siteId?: string): Promise<void> {
|
||||
async openCourse(
|
||||
course: CoreCourseAnyCourseData | { id: number },
|
||||
navOptions?: CoreNavigationOptions & { siteId?: string },
|
||||
): Promise<void> {
|
||||
const siteId = navOptions?.siteId;
|
||||
if (!siteId || siteId == CoreSites.getCurrentSiteId()) {
|
||||
// Current site, we can open the course.
|
||||
return CoreCourse.openCourse(course, params);
|
||||
return CoreCourse.openCourse(course, navOptions);
|
||||
} else {
|
||||
// We need to load the site first.
|
||||
params = params || {};
|
||||
Object.assign(params, { course: course });
|
||||
navOptions = navOptions || {};
|
||||
|
||||
await CoreNavigator.navigateToSitePath(`course/${course.id}`, { siteId, params });
|
||||
navOptions.params = navOptions.params || {};
|
||||
Object.assign(navOptions.params, { course: course });
|
||||
|
||||
await CoreNavigator.navigateToSitePath(`course/${course.id}`, navOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ import { CoreCourseLogCronHandler } from './handlers/log-cron';
|
|||
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
|
||||
import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync';
|
||||
import { CoreTagItem } from '@features/tag/services/tag';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||
import { CoreCourseModuleDelegate } from './module-delegate';
|
||||
|
||||
const ROOT_CACHE_KEY = 'mmCourse:';
|
||||
|
@ -62,7 +62,7 @@ declare module '@singletons/events' {
|
|||
}
|
||||
|
||||
/**
|
||||
* Completion status valid values.
|
||||
* Course Module completion status enumeration.
|
||||
*/
|
||||
export enum CoreCourseModuleCompletionStatus {
|
||||
COMPLETION_INCOMPLETE = 0,
|
||||
|
@ -71,6 +71,11 @@ export enum CoreCourseModuleCompletionStatus {
|
|||
COMPLETION_COMPLETE_FAIL = 3,
|
||||
}
|
||||
|
||||
export enum CoreCourseCompletionType {
|
||||
MANUAL = 0,
|
||||
AUTO = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion tracking valid values.
|
||||
*/
|
||||
|
@ -1172,10 +1177,13 @@ export class CoreCourseProvider {
|
|||
* This function must be in here instead of course helper to prevent circular dependencies.
|
||||
*
|
||||
* @param course Course to open
|
||||
* @param params Other params to pass to the course page.
|
||||
* @param navOptions Navigation options that includes params to pass to the page.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise<void> {
|
||||
async openCourse(
|
||||
course: CoreCourseAnyCourseData | { id: number },
|
||||
navOptions?: CoreNavigationOptions,
|
||||
): Promise<void> {
|
||||
const loading = await CoreDomUtils.showModalLoading();
|
||||
|
||||
// Wait for site plugins to be fetched.
|
||||
|
@ -1192,7 +1200,7 @@ export class CoreCourseProvider {
|
|||
if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) {
|
||||
// No custom format plugin. We don't need to wait for anything.
|
||||
loading.dismiss();
|
||||
await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params);
|
||||
await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -1203,7 +1211,7 @@ export class CoreCourseProvider {
|
|||
|
||||
// The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
|
||||
if (CoreSitePlugins.sitePluginsFinishedLoading) {
|
||||
return CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params);
|
||||
return CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions);
|
||||
}
|
||||
|
||||
// Wait for plugins to be loaded.
|
||||
|
@ -1212,7 +1220,7 @@ export class CoreCourseProvider {
|
|||
const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => {
|
||||
observer?.off();
|
||||
|
||||
CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params)
|
||||
CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions)
|
||||
.then(deferred.resolve).catch(deferred.reject);
|
||||
});
|
||||
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
import { CoreNavigationOptions } from '@services/navigator';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreCourseWSSection } from './course';
|
||||
import { CoreCourseSection } from './course-helper';
|
||||
|
@ -66,13 +66,22 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
|
|||
*/
|
||||
displayEnableDownload?(course: CoreCourseAnyCourseData): boolean;
|
||||
|
||||
/**
|
||||
* Whether the default course index should be displayed. Defaults to true.
|
||||
*
|
||||
* @deprecated on 4.0. Please use displayCourseIndex instead.
|
||||
* @param course The course to check.
|
||||
* @return Whether the default course index should be displayed.
|
||||
*/
|
||||
displaySectionSelector?(course: CoreCourseAnyCourseData): boolean;
|
||||
|
||||
/**
|
||||
* Whether the default section selector should be displayed. Defaults to true.
|
||||
*
|
||||
* @param course The course to check.
|
||||
* @return Whether the default section selector should be displayed.
|
||||
*/
|
||||
displaySectionSelector?(course: CoreCourseAnyCourseData): boolean;
|
||||
displayCourseIndex?(course: CoreCourseAnyCourseData): boolean;
|
||||
|
||||
/**
|
||||
* Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format,
|
||||
|
@ -93,6 +102,13 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
|
|||
*/
|
||||
getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection>;
|
||||
|
||||
/**
|
||||
* Returns the name for the highlighted section.
|
||||
*
|
||||
* @return The name for the highlighted section based on the given course format.
|
||||
*/
|
||||
getSectionHightlightedName?(): string;
|
||||
|
||||
/**
|
||||
* Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened.
|
||||
* Implement it only if you want to create your own page to display the course. In general it's better to use the method
|
||||
|
@ -100,10 +116,10 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
|
|||
* Your page should include the course handlers using CoreCoursesDelegate.
|
||||
*
|
||||
* @param course The course to open. It should contain a "format" attribute.
|
||||
* @param params Params to pass to the course page.
|
||||
* @param navOptions Navigation options that includes params to pass to the page.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
openCourse?(course: CoreCourseAnyCourseData, params?: Params): Promise<void>;
|
||||
openCourse?(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the course format instead of using the default one.
|
||||
|
@ -125,15 +141,6 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
|
|||
*/
|
||||
getCourseSummaryComponent?(course: CoreCourseAnyCourseData): Promise<Type<unknown> | undefined>;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the section selector inside the default course format.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @param course The course to render.
|
||||
* @return Promise resolved with component to use, undefined if not found.
|
||||
*/
|
||||
getSectionSelectorComponent?(course: CoreCourseAnyCourseData): Promise<Type<unknown> | undefined>;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display a single section. This component will only be used if the user is viewing a
|
||||
* single section. If all the sections are displayed at once then it won't be used.
|
||||
|
@ -218,12 +225,19 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Whether the default section selector should be displayed. Defaults to true.
|
||||
* Whether the default course index should be displayed. Defaults to true.
|
||||
*
|
||||
* @param course The course to check.
|
||||
* @return Whether the section selector should be displayed.
|
||||
* @return Whether the course index should be displayed.
|
||||
*/
|
||||
displaySectionSelector(course: CoreCourseAnyCourseData): boolean {
|
||||
displayCourseIndex(course: CoreCourseAnyCourseData): boolean {
|
||||
const display = this.executeFunctionOnEnabled<boolean>(course.format || '', 'displayCourseIndex', [course]);
|
||||
|
||||
if (display !== undefined) {
|
||||
return display;
|
||||
}
|
||||
|
||||
// Use displaySectionSelector while is not completely deprecated.
|
||||
return !!this.executeFunctionOnEnabled<boolean>(course.format || '', 'displaySectionSelector', [course]);
|
||||
}
|
||||
|
||||
|
@ -276,8 +290,8 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
|
|||
* @param sections List of sections.
|
||||
* @return Course title.
|
||||
*/
|
||||
getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string | undefined {
|
||||
return this.executeFunctionOnEnabled(course.format || '', 'getCourseTitle', [course, sections]);
|
||||
getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string {
|
||||
return this.executeFunctionOnEnabled(course.format || '', 'getCourseTitle', [course, sections]) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -287,9 +301,9 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
|
|||
* @param sections List of sections.
|
||||
* @return Promise resolved with current section.
|
||||
*/
|
||||
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
|
||||
async getCurrentSection<T = CoreCourseSection>(course: CoreCourseAnyCourseData, sections: T[]): Promise<T> {
|
||||
try {
|
||||
const section = await this.executeFunctionOnEnabled<CoreCourseSection>(
|
||||
const section = await this.executeFunctionOnEnabled<T>(
|
||||
course.format || '',
|
||||
'getCurrentSection',
|
||||
[course, sections],
|
||||
|
@ -303,17 +317,16 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the component to use to display the section selector inside the default course format.
|
||||
* Returns the name for the highlighted section.
|
||||
*
|
||||
* @param course The course to render.
|
||||
* @return Promise resolved with component to use, undefined if not found.
|
||||
* @param course The course to get the text.
|
||||
* @return The name for the highlighted section based on the given course format.
|
||||
*/
|
||||
async getSectionSelectorComponent(course: CoreCourseAnyCourseData): Promise<Type<unknown> | undefined> {
|
||||
try {
|
||||
return await this.executeFunctionOnEnabled<Type<unknown>>(course.format || '', 'getSectionSelectorComponent', [course]);
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting section selector component', error);
|
||||
}
|
||||
getSectionHightlightedName(course: CoreCourseAnyCourseData): string | undefined {
|
||||
return this.executeFunctionOnEnabled<string>(
|
||||
course.format || '',
|
||||
'getSectionHightlightedName',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -346,11 +359,11 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
|
|||
* Open a course. Should not be called directly. Call CoreCourseHelper.openCourse instead.
|
||||
*
|
||||
* @param course The course to open. It should contain a "format" attribute.
|
||||
* @param params Params to pass to the course page.
|
||||
* @param navOptions Navigation options that includes params to pass to the page.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise<void> {
|
||||
await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, params]);
|
||||
async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void> {
|
||||
await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, navOptions]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,12 +13,10 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
|
||||
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourseWSSection } from '../course';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreCourseSection } from '../course-helper';
|
||||
import { CoreCourseFormatHandler } from '../format-delegate';
|
||||
|
||||
|
@ -32,19 +30,14 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
|
|||
format = 'default';
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return Promise resolved with true if enabled.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title to use in course page.
|
||||
*
|
||||
* @param course The course.
|
||||
* @return Title.
|
||||
* @inheritdoc
|
||||
*/
|
||||
getCourseTitle(course: CoreCourseAnyCourseData): string {
|
||||
if (course.displayname) {
|
||||
|
@ -57,57 +50,35 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Whether it allows seeing all sections at the same time. Defaults to true.
|
||||
*
|
||||
* @param course The course to check.
|
||||
* @return Whether it can view all sections.
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
canViewAllSections(course: CoreCourseAnyCourseData): boolean {
|
||||
canViewAllSections(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the option blocks should be displayed. Defaults to true.
|
||||
*
|
||||
* @param course The course to check.
|
||||
* @return Whether it can display blocks.
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
displayBlocks(course: CoreCourseAnyCourseData): boolean {
|
||||
displayBlocks(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the default section selector should be displayed. Defaults to true.
|
||||
*
|
||||
* @param course The course to check.
|
||||
* @return Whether the default section selector should be displayed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
displaySectionSelector(course: CoreCourseAnyCourseData): boolean {
|
||||
displayCourseIndex(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format,
|
||||
* and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true.
|
||||
*
|
||||
* @param course The course to check.
|
||||
* @param sections List of course sections.
|
||||
* @return Whether the refresher should be displayed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean {
|
||||
displayRefresher(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of sections, get the "current" section that should be displayed first.
|
||||
*
|
||||
* @param course The course to get the title.
|
||||
* @param sections List of sections.
|
||||
* @return Current section (or promise resolved with current section).
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
|
||||
let marker: number | undefined;
|
||||
|
@ -137,48 +108,40 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Invalidate the data required to load the course format.
|
||||
*
|
||||
* @param course The course to get the title.
|
||||
* @param sections List of sections.
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async invalidateData(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): Promise<void> {
|
||||
getSectionHightlightedName(): string {
|
||||
return Translate.instant('core.course.highlighted');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async invalidateData(course: CoreCourseAnyCourseData): Promise<void> {
|
||||
await CoreCourses.invalidateCoursesByField('id', course.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened.
|
||||
* Implement it only if you want to create your own page to display the course. In general it's better to use the method
|
||||
* getCourseFormatComponent because it will display the course handlers at the top.
|
||||
* Your page should include the course handlers using CoreCoursesDelegate.
|
||||
*
|
||||
* @param course The course to open. It should contain a "format" attribute.
|
||||
* @param params Params to pass to the course page.
|
||||
* @return Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise<void> {
|
||||
params = params || {};
|
||||
Object.assign(params, { course: course });
|
||||
async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void> {
|
||||
navOptions = navOptions || {};
|
||||
|
||||
navOptions.params = navOptions.params || {};
|
||||
Object.assign(navOptions.params, { course: course });
|
||||
|
||||
// Don't return the .push promise, we don't want to display a loading modal during the page transition.
|
||||
const currentTab = CoreNavigator.getCurrentMainMenuTab();
|
||||
const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`);
|
||||
const deepPath = '/deep'.repeat(routeDepth);
|
||||
|
||||
CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, { params });
|
||||
CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, navOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the view should be refreshed when completion changes. If your course format doesn't display
|
||||
* activity completion then you should return false.
|
||||
*
|
||||
* @param course The course.
|
||||
* @return Whether course view should be refreshed when an activity completion changes.
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise<boolean> {
|
||||
async shouldRefreshWhenCompletionChanges(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -171,7 +171,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
|
|||
CoreCourseHelper.openCourse(this.course);
|
||||
} else {
|
||||
CoreNavigator.navigateToSitePath(
|
||||
'/course/' + this.course.id + '/preview',
|
||||
`/course/${this.course.id}/preview`,
|
||||
{ params: { course: this.course } },
|
||||
);
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler
|
|||
modal.dismiss();
|
||||
|
||||
// Now open the course.
|
||||
CoreCourseHelper.openCourse(course, pageParams);
|
||||
CoreCourseHelper.openCourse(course, { params: pageParams });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -38,8 +38,9 @@ export class CoreSitePluginsCourseFormatHandler extends CoreSitePluginsBaseHandl
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
displaySectionSelector(): boolean {
|
||||
return this.handlerSchema.displaysectionselector ?? true;
|
||||
displayCourseIndex(): boolean {
|
||||
// Use displaysectionselector while is not completely deprecated.
|
||||
return this.handlerSchema.displaycourseindex ?? this.handlerSchema.displaysectionselector ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Component, OnChanges, Input, ViewChild, Output, EventEmitter } from '@a
|
|||
import { IonRefresher } from '@ionic/angular';
|
||||
|
||||
import { CoreCourseFormatComponent } from '@features/course/components/format/format';
|
||||
import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper';
|
||||
import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins';
|
||||
|
@ -33,7 +33,7 @@ import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-
|
|||
export class CoreSitePluginsCourseFormatComponent implements OnChanges {
|
||||
|
||||
@Input() course?: CoreCourseAnyCourseData; // The course to render.
|
||||
@Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections. The status will be calculated in this component.
|
||||
@Input() sections?: CoreCourseSection[]; // List of course sections. The status will be calculated in this component.
|
||||
@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).
|
||||
|
|
|
@ -884,7 +884,11 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo
|
|||
export type CoreSitePluginsCourseFormatHandlerData = CoreSitePluginsHandlerCommonData & {
|
||||
canviewallsections?: boolean;
|
||||
displayenabledownload?: boolean;
|
||||
/**
|
||||
* @deprecated on 4.0, use displaycourseindex instead.
|
||||
*/
|
||||
displaysectionselector?: boolean;
|
||||
displaycourseindex?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -301,6 +301,7 @@
|
|||
"strftimetime24": "%H:%M",
|
||||
"submit": "Submit",
|
||||
"success": "Success",
|
||||
"summary": "Summary",
|
||||
"tablet": "Tablet",
|
||||
"teachers": "Teachers",
|
||||
"thereisdatatosync": "There are offline {{$a}} to be synchronised.",
|
||||
|
|
|
@ -1118,7 +1118,7 @@ export class CoreDomUtilsProvider {
|
|||
content.scrollToPoint(position[0], position[1], duration || 0);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool';
|
|||
import { CoreNavigationOptions } from '@services/navigator';
|
||||
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
import { CoreScreenOrientation } from '@services/screen';
|
||||
import { CoreCourseCompletionType } from '@features/course/services/course';
|
||||
|
||||
/**
|
||||
* Observer instance to stop listening to an event.
|
||||
|
@ -48,6 +49,7 @@ export interface CoreEventsData {
|
|||
[CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData;
|
||||
[CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData;
|
||||
[CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData;
|
||||
[CoreEvents.COMPLETION_CHANGED]: CoreEventCompletionChangedData;
|
||||
[CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData;
|
||||
[CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData;
|
||||
[CoreEvents.IAB_LOAD_START]: InAppBrowserEvent;
|
||||
|
@ -77,7 +79,11 @@ export class CoreEvents {
|
|||
static readonly SITE_UPDATED = 'site_updated';
|
||||
static readonly SITE_DELETED = 'site_deleted';
|
||||
static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed';
|
||||
/**
|
||||
* Deprecated on 4.0 use COMPLETION_CHANGED instead.
|
||||
*/
|
||||
static readonly MANUAL_COMPLETION_CHANGED = 'manual_completion_changed';
|
||||
static readonly COMPLETION_CHANGED = 'completion_changed';
|
||||
static readonly USER_DELETED = 'user_deleted';
|
||||
static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed';
|
||||
static readonly COURSE_STATUS_CHANGED = 'course_status_changed';
|
||||
|
@ -347,6 +353,14 @@ export type CoreEventManualCompletionChangedData = {
|
|||
completion: CoreCourseModuleCompletionData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data passed to COMPLETION_CHANGED event.
|
||||
*/
|
||||
export type CoreEventCompletionChangedData = {
|
||||
completion: CoreCourseModuleCompletionData;
|
||||
type: CoreCourseCompletionType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data passed to SECTION_STATUS_CHANGED event.
|
||||
*/
|
||||
|
|
|
@ -382,17 +382,21 @@ ion-alert.core-nohead {
|
|||
}
|
||||
}
|
||||
|
||||
ion-alert .alert-wrapper {
|
||||
overflow: auto;
|
||||
}
|
||||
ion-alert {
|
||||
--border-radius: var(--huge-radius);
|
||||
.alert-wrapper {
|
||||
overflow: auto;
|
||||
border-radius: var(--border-radius) !important;
|
||||
|
||||
ion-alert .alert-message {
|
||||
user-select: text;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
button.alert-button {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
ion-alert .alert-wrapper button.alert-button {
|
||||
color: var(--primary);
|
||||
.alert-message {
|
||||
user-select: text;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Ionic list.
|
||||
|
@ -844,6 +848,8 @@ ion-select-popover ion-item.core-select-option-title {
|
|||
|
||||
ion-chip {
|
||||
line-height: 1.1;
|
||||
border-radius: var(--medium-radius);
|
||||
@include padding-horizontal(16px);
|
||||
}
|
||||
|
||||
ion-searchbar {
|
||||
|
@ -1167,9 +1173,8 @@ ion-item.item-input ion-input.has-focus {
|
|||
}
|
||||
}
|
||||
|
||||
// Ionic set this value to 0 without px that provoked miscalculations.
|
||||
ion-item-divider {
|
||||
--inner-padding-end: 0px;
|
||||
--inner-padding-end: 8px;
|
||||
}
|
||||
|
||||
// Change default outline.
|
||||
|
@ -1225,6 +1230,13 @@ ion-grid.core-no-grid > ion-row {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.core-underheader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
ion-header[collapsible] {
|
||||
@include core-transition(all, 500ms);
|
||||
|
||||
|
@ -1247,7 +1259,14 @@ ion-header[collapsible] {
|
|||
|
||||
.collapsible-title {
|
||||
overflow: visible;
|
||||
*, h1, h2, .subheading {
|
||||
--inner-padding-top: 0px;
|
||||
--padding-top: 0px;
|
||||
|
||||
ion-label {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
*, h1 {
|
||||
@include core-transition(all, 200ms, linear);
|
||||
}
|
||||
|
||||
|
@ -1255,9 +1274,12 @@ ion-header[collapsible] {
|
|||
overflow: visible !important;
|
||||
}
|
||||
|
||||
h1, h2, .subheading {
|
||||
h1 {
|
||||
--max-width: none;
|
||||
}
|
||||
h1.cloned {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
ion-app.ios .collapsible-title h1 {
|
||||
|
@ -1269,7 +1291,7 @@ ion-app.md .collapsible-title h1 {
|
|||
}
|
||||
|
||||
.collapsible-title.collapsible-title-collapsed {
|
||||
ion-label, h1, h2, ion-row, ion-col, .subheading {
|
||||
ion-label, h1, ion-row, ion-col {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
@ -1279,13 +1301,13 @@ ion-app.md .collapsible-title h1 {
|
|||
opacity: var(--collapse-opacity, 0);
|
||||
}
|
||||
|
||||
ion-label, h1, h2, ion-row, ion-col, .subheading {
|
||||
ion-label, h1, ion-row, ion-col, .subheading {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible-title.collapsible-title-collapse-nowrap {
|
||||
h1, h2, .subheading {
|
||||
h1:not(.cloned) {
|
||||
max-width: var(--max-width);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -105,7 +105,9 @@
|
|||
--subdued-text-color: #595959;
|
||||
|
||||
--small-radius: 4px;
|
||||
--big-radius: 8px;
|
||||
--medium-radius: 8px;
|
||||
--big-radius: 16px;
|
||||
--huge-radius: 24px;
|
||||
|
||||
--ion-card-color: var(--text-color);
|
||||
ion-card {
|
||||
|
@ -113,7 +115,7 @@
|
|||
--border-style: solid;
|
||||
--border-color: var(--stroke);
|
||||
--box-shadow: none;
|
||||
--border-radius: var(--big-radius);
|
||||
--border-radius: var(--medium-radius);
|
||||
}
|
||||
|
||||
--text-hightlight-background-color: #{$core-text-hightlight-background-color};
|
||||
|
@ -181,7 +183,7 @@
|
|||
height: var(--a11y-min-target-size);
|
||||
border: 1px solid var(--stroke);
|
||||
box-shadow: none;
|
||||
border-radius: var(--big-radius);
|
||||
border-radius: var(--medium-radius);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,7 +284,7 @@
|
|||
--core-combobox-color: var(--gray-900);
|
||||
--core-combobox-border-color: var(--stroke);
|
||||
--core-combobox-border-width: 1px;
|
||||
--core-combobox-radius: var(--big-radius);
|
||||
--core-combobox-radius: var(--medium-radius);
|
||||
--core-combobox-box-shadow: none;
|
||||
|
||||
--selected-item-color: var(--primary);
|
||||
|
@ -301,7 +303,7 @@
|
|||
--core-send-message-input-background: var(--gray-200);
|
||||
--core-send-message-input-color: var(--gray-900);
|
||||
|
||||
--core-courseimage-on-course-height: 150px;
|
||||
--core-courseimage-on-course-size: 72px;
|
||||
|
||||
--core-course-module-navigation-max-height: 56px;
|
||||
--core-course-module-navigation-background: var(--contrast-background);
|
||||
|
|
|
@ -15,6 +15,8 @@ information provided here is intended especially for developers.
|
|||
The function CoreUserDelegate.getProfileHandlersFor must now receive a context + contextId instead of a courseId.
|
||||
The user handler function isEnabledForCourse is now called isEnabledForContext and receives a context + contextId instead of a courseId.
|
||||
Some user handler's functions have also changed to accept context + contextId instead of a courseId: isEnabledForUser, getDisplayData, action.
|
||||
- CoreCourseHelperProvider.openCourse parameters changed, now it admits CoreNavigationOptions + siteId on the same object that includes Params passed to page.
|
||||
- displaySectionSelector has been deprecated on CoreCourseFormatHandler, use displayCourseIndex instead.
|
||||
|
||||
=== 3.9.5 ===
|
||||
|
||||
|
|
Loading…
Reference in New Issue