MOBILE-3810 core: Collapsible headers
parent
57b5266198
commit
d8718c5eaa
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -17,7 +17,6 @@
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
|
|
||||||
<addon-mod-book-index [module]="module" [courseId]="courseId" [initialChapterId]="chapterId"
|
<addon-mod-book-index [module]="module" [courseId]="courseId" [initialChapterId]="chapterId" (dataRetrieved)="updateData($event)">
|
||||||
(dataRetrieved)="updateData($event)">
|
|
||||||
</addon-mod-book-index>
|
</addon-mod-book-index>
|
||||||
</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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -26,9 +26,8 @@
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
|
|
||||||
<!-- Activity info. -->
|
<!-- Activity info. -->
|
||||||
<core-course-module-info [module]="module" (completionChanged)="onCompletionChange()" [description]="description"
|
<core-course-module-info *ngIf="!subfolder" [module]="module" (completionChanged)="onCompletionChange()" [description]="description"
|
||||||
[component]="component" [componentId]="componentId" [courseId]="courseId">
|
[component]="component" [componentId]="componentId" [courseId]="courseId">
|
||||||
<h3 *ngIf="subfolder" title>{{subfolder.filename}}</h3>
|
|
||||||
</core-course-module-info>
|
</core-course-module-info>
|
||||||
|
|
||||||
<ion-list *ngIf="contents && (contents.files.length + contents.folders.length > 0)">
|
<ion-list *ngIf="contents && (contents.files.length + contents.folders.length > 0)">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -17,7 +17,6 @@
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
|
|
||||||
<addon-mod-lesson-index [module]="module" [courseId]="courseId" [group]="group" [action]="action"
|
<addon-mod-lesson-index [module]="module" [courseId]="courseId" [group]="group" [action]="action" (dataRetrieved)="updateData($event)">
|
||||||
(dataRetrieved)="updateData($event)">
|
|
||||||
</addon-mod-lesson-index>
|
</addon-mod-lesson-index>
|
||||||
</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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -14,8 +14,7 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-refresher slot="fixed"
|
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded || activityComponent?.mode == 'iframe'"
|
||||||
[disabled]="!activityComponent?.loaded || activityComponent?.mode == 'iframe'"
|
|
||||||
(ionRefresh)="activityComponent?.doRefresh($event.target)">
|
(ionRefresh)="activityComponent?.doRefresh($event.target)">
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -135,6 +135,8 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.wiki) {
|
if (!this.wiki) {
|
||||||
|
CoreNavigator.back();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +145,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
await AddonModWiki.logView(this.wiki.id, this.wiki.name);
|
await AddonModWiki.logView(this.wiki.id, this.wiki.name);
|
||||||
|
|
||||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -210,7 +212,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Get the wiki instance.
|
// Get the wiki instance.
|
||||||
this.wiki = await AddonModWiki.getWiki(this.courseId, this.module.id);
|
this.wiki = await AddonModWiki.getWiki(this.courseId, this.module.id);
|
||||||
|
@ -219,6 +221,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
// Page not loaded yet, emit the data to update the page title.
|
// Page not loaded yet, emit the data to update the page title.
|
||||||
this.dataRetrieved.emit(this.wiki);
|
this.dataRetrieved.emit(this.wiki);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddonModWiki.wikiPageOpened(this.wiki.id, this.currentPath);
|
AddonModWiki.wikiPageOpened(this.wiki.id, this.currentPath);
|
||||||
|
|
||||||
if (sync) {
|
if (sync) {
|
||||||
|
@ -299,14 +302,14 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
|
|
||||||
// No page ID but we received a title. This means we're trying to load an offline page.
|
// No page ID but we received a title. This means we're trying to load an offline page.
|
||||||
try {
|
try {
|
||||||
const title = this.pageTitle || this.wiki!.firstpagetitle!;
|
const title = this.pageTitle || this.wiki?.firstpagetitle || '';
|
||||||
|
|
||||||
const offlinePage = await AddonModWikiOffline.getNewPage(
|
const offlinePage = await AddonModWikiOffline.getNewPage(
|
||||||
title,
|
title,
|
||||||
this.currentSubwiki!.id,
|
this.currentSubwiki?.id,
|
||||||
this.currentSubwiki!.wikiid,
|
this.currentSubwiki?.wikiid,
|
||||||
this.currentSubwiki!.userid,
|
this.currentSubwiki?.userid,
|
||||||
this.currentSubwiki!.groupid,
|
this.currentSubwiki?.groupid,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.pageIsOffline = true;
|
this.pageIsOffline = true;
|
||||||
|
@ -321,7 +324,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
this.currentPage = data.pageId;
|
this.currentPage = data.pageId;
|
||||||
|
|
||||||
// Stop listening for new page events.
|
// Stop listening for new page events.
|
||||||
this.newPageObserver!.off();
|
this.newPageObserver?.off();
|
||||||
this.newPageObserver = undefined;
|
this.newPageObserver = undefined;
|
||||||
|
|
||||||
await this.showLoadingAndFetch(true, false);
|
await this.showLoadingAndFetch(true, false);
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -7,6 +7,10 @@
|
||||||
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
|
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</h1>
|
</h1>
|
||||||
|
<p>
|
||||||
|
<core-format-text [text]="pageTitle" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<!-- The buttons defined by the component will be added in here. -->
|
<!-- The buttons defined by the component will be added in here. -->
|
||||||
|
|
|
@ -47,8 +47,6 @@ export class AddonModWikiIndexPage extends CoreCourseModuleMainActivityPage<Addo
|
||||||
this.subwikiId = CoreNavigator.getRouteNumberParam('subwikiId');
|
this.subwikiId = CoreNavigator.getRouteNumberParam('subwikiId');
|
||||||
this.userId = CoreNavigator.getRouteNumberParam('userId');
|
this.userId = CoreNavigator.getRouteNumberParam('userId');
|
||||||
this.groupId = CoreNavigator.getRouteNumberParam('groupId');
|
this.groupId = CoreNavigator.getRouteNumberParam('groupId');
|
||||||
|
|
||||||
this.title = this.pageTitle || this.module.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,10 +55,9 @@ export class AddonModWikiIndexPage extends CoreCourseModuleMainActivityPage<Addo
|
||||||
updateData(data: { name: string } | string): void {
|
updateData(data: { name: string } | string): void {
|
||||||
if (typeof data == 'string') {
|
if (typeof data == 'string') {
|
||||||
// We received the title to display.
|
// We received the title to display.
|
||||||
this.title = data;
|
this.pageTitle = data;
|
||||||
} else {
|
} else {
|
||||||
// We received a wiki instance.
|
super.updateData(data);
|
||||||
this.title = this.pageTitle || data.name || this.title;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
SimpleChange,
|
SimpleChange,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { IonSlides } from '@ionic/angular';
|
import { IonSlides } from '@ionic/angular';
|
||||||
import { BackButtonEvent } from '@ionic/core';
|
import { BackButtonEvent, ScrollDetail } from '@ionic/core';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { Platform, Translate } from '@singletons';
|
import { Platform, Translate } from '@singletons';
|
||||||
|
@ -625,8 +625,8 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
|
||||||
|
|
||||||
content.scrollEvents = true;
|
content.scrollEvents = true;
|
||||||
this.scrollElements[id] = scroll;
|
this.scrollElements[id] = scroll;
|
||||||
content.addEventListener('ionScroll', (e: CustomEvent): void => {
|
content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => {
|
||||||
this.showHideTabs(parseInt(e.detail.scrollTop, 10), scroll);
|
this.showHideTabs(e.detail.scrollTop, scroll);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,13 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
contain: strict;
|
contain: strict;
|
||||||
|
|
||||||
.menu,
|
.menu,
|
||||||
.content-outlet {
|
.content-outlet {
|
||||||
top: 0;
|
top: var(--offset-top);
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
// (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 { Directive, ElementRef, OnDestroy } from '@angular/core';
|
||||||
|
import { ScrollDetail } from '@ionic/core';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { Platform } from '@singletons';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreMath } from '@singletons/math';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive to make ion-header collapsible.
|
||||||
|
* Ion content should have h1 tag inside.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* <ion-header collapsible>
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: 'ion-header[collapsible]',
|
||||||
|
})
|
||||||
|
export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
||||||
|
|
||||||
|
protected scrollElement?: HTMLElement;
|
||||||
|
protected loadingObserver: CoreEventObserver;
|
||||||
|
protected content?: HTMLIonContentElement | null;
|
||||||
|
protected header: HTMLIonHeaderElement;
|
||||||
|
protected titleTopDifference = 1;
|
||||||
|
protected h1StartDifference = 0;
|
||||||
|
protected headerH1FontSize = 0;
|
||||||
|
protected contentH1FontSize = 0;
|
||||||
|
protected headerSubHeadingFontSize = 0;
|
||||||
|
protected contentSubHeadingFontSize = 0;
|
||||||
|
protected subHeadingStartDifference = 0;
|
||||||
|
|
||||||
|
constructor(el: ElementRef) {
|
||||||
|
this.header = el.nativeElement;
|
||||||
|
|
||||||
|
this.loadingObserver = CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data) => {
|
||||||
|
const loadingId = await this.getLoadingId();
|
||||||
|
if (loadingId && data.loaded && data.uniqueId == loadingId) {
|
||||||
|
// Remove event when loading is done.
|
||||||
|
this.loadingObserver.off();
|
||||||
|
|
||||||
|
// Wait to render.
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
this.setupRealTitle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
||||||
|
if (!this.content) {
|
||||||
|
this.content = this.header.parentElement?.querySelector('ion-content:not(.disable-scroll-y)');
|
||||||
|
|
||||||
|
if (!this.content) {
|
||||||
|
this.cannotCollapse();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.content.querySelector('core-loading .core-loading-content')?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function when header is not collapsible.
|
||||||
|
*/
|
||||||
|
protected cannotCollapse(): void {
|
||||||
|
this.loadingObserver.off();
|
||||||
|
this.header.classList.add('core-header-collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the real title on ion content to watch scroll.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async setupRealTitle(): Promise<void> {
|
||||||
|
|
||||||
|
if (!this.content) {
|
||||||
|
this.cannotCollapse();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = this.content.querySelector<HTMLElement>('.collapsible-title, h1');
|
||||||
|
const contentH1 = this.content.querySelector<HTMLElement>('h1');
|
||||||
|
const headerH1 = this.header.querySelector<HTMLElement>('h1');
|
||||||
|
if (!title || !contentH1 || !headerH1) {
|
||||||
|
this.cannotCollapse();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.titleTopDifference = contentH1.getBoundingClientRect().top - headerH1.getBoundingClientRect().top;
|
||||||
|
|
||||||
|
// Split view part.
|
||||||
|
const contentAux = this.header.parentElement?.querySelector<HTMLElement>('ion-content.disable-scroll-y');
|
||||||
|
if (contentAux) {
|
||||||
|
if (contentAux.querySelector('core-split-view.menu-and-content')) {
|
||||||
|
this.cannotCollapse();
|
||||||
|
title.remove();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contentAux.style.setProperty('--offset-top', this.header.clientHeight + 'px');
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerH1Styles = getComputedStyle(headerH1);
|
||||||
|
const contentH1Styles = getComputedStyle(contentH1);
|
||||||
|
|
||||||
|
if (Platform.isRTL) {
|
||||||
|
this.h1StartDifference = contentH1.getBoundingClientRect().right -
|
||||||
|
(headerH1.getBoundingClientRect().right - parseFloat(headerH1Styles.paddingRight));
|
||||||
|
} else {
|
||||||
|
this.h1StartDifference = contentH1.getBoundingClientRect().left -
|
||||||
|
(headerH1.getBoundingClientRect().left + parseFloat(headerH1Styles.paddingLeft));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.headerH1FontSize = parseFloat(headerH1Styles.fontSize);
|
||||||
|
this.contentH1FontSize = parseFloat(contentH1Styles.fontSize);
|
||||||
|
|
||||||
|
// Transfer font styles.
|
||||||
|
Array.from(headerH1Styles).forEach((styleName) => {
|
||||||
|
if (styleName != 'font-size' && (styleName.startsWith('font-') || styleName.startsWith('letter-'))) {
|
||||||
|
contentH1.style.setProperty(styleName, headerH1Styles.getPropertyValue(styleName));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
contentH1.style.setProperty(
|
||||||
|
'--max-width',
|
||||||
|
(parseFloat(headerH1Styles.width)
|
||||||
|
-parseFloat(headerH1Styles.paddingLeft)
|
||||||
|
-parseFloat(headerH1Styles.paddingRight)
|
||||||
|
+'px'),
|
||||||
|
);
|
||||||
|
|
||||||
|
contentH1.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
// Add something under the hood to change the page background.
|
||||||
|
let color = getComputedStyle(title).getPropertyValue('backgroundColor').trim();
|
||||||
|
if (color == '') {
|
||||||
|
color = getComputedStyle(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));
|
||||||
|
}
|
||||||
|
|
||||||
|
contentSubHeading.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.content.scrollEvents = true;
|
||||||
|
this.content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => {
|
||||||
|
this.onScroll(title, contentH1, contentSubHeading, e.detail);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On scroll function.
|
||||||
|
*
|
||||||
|
* @param title Title on ion content.
|
||||||
|
* @param contentH1 Heading 1 of title, if found.
|
||||||
|
* @param scrollDetail Event details.
|
||||||
|
*/
|
||||||
|
protected onScroll(
|
||||||
|
title: HTMLElement,
|
||||||
|
contentH1: HTMLElement,
|
||||||
|
contentSubheading: HTMLElement | null,
|
||||||
|
scrollDetail: ScrollDetail,
|
||||||
|
): void {
|
||||||
|
const progress = CoreMath.clamp(scrollDetail.scrollTop / this.titleTopDifference, 0, 1);
|
||||||
|
const collapsed = progress >= 1;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom font-size out.
|
||||||
|
const newFontSize = this.contentH1FontSize - ((this.contentH1FontSize - this.headerH1FontSize) * progress);
|
||||||
|
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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.loadingObserver.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import { CoreUserLinkDirective } from './user-link';
|
||||||
import { CoreAriaButtonClickDirective } from './aria-button';
|
import { CoreAriaButtonClickDirective } from './aria-button';
|
||||||
import { CoreOnResizeDirective } from './on-resize';
|
import { CoreOnResizeDirective } from './on-resize';
|
||||||
import { CoreDownloadFileDirective } from './download-file';
|
import { CoreDownloadFileDirective } from './download-file';
|
||||||
|
import { CoreCollapsibleHeaderDirective } from './collapsible-header';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -43,6 +44,7 @@ import { CoreDownloadFileDirective } from './download-file';
|
||||||
CoreAriaButtonClickDirective,
|
CoreAriaButtonClickDirective,
|
||||||
CoreOnResizeDirective,
|
CoreOnResizeDirective,
|
||||||
CoreDownloadFileDirective,
|
CoreDownloadFileDirective,
|
||||||
|
CoreCollapsibleHeaderDirective,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CoreAutoFocusDirective,
|
CoreAutoFocusDirective,
|
||||||
|
@ -58,6 +60,7 @@ import { CoreDownloadFileDirective } from './download-file';
|
||||||
CoreAriaButtonClickDirective,
|
CoreAriaButtonClickDirective,
|
||||||
CoreOnResizeDirective,
|
CoreOnResizeDirective,
|
||||||
CoreDownloadFileDirective,
|
CoreDownloadFileDirective,
|
||||||
|
CoreCollapsibleHeaderDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreDirectivesModule {}
|
export class CoreDirectivesModule {}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<ion-item class="ion-text-wrap" lines="none">
|
<ion-item class="ion-text-wrap collapsible-title" lines="none">
|
||||||
<core-mod-icon slot="start" [modicon]="modicon" [modname]="module.modname" [componentId]="module.instance">
|
<core-mod-icon slot="start" [modicon]="modicon" [modname]="module.modname" [componentId]="module.instance">
|
||||||
</core-mod-icon>
|
</core-mod-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>
|
<h1>
|
||||||
<core-format-text [text]="module.name" contextLevel="module" [component]="component" [componentId]="componentId"
|
<core-format-text [text]="module.name" contextLevel="module" [component]="component" [componentId]="componentId"
|
||||||
[contextInstanceId]="module.id" [courseId]="courseId">
|
[contextInstanceId]="module.id" [courseId]="courseId">
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</h2>
|
</h1>
|
||||||
<ng-content select="[title]"></ng-content>
|
<ng-content select="[title]"></ng-content>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="ion-padding">
|
<core-course-module-info [description]="module?.description" [courseId]="courseId" [module]="module">
|
||||||
<core-course-module-info [description]="module?.description" [courseId]="courseId" [module]="module">
|
</core-course-module-info>
|
||||||
</core-course-module-info>
|
|
||||||
|
|
||||||
|
<div class="ion-padding">
|
||||||
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
|
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
|
||||||
<h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2>
|
<h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -23,12 +23,12 @@
|
||||||
// border: var(--a11y-focus-width) solid var(--a11y-focus-color);
|
// border: var(--a11y-focus-width) solid var(--a11y-focus-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin core-transition($where: all, $time: 500ms) {
|
@mixin core-transition($property: all, $duration: 500ms, $timing-function: ease-in-out) {
|
||||||
-webkit-transition: $where $time ease-in-out;
|
-webkit-transition: $property $duration $timing-function;
|
||||||
-moz-transition: $where $time ease-in-out;
|
-moz-transition: $property $duration $timing-function;
|
||||||
-ms-transition: $where $time ease-in-out;
|
-ms-transition: $property $duration $timing-function;
|
||||||
-o-transition: $where $time ease-in-out;
|
-o-transition: $property $duration $timing-function;
|
||||||
transition: $where $time ease-in-out;
|
transition: $property $duration $timing-function;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin push-arrow-color($color: dedede, $flip-rtl: false) {
|
@mixin push-arrow-color($color: dedede, $flip-rtl: false) {
|
||||||
|
|
|
@ -119,45 +119,50 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some styles taken from ion-title
|
ion-header ion-title{
|
||||||
ion-header h1,
|
h1, h2, .subheading {
|
||||||
ion-header h2 {
|
text-overflow: ellipsis;
|
||||||
display: block;
|
white-space: nowrap;
|
||||||
transform: translateZ(0);
|
overflow: hidden;
|
||||||
--color: initial;
|
margin: 0;
|
||||||
color: var(--color);
|
}
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: auto;
|
|
||||||
|
|
||||||
.filter_mathjaxloader_equation div {
|
.filter_mathjaxloader_equation div {
|
||||||
display: inline !important;
|
display: inline !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-app.md ion-header h1,
|
ion-app.md ion-header ion-title{
|
||||||
ion-app.md ion-header h2 {
|
|
||||||
@include padding(0, 20px);
|
@include padding(0, 20px);
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
h1, h2, .subheading {
|
||||||
letter-spacing: .0125em;
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: .0125em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 + h2,
|
||||||
|
h1 + .subheading {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-app.ios ion-header h1,
|
ion-app.ios ion-header ion-title {
|
||||||
ion-app.ios ion-header h2 {
|
display: flex;
|
||||||
@include position(0, null, null, 0);
|
flex-direction: column;
|
||||||
@include padding(0, 90px, 0);
|
justify-content: center;
|
||||||
|
|
||||||
position: absolute;
|
h1, h2, .subheading {
|
||||||
text-align: center;
|
font-size: 17px;
|
||||||
font-size: 17px;
|
font-weight: 600;
|
||||||
font-weight: 600;
|
}
|
||||||
line-height: var(--core-header-toolbar-height);
|
|
||||||
box-sizing: border-box;
|
h1 + h2,
|
||||||
pointer-events: none;
|
h1 + .subheading {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -901,9 +906,13 @@ ion-back-button.md::part(text) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide close button because when present is read on voice over.
|
ion-fab[core-fab] {
|
||||||
ion-fab[core-fab] ion-fab-button::part(close-icon) {
|
position: fixed;
|
||||||
display: none;
|
|
||||||
|
// Hide close button because when present is read on voice over.
|
||||||
|
ion-fab-button::part(close-icon) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-media-adapt-width {
|
.core-media-adapt-width {
|
||||||
|
@ -1166,4 +1175,67 @@ iframe {
|
||||||
|
|
||||||
ion-grid.core-no-grid > ion-row {
|
ion-grid.core-no-grid > ion-row {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-header[collapsible] {
|
||||||
|
@include core-transition(all, 500ms);
|
||||||
|
|
||||||
|
ion-title {
|
||||||
|
@include core-transition(opacity, 0ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.core-header-collapsed) {
|
||||||
|
ion-toolbar {
|
||||||
|
--core-header-toolbar-background: rgba(255, 255, 255, 0);
|
||||||
|
--core-header-toolbar-border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-title, &::after {
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-title {
|
||||||
|
overflow: visible;
|
||||||
|
*, h1, h2, .subheading {
|
||||||
|
@include core-transition(all, 200ms, linear);
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-label {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, .subheading {
|
||||||
|
--max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-app.ios .collapsible-title h1 {
|
||||||
|
font-weight: 600; // Default heading weight.
|
||||||
|
}
|
||||||
|
ion-app.md .collapsible-title h1 {
|
||||||
|
font-weight: 500; // Default heading weight.
|
||||||
|
letter-spacing: .0125em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-title.collapsible-title-collapsed {
|
||||||
|
ion-label, h1, h2, .subheading {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-title.collapsible-title-collapse-started {
|
||||||
|
* {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
ion-label, h1, h2, .subheading {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
h1, h2, .subheading {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue