MOBILE-3915 course: Move Course summary info outside the tabs
parent
86365d260d
commit
901a445408
|
@ -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() layout: 'icon-top' | 'icon-start' | 'icon-end' | 'icon-bottom' | 'icon-hide' | 'label-hide' = 'icon-hide';
|
||||||
@Input() tabs: CoreTabsOutletTab[] = [];
|
@Input() tabs: CoreTabsOutletTab[] = [];
|
||||||
|
|
||||||
@ViewChild(IonTabs) protected ionTabs?: IonTabs;
|
@ViewChild(IonTabs) protected ionTabs!: IonTabs;
|
||||||
|
|
||||||
protected stackEventsSubscription?: Subscription;
|
protected stackEventsSubscription?: Subscription;
|
||||||
protected outletActivatedSubscription?: Subscription;
|
protected outletActivatedSubscription?: Subscription;
|
||||||
|
@ -96,11 +96,17 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tabsElement = this.element.nativeElement.querySelector('ion-tabs');
|
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) {
|
if (!this.isCurrentView) {
|
||||||
return;
|
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);
|
this.showHideNavBarButtons(stackEvent.enteringView.element.tagName);
|
||||||
|
|
||||||
await this.listenContentScroll(stackEvent.enteringView.element, stackEvent.enteringView.id);
|
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.showHideTabs(scrollElement.scrollTop, scrollElement);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.outletActivatedSubscription = this.ionTabs?.outlet.activateEvents.subscribe(() => {
|
this.outletActivatedSubscription = this.ionTabs.outlet.activateEvents.subscribe(() => {
|
||||||
this.lastActiveComponent = this.ionTabs?.outlet.component;
|
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,
|
// 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
|
// 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).
|
// entering in response to a back navigation from the page on top).
|
||||||
if (this.existsInNavigationStack && this.ionTabs?.outlet.isActivated) {
|
if (this.existsInNavigationStack && this.ionTabs.outlet.isActivated) {
|
||||||
(this.ionTabs?.outlet.component as Partial<ViewDidEnter>).ionViewDidEnter?.();
|
(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
|
// 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.
|
* @param activatedPageName Activated page name.
|
||||||
*/
|
*/
|
||||||
protected showHideNavBarButtons(activatedPageName: string): void {
|
protected showHideNavBarButtons(activatedPageName: string): void {
|
||||||
const elements = this.ionTabs!.outlet.nativeEl.querySelectorAll('core-navbar-buttons');
|
const elements = this.ionTabs.outlet.nativeEl.querySelectorAll('core-navbar-buttons');
|
||||||
const domUtils = CoreDomUtils.instance;
|
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
const instance = domUtils.getInstanceByElement<CoreNavBarButtonsComponent>(element);
|
const instance = CoreDomUtils.getInstanceByElement<CoreNavBarButtonsComponent>(element);
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
const pagetagName = element.closest('.ion-page')?.tagName;
|
const pagetagName = element.closest('.ion-page')?.tagName;
|
||||||
|
|
|
@ -43,6 +43,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
||||||
protected headerSubHeadingFontSize = 0;
|
protected headerSubHeadingFontSize = 0;
|
||||||
protected contentSubHeadingFontSize = 0;
|
protected contentSubHeadingFontSize = 0;
|
||||||
protected subHeadingStartDifference = 0;
|
protected subHeadingStartDifference = 0;
|
||||||
|
protected inContent = true;
|
||||||
|
protected title?: HTMLElement | null;
|
||||||
|
protected titleHeight = 0;
|
||||||
|
protected contentH1?: HTMLElement | null;
|
||||||
|
|
||||||
constructor(el: ElementRef<HTMLIonHeaderElement>) {
|
constructor(el: ElementRef<HTMLIonHeaderElement>) {
|
||||||
this.header = el.nativeElement;
|
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.
|
* 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.
|
* @return Promise resolved with Loading Id, if any.
|
||||||
*/
|
*/
|
||||||
protected async getLoadingId(): Promise<string | undefined> {
|
protected async getLoadingId(): Promise<string | undefined> {
|
||||||
|
@ -80,6 +82,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
||||||
|
|
||||||
return;
|
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;
|
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.
|
* Call this function when header is not collapsible.
|
||||||
*/
|
*/
|
||||||
protected cannotCollapse(): void {
|
protected cannotCollapse(): void {
|
||||||
|
this.content = undefined;
|
||||||
this.loadingObserver.off();
|
this.loadingObserver.off();
|
||||||
this.header.classList.add('core-header-collapsed');
|
this.header.classList.add('core-header-collapsed');
|
||||||
}
|
}
|
||||||
|
@ -112,16 +126,25 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
||||||
await animation.finished;
|
await animation.finished;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const title = this.content.querySelector<HTMLElement>('.collapsible-title, h1');
|
let title = this.content.querySelector<HTMLElement>('.collapsible-title');
|
||||||
const contentH1 = this.content.querySelector<HTMLElement>('h1');
|
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');
|
const headerH1 = this.header.querySelector<HTMLElement>('h1');
|
||||||
if (!title || !contentH1 || !headerH1) {
|
if (!title || !this.contentH1 || !headerH1 || !this.contentH1.parentElement) {
|
||||||
this.cannotCollapse();
|
this.cannotCollapse();
|
||||||
|
|
||||||
return;
|
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) {
|
if (this.titleTopDifference <= 0) {
|
||||||
this.cannotCollapse();
|
this.cannotCollapse();
|
||||||
|
|
||||||
|
@ -141,17 +164,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerH1Styles = getComputedStyle(headerH1);
|
const headerH1Styles = getComputedStyle(headerH1);
|
||||||
const contentH1Styles = getComputedStyle(contentH1);
|
const contentH1Styles = getComputedStyle(this.contentH1);
|
||||||
|
|
||||||
if (Platform.isRTL) {
|
if (Platform.isRTL) {
|
||||||
// Checking position over parent because transition may not be finished.
|
// 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;
|
const headerH1Position = headerH1.getBoundingClientRect().right - this.header.getBoundingClientRect().right;
|
||||||
|
|
||||||
this.h1StartDifference = Math.round(contentH1Position - (headerH1Position - parseFloat(headerH1Styles.paddingRight)));
|
this.h1StartDifference = Math.round(contentH1Position - (headerH1Position - parseFloat(headerH1Styles.paddingRight)));
|
||||||
} else {
|
} else {
|
||||||
// Checking position over parent because transition may not be finished.
|
// 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;
|
const headerH1Position = headerH1.getBoundingClientRect().left - this.header.getBoundingClientRect().left;
|
||||||
|
|
||||||
this.h1StartDifference = Math.round(contentH1Position - (headerH1Position + parseFloat(headerH1Styles.paddingLeft)));
|
this.h1StartDifference = Math.round(contentH1Position - (headerH1Position + parseFloat(headerH1Styles.paddingLeft)));
|
||||||
|
@ -165,10 +188,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
||||||
if (styleName != 'font-size' &&
|
if (styleName != 'font-size' &&
|
||||||
styleName != 'font-family' &&
|
styleName != 'font-family' &&
|
||||||
(styleName.startsWith('font-') || styleName.startsWith('letter-'))) {
|
(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',
|
'--max-width',
|
||||||
(parseFloat(headerH1Styles.width)
|
(parseFloat(headerH1Styles.width)
|
||||||
-parseFloat(headerH1Styles.paddingLeft)
|
-parseFloat(headerH1Styles.paddingLeft)
|
||||||
|
@ -176,47 +199,70 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
||||||
+'px'),
|
+'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.
|
// 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 == '') {
|
if (color == '') {
|
||||||
color = getComputedStyle(title).getPropertyValue('--background').trim();
|
color = getComputedStyle(this.title).getPropertyValue('--background').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const underHeader = document.createElement('div');
|
const underHeader = document.createElement('div');
|
||||||
underHeader.classList.add('core-underheader');
|
underHeader.classList.add('core-underheader');
|
||||||
underHeader.style.setProperty('height', this.header.clientHeight + 'px');
|
underHeader.style.setProperty('height', this.header.clientHeight + 'px');
|
||||||
underHeader.style.setProperty('background', color);
|
underHeader.style.setProperty('background', color);
|
||||||
|
if (this.inContent) {
|
||||||
this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader);
|
this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader);
|
||||||
|
|
||||||
this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px');
|
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 {
|
} else {
|
||||||
this.subHeadingStartDifference = contentSubHeading.getBoundingClientRect().left -
|
if (!this.header.closest('.ion-page')?.querySelector('.core-underheader')) {
|
||||||
(headerSubHeading.getBoundingClientRect().left + parseFloat(headerSubHeadingStyles.paddingLeft));
|
this.header.closest('.ion-page')?.insertBefore(underHeader, this.header);
|
||||||
}
|
}
|
||||||
|
|
||||||
contentSubHeading.setAttribute('aria-hidden', 'true');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.content.scrollEvents = true;
|
this.content.scrollEvents = true;
|
||||||
this.content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => {
|
this.content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => {
|
||||||
if (e.target == this.content) {
|
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.
|
* On scroll function.
|
||||||
*
|
*
|
||||||
* @param title Title on ion content.
|
* @param scrollTop Scroll top measure.
|
||||||
* @param contentH1 Heading 1 of title, if found.
|
|
||||||
* @param scrollDetail Event details.
|
|
||||||
*/
|
*/
|
||||||
protected onScroll(
|
protected onScroll(
|
||||||
title: HTMLElement,
|
scrollTop: number,
|
||||||
contentH1: HTMLElement,
|
|
||||||
contentSubheading: HTMLElement | null,
|
|
||||||
scrollDetail: ScrollDetail,
|
|
||||||
): void {
|
): 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;
|
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.
|
// Check total collapse.
|
||||||
this.header.classList.toggle('core-header-collapsed', collapsed);
|
this.header.classList.toggle('core-header-collapsed', collapsed);
|
||||||
title.classList.toggle('collapsible-title-collapsed', collapsed);
|
this.title.classList.toggle('collapsible-title-collapsed', collapsed);
|
||||||
title.classList.toggle('collapsible-title-collapse-started', scrollDetail.scrollTop > 0);
|
this.title.classList.toggle('collapsible-title-collapse-started', scrollTop > 0);
|
||||||
title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5);
|
this.title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5);
|
||||||
title.style.setProperty('--collapse-opacity', (1 - progress) +'');
|
this.title.style.setProperty('--collapse-opacity', (1 - progress) +'');
|
||||||
|
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)';
|
this.contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)';
|
||||||
contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px');
|
this.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');
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zoom font-size out.
|
// Zoom font-size out.
|
||||||
const newFontSize = this.contentH1FontSize - ((this.contentH1FontSize - this.headerH1FontSize) * progress);
|
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.
|
// Move.
|
||||||
const newStart = - this.h1StartDifference * progress;
|
const newStart = - this.h1StartDifference * progress;
|
||||||
contentH1.style.transform = 'translateX(' + newStart + 'px)';
|
this.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)';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
@include position(50%, 0px, null, null);
|
@include position(50%, 0px, null, null);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
ion-button {
|
ion-button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -10,30 +10,6 @@
|
||||||
<!-- Default course format. -->
|
<!-- Default course format. -->
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
|
|
||||||
<!-- Course summary. By default we only display the course progress. -->
|
|
||||||
<core-dynamic-component [component]="courseSummaryComponent" [data]="data">
|
|
||||||
<ion-item lines="full" class="core-format-progress-list ion-text-wrap">
|
|
||||||
<ion-avatar slot="start" class="core-course-thumb" *ngIf="imageThumb">
|
|
||||||
<img [src]="imageThumb" core-external-content alt="" />
|
|
||||||
</ion-avatar>
|
|
||||||
<ion-label>
|
|
||||||
<p *ngIf="course.categoryname">
|
|
||||||
<core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid">
|
|
||||||
</core-format-text>
|
|
||||||
</p>
|
|
||||||
<p class="item-heading">{{ course.displayname || course.fullname }}</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-button fill="clear" slot="end" (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-item>
|
|
||||||
</core-dynamic-component>
|
|
||||||
|
|
||||||
<!-- Single section. -->
|
<!-- Single section. -->
|
||||||
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
|
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
|
||||||
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
|
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
|
||||||
|
@ -72,15 +48,15 @@
|
||||||
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
|
|
||||||
<core-block-side-blocks-button *ngIf="course && displayBlocks && hasBlocks" [courseId]="course.id">
|
|
||||||
</core-block-side-blocks-button>
|
|
||||||
|
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</core-dynamic-component>
|
</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. -->
|
<!-- Course Index button. -->
|
||||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="displayCourseIndex">
|
<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-fab-button (click)="openCourseIndex()" [attr.aria-label]="'core.course.courseindex' | translate">
|
||||||
<ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon>
|
||||||
<span class="sr-only">{{'core.course.courseindex' | translate }}</span>
|
<span class="sr-only">{{'core.course.courseindex' | translate }}</span>
|
||||||
|
@ -121,8 +97,7 @@
|
||||||
|
|
||||||
<ng-container *ngFor="let module of section.modules">
|
<ng-container *ngFor="let module of section.modules">
|
||||||
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
|
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
|
||||||
(completionChanged)="onCompletionChange($event)" [showActivityDates]="course.showactivitydates"
|
[showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions">
|
||||||
[showCompletionConditions]="course.showcompletionconditions">
|
|
||||||
</core-course-module>
|
</core-course-module>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,45 +1,3 @@
|
||||||
@import '~theme/globals.scss';
|
|
||||||
|
|
||||||
:host {
|
|
||||||
|
|
||||||
.core-format-progress-list {
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
.item {
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-course-thumb {
|
|
||||||
height: var(--core-courseimage-on-course-size);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.core-button-selector-row {
|
|
||||||
display: flex;
|
|
||||||
core-combobox {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-course-section-nav-buttons {
|
.core-course-section-nav-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@ -51,5 +9,3 @@
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called.
|
static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called.
|
||||||
|
|
||||||
@Input() course!: CoreCourseAnyCourseData; // The course to render.
|
@Input() course!: CoreCourseAnyCourseData; // The course to render.
|
||||||
@Input() sections?: CoreCourseSection[]; // List of course sections.
|
@Input() sections: CoreCourseSectionWithStatus[] = []; // List of course sections.
|
||||||
@Input() initialSectionId?: number; // The section to load first (by ID).
|
@Input() initialSectionId?: number; // The section to load first (by ID).
|
||||||
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
@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.
|
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
||||||
|
@ -95,10 +95,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
|
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
|
||||||
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||||
loaded = false;
|
loaded = false;
|
||||||
imageThumb?: string;
|
|
||||||
progress?: number;
|
progress?: number;
|
||||||
|
|
||||||
protected selectTabObserver?: CoreEventObserver;
|
protected selectTabObserver?: CoreEventObserver;
|
||||||
|
protected completionObserver?: CoreEventObserver;
|
||||||
protected lastCourseFormat?: string;
|
protected lastCourseFormat?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -139,6 +139,37 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
this.sectionChanged(section);
|
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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,10 +188,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
|
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
|
||||||
|
|
||||||
this.updateProgress();
|
this.updateProgress();
|
||||||
|
|
||||||
if ('overviewfiles' in this.course) {
|
|
||||||
this.imageThumb = this.course.overviewfiles?.[0]?.fileurl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changes.sections && this.sections) {
|
if (changes.sections && this.sections) {
|
||||||
|
@ -246,8 +273,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
|
const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
|
||||||
|
|
||||||
if (this.selectedSection) {
|
if (this.selectedSection) {
|
||||||
|
const selectedSection = this.selectedSection;
|
||||||
// We have a selected section, but the list has changed. Search the section in the list.
|
// 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) {
|
if (!newSection) {
|
||||||
// Section not found, calculate which one to use.
|
// Section not found, calculate which one to use.
|
||||||
|
@ -317,22 +345,22 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
|
||||||
if (newSection.id != this.allSectionsId) {
|
if (newSection.id != this.allSectionsId) {
|
||||||
// Select next and previous sections to show the arrows.
|
// 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;
|
let j: number;
|
||||||
for (j = i - 1; j >= 1; j--) {
|
for (j = i - 1; j >= 1; j--) {
|
||||||
if (this.canViewSection(this.sections![j])) {
|
if (this.canViewSection(this.sections[j])) {
|
||||||
break;
|
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++) {
|
for (j = i + 1; j < this.sections.length; j++) {
|
||||||
if (this.canViewSection(this.sections![j])) {
|
if (this.canViewSection(this.sections[j])) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.nextSection = j < this.sections!.length ? this.sections![j] : undefined;
|
this.nextSection = j < this.sections.length ? this.sections[j] : undefined;
|
||||||
} else {
|
} else {
|
||||||
this.previousSection = undefined;
|
this.previousSection = undefined;
|
||||||
this.nextSection = undefined;
|
this.nextSection = undefined;
|
||||||
|
@ -463,7 +491,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component destroyed.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.selectTabObserver && this.selectTabObserver.off();
|
this.selectTabObserver && this.selectTabObserver.off();
|
||||||
|
@ -498,35 +526,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
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.
|
* Update course progress.
|
||||||
*/
|
*/
|
||||||
|
@ -546,14 +545,4 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
this.progress = this.course.progress;
|
this.progress = this.course.progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the course summary
|
|
||||||
*/
|
|
||||||
openCourseSummary(): void {
|
|
||||||
CoreNavigator.navigateToSitePath(
|
|
||||||
'/course/' + this.course.id + '/preview',
|
|
||||||
{ params: { course: this.course, avoidOpenCourse: true } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,19 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { CoreUser } from '@features/user/services/user';
|
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 { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion';
|
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion';
|
||||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
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.
|
* 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',
|
templateUrl: 'core-course-module-completion-legacy.html',
|
||||||
styleUrls: ['module-completion-legacy.scss'],
|
styleUrls: ['module-completion-legacy.scss'],
|
||||||
})
|
})
|
||||||
export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent {
|
export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent
|
||||||
|
implements OnInit, OnDestroy {
|
||||||
|
|
||||||
completionImage?: string;
|
completionImage?: string;
|
||||||
completionDescription?: 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
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -126,9 +149,16 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC
|
||||||
|
|
||||||
await CoreCourseHelper.changeManualCompletion(this.completion, event);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core';
|
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 { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||||
import { CoreUser } from '@features/user/services/user';
|
import { CoreUser } from '@features/user/services/user';
|
||||||
|
@ -34,14 +35,14 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
|
||||||
|
|
||||||
accessibleDescription: string | null = null;
|
accessibleDescription: string | null = null;
|
||||||
|
|
||||||
protected manualChangedObserver?: CoreEventObserver;
|
protected completionObserver?: CoreEventObserver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => {
|
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => {
|
||||||
if (!this.completion || this.completion.cmid != data.completion.cmid) {
|
if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,14 +99,16 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
|
||||||
|
|
||||||
await CoreCourseHelper.changeManualCompletion(this.completion, event);
|
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.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
|
||||||
|
CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.manualChangedObserver?.off();
|
this.completionObserver?.off();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
|
|
||||||
<core-loading [hideUntil]="dataLoaded">
|
<core-loading [hideUntil]="dataLoaded">
|
||||||
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber"
|
<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-course-format>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<ion-header>
|
<ion-header collapsible>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-buttons slot="start">
|
<ion-buttons slot="start">
|
||||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||||
|
@ -12,4 +12,31 @@
|
||||||
<ion-buttons slot="end"></ion-buttons>
|
<ion-buttons slot="end"></ion-buttons>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</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 { resolveModuleRoutes } from '@/app/app-routing.module';
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { CoreCourseIndexPage } from './index.page';
|
import { CoreCourseIndexPage } from '.';
|
||||||
import { COURSE_INDEX_ROUTES } from './index-routing.module';
|
import { COURSE_INDEX_ROUTES } from './index-routing.module';
|
||||||
|
|
||||||
function buildRoutes(injector: Injector): Routes {
|
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 { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CONTENTS_PAGE_NAME } from '@features/course/course.module';
|
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.
|
* 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({
|
@Component({
|
||||||
selector: 'page-core-course-index',
|
selector: 'page-core-course-index',
|
||||||
templateUrl: 'index.html',
|
templateUrl: 'index.html',
|
||||||
|
styleUrls: ['index.scss'],
|
||||||
})
|
})
|
||||||
export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent;
|
@ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent;
|
||||||
|
@ViewChild(CoreCollapsibleHeaderDirective) ionCollapsibleHeader?: CoreCollapsibleHeaderDirective;
|
||||||
|
|
||||||
title?: string;
|
title = '';
|
||||||
|
category = '';
|
||||||
course?: CoreCourseAnyCourseData;
|
course?: CoreCourseAnyCourseData;
|
||||||
tabs: CourseTab[] = [];
|
tabs: CourseTab[] = [];
|
||||||
loaded = false;
|
loaded = false;
|
||||||
|
imageThumb?: string;
|
||||||
|
progress?: number;
|
||||||
|
|
||||||
protected currentPagePath = '';
|
protected currentPagePath = '';
|
||||||
protected selectTabObserver: CoreEventObserver;
|
protected selectTabObserver: CoreEventObserver;
|
||||||
protected firstTabName?: string;
|
protected firstTabName?: string;
|
||||||
protected module?: CoreCourseModuleData;
|
protected module?: CoreCourseModuleData;
|
||||||
protected modParams?: Params;
|
protected modParams?: Params;
|
||||||
protected isGuest?: boolean;
|
protected isGuest = false;
|
||||||
protected contentsTab: CoreTabsOutletTab = {
|
protected contentsTab: CoreTabsOutletTab & { pageParams: Params } = {
|
||||||
page: CONTENTS_PAGE_NAME,
|
page: CONTENTS_PAGE_NAME,
|
||||||
title: 'core.course',
|
title: 'core.course',
|
||||||
pageParams: {},
|
pageParams: {},
|
||||||
|
@ -60,10 +67,10 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
if (!data.name) {
|
if (!data.name) {
|
||||||
// If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
|
// If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
|
||||||
if (data.sectionId) {
|
if (data.sectionId) {
|
||||||
this.contentsTab.pageParams!.sectionId = data.sectionId;
|
this.contentsTab.pageParams.sectionId = data.sectionId;
|
||||||
}
|
}
|
||||||
if (data.sectionNumber) {
|
if (data.sectionNumber) {
|
||||||
this.contentsTab.pageParams!.sectionNumber = data.sectionNumber;
|
this.contentsTab.pageParams.sectionNumber = data.sectionNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select course contents.
|
// Select course contents.
|
||||||
|
@ -79,7 +86,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
// Increase route depth.
|
// Increase route depth.
|
||||||
|
@ -87,12 +94,19 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, ''));
|
CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, ''));
|
||||||
|
|
||||||
// Get params.
|
try {
|
||||||
this.course = CoreNavigator.getRouteParam('course');
|
this.course = CoreNavigator.getRequiredRouteParam('course');
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
CoreNavigator.back();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.firstTabName = CoreNavigator.getRouteParam('selectedTab');
|
this.firstTabName = CoreNavigator.getRouteParam('selectedTab');
|
||||||
this.module = CoreNavigator.getRouteParam<CoreCourseModuleData>('module');
|
this.module = CoreNavigator.getRouteParam<CoreCourseModuleData>('module');
|
||||||
this.modParams = CoreNavigator.getRouteParam<Params>('modParams');
|
this.modParams = CoreNavigator.getRouteParam<Params>('modParams');
|
||||||
this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest');
|
this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
|
||||||
|
|
||||||
this.currentPagePath = CoreNavigator.getCurrentPath();
|
this.currentPagePath = CoreNavigator.getCurrentPath();
|
||||||
this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page);
|
this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page);
|
||||||
|
@ -104,7 +118,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.module) {
|
if (this.module) {
|
||||||
this.contentsTab.pageParams!.moduleId = this.module.id;
|
this.contentsTab.pageParams.moduleId = this.module.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tabs.push(this.contentsTab);
|
this.tabs.push(this.contentsTab);
|
||||||
|
@ -112,21 +126,24 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadCourseHandlers(),
|
this.loadCourseHandlers(),
|
||||||
this.loadTitle(),
|
this.loadBasinInfo(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tab was selected.
|
* A tab was selected.
|
||||||
*/
|
*/
|
||||||
tabSelected(): void {
|
tabSelected(tabToSelect: CoreTabsOutletTab): void {
|
||||||
if (this.module) {
|
this.ionCollapsibleHeader?.setupContent(tabToSelect.id);
|
||||||
|
|
||||||
|
if (!this.module || !this.course) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Now that the first tab has been selected we can load the 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);
|
CoreCourseHelper.openModule(this.module, this.course.id, this.contentsTab.pageParams.sectionId, this.modParams);
|
||||||
|
|
||||||
delete this.module;
|
delete this.module;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load course option handlers.
|
* Load course option handlers.
|
||||||
|
@ -134,8 +151,12 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async loadCourseHandlers(): Promise<void> {
|
protected async loadCourseHandlers(): Promise<void> {
|
||||||
|
if (!this.course) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Load the course handlers.
|
// 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;
|
let tabToLoad: number | undefined;
|
||||||
|
|
||||||
|
@ -169,23 +190,34 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
*
|
*
|
||||||
* @return Promise resolved when done.
|
* @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.
|
// 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.
|
// 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) {
|
if (!sections) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the title again now that we have sections.
|
// 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 {
|
ngOnDestroy(): void {
|
||||||
const path = CoreNavigator.getRouteFullPath(this.route.snapshot);
|
const path = CoreNavigator.getRouteFullPath(this.route.snapshot);
|
||||||
|
@ -208,6 +240,39 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
this.tabsComponent?.ionViewDidLeave();
|
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 & {
|
type CourseTab = CoreTabsOutletTab & {
|
|
@ -71,6 +71,11 @@ export enum CoreCourseModuleCompletionStatus {
|
||||||
COMPLETION_COMPLETE_FAIL = 3,
|
COMPLETION_COMPLETE_FAIL = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CoreCourseCompletionType {
|
||||||
|
MANUAL = 0,
|
||||||
|
AUTO = 1,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completion tracking valid values.
|
* Completion tracking valid values.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -267,8 +267,8 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
|
||||||
* @param sections List of sections.
|
* @param sections List of sections.
|
||||||
* @return Course title.
|
* @return Course title.
|
||||||
*/
|
*/
|
||||||
getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string | undefined {
|
getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string {
|
||||||
return this.executeFunctionOnEnabled(course.format || '', 'getCourseTitle', [course, sections]);
|
return this.executeFunctionOnEnabled(course.format || '', 'getCourseTitle', [course, sections]) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool';
|
||||||
import { CoreNavigationOptions } from '@services/navigator';
|
import { CoreNavigationOptions } from '@services/navigator';
|
||||||
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||||
import { CoreScreenOrientation } from '@services/screen';
|
import { CoreScreenOrientation } from '@services/screen';
|
||||||
|
import { CoreCourseCompletionType } from '@features/course/services/course';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observer instance to stop listening to an event.
|
* Observer instance to stop listening to an event.
|
||||||
|
@ -48,6 +49,7 @@ export interface CoreEventsData {
|
||||||
[CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData;
|
[CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData;
|
||||||
[CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData;
|
[CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData;
|
||||||
[CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData;
|
[CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData;
|
||||||
|
[CoreEvents.COMPLETION_CHANGED]: CoreEventCompletionChangedData;
|
||||||
[CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData;
|
[CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData;
|
||||||
[CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData;
|
[CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData;
|
||||||
[CoreEvents.IAB_LOAD_START]: InAppBrowserEvent;
|
[CoreEvents.IAB_LOAD_START]: InAppBrowserEvent;
|
||||||
|
@ -77,7 +79,11 @@ export class CoreEvents {
|
||||||
static readonly SITE_UPDATED = 'site_updated';
|
static readonly SITE_UPDATED = 'site_updated';
|
||||||
static readonly SITE_DELETED = 'site_deleted';
|
static readonly SITE_DELETED = 'site_deleted';
|
||||||
static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed';
|
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 MANUAL_COMPLETION_CHANGED = 'manual_completion_changed';
|
||||||
|
static readonly COMPLETION_CHANGED = 'completion_changed';
|
||||||
static readonly USER_DELETED = 'user_deleted';
|
static readonly USER_DELETED = 'user_deleted';
|
||||||
static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed';
|
static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed';
|
||||||
static readonly COURSE_STATUS_CHANGED = 'course_status_changed';
|
static readonly COURSE_STATUS_CHANGED = 'course_status_changed';
|
||||||
|
@ -347,6 +353,14 @@ export type CoreEventManualCompletionChangedData = {
|
||||||
completion: CoreCourseModuleCompletionData;
|
completion: CoreCourseModuleCompletionData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data passed to COMPLETION_CHANGED event.
|
||||||
|
*/
|
||||||
|
export type CoreEventCompletionChangedData = {
|
||||||
|
completion: CoreCourseModuleCompletionData;
|
||||||
|
type: CoreCourseCompletionType;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data passed to SECTION_STATUS_CHANGED event.
|
* Data passed to SECTION_STATUS_CHANGED event.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1225,6 +1225,13 @@ ion-grid.core-no-grid > ion-row {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.core-underheader {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
ion-header[collapsible] {
|
ion-header[collapsible] {
|
||||||
@include core-transition(all, 500ms);
|
@include core-transition(all, 500ms);
|
||||||
|
|
||||||
|
@ -1247,7 +1254,14 @@ ion-header[collapsible] {
|
||||||
|
|
||||||
.collapsible-title {
|
.collapsible-title {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
*, h1, h2, .subheading {
|
--inner-padding-top: 0px;
|
||||||
|
--padding-top: 0px;
|
||||||
|
|
||||||
|
ion-label {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, h1 {
|
||||||
@include core-transition(all, 200ms, linear);
|
@include core-transition(all, 200ms, linear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1255,9 +1269,12 @@ ion-header[collapsible] {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, .subheading {
|
h1 {
|
||||||
--max-width: none;
|
--max-width: none;
|
||||||
}
|
}
|
||||||
|
h1.cloned {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-app.ios .collapsible-title h1 {
|
ion-app.ios .collapsible-title h1 {
|
||||||
|
@ -1269,7 +1286,7 @@ ion-app.md .collapsible-title h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-title.collapsible-title-collapsed {
|
.collapsible-title.collapsible-title-collapsed {
|
||||||
ion-label, h1, h2, ion-row, ion-col, .subheading {
|
ion-label, h1, ion-row, ion-col {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1279,13 +1296,13 @@ ion-app.md .collapsible-title h1 {
|
||||||
opacity: var(--collapse-opacity, 0);
|
opacity: var(--collapse-opacity, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-label, h1, h2, ion-row, ion-col, .subheading {
|
ion-label, h1, ion-row, ion-col, .subheading {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-title.collapsible-title-collapse-nowrap {
|
.collapsible-title.collapsible-title-collapse-nowrap {
|
||||||
h1, h2, .subheading {
|
h1:not(.cloned) {
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
Loading…
Reference in New Issue