forked from EVOgeek/Vmeda.Online
		
	MOBILE-3810 core: Collapsible headers
This commit is contained in:
		
							parent
							
								
									57b5266198
								
							
						
					
					
						commit
						d8718c5eaa
					
				| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
| @ -17,7 +17,6 @@ | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
| 
 | ||||
|     <addon-mod-book-index [module]="module" [courseId]="courseId" [initialChapterId]="chapterId" | ||||
|         (dataRetrieved)="updateData($event)"> | ||||
|     <addon-mod-book-index [module]="module" [courseId]="courseId" [initialChapterId]="chapterId" (dataRetrieved)="updateData($event)"> | ||||
|     </addon-mod-book-index> | ||||
| </ion-content> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -26,9 +26,8 @@ | ||||
| <core-loading [hideUntil]="loaded"> | ||||
| 
 | ||||
|     <!-- 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"> | ||||
|         <h3 *ngIf="subfolder" title>{{subfolder.filename}}</h3> | ||||
|     </core-course-module-info> | ||||
| 
 | ||||
|     <ion-list *ngIf="contents && (contents.files.length + contents.folders.length > 0)"> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
| @ -17,7 +17,6 @@ | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
| 
 | ||||
|     <addon-mod-lesson-index [module]="module" [courseId]="courseId" [group]="group" [action]="action" | ||||
|         (dataRetrieved)="updateData($event)"> | ||||
|     <addon-mod-lesson-index [module]="module" [courseId]="courseId" [group]="group" [action]="action" (dataRetrieved)="updateData($event)"> | ||||
|     </addon-mod-lesson-index> | ||||
| </ion-content> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
| @ -14,8 +14,7 @@ | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-refresher slot="fixed" | ||||
|         [disabled]="!activityComponent?.loaded || activityComponent?.mode == 'iframe'" | ||||
|     <ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded || activityComponent?.mode == 'iframe'" | ||||
|         (ionRefresh)="activityComponent?.doRefresh($event.target)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -135,6 +135,8 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|         } | ||||
| 
 | ||||
|         if (!this.wiki) { | ||||
|             CoreNavigator.back(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| @ -143,7 +145,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|                 await AddonModWiki.logView(this.wiki.id, this.wiki.name); | ||||
| 
 | ||||
|                 CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); | ||||
|             } catch (error) { | ||||
|             } catch { | ||||
|                 // Ignore errors.
 | ||||
|             } | ||||
|         } else { | ||||
| @ -210,7 +212,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|     /** | ||||
|      * @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 { | ||||
|             // Get the wiki instance.
 | ||||
|             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.
 | ||||
|                 this.dataRetrieved.emit(this.wiki); | ||||
|             } | ||||
| 
 | ||||
|             AddonModWiki.wikiPageOpened(this.wiki.id, this.currentPath); | ||||
| 
 | ||||
|             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.
 | ||||
|         try { | ||||
|             const title = this.pageTitle || this.wiki!.firstpagetitle!; | ||||
|             const title = this.pageTitle || this.wiki?.firstpagetitle || ''; | ||||
| 
 | ||||
|             const offlinePage = await AddonModWikiOffline.getNewPage( | ||||
|                 title, | ||||
|                 this.currentSubwiki!.id, | ||||
|                 this.currentSubwiki!.wikiid, | ||||
|                 this.currentSubwiki!.userid, | ||||
|                 this.currentSubwiki!.groupid, | ||||
|                 this.currentSubwiki?.id, | ||||
|                 this.currentSubwiki?.wikiid, | ||||
|                 this.currentSubwiki?.userid, | ||||
|                 this.currentSubwiki?.groupid, | ||||
|             ); | ||||
| 
 | ||||
|             this.pageIsOffline = true; | ||||
| @ -321,7 +324,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|                     this.currentPage = data.pageId; | ||||
| 
 | ||||
|                     // Stop listening for new page events.
 | ||||
|                     this.newPageObserver!.off(); | ||||
|                     this.newPageObserver?.off(); | ||||
|                     this.newPageObserver = undefined; | ||||
| 
 | ||||
|                     await this.showLoadingAndFetch(true, false); | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
| @ -7,6 +7,10 @@ | ||||
|             <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </h1> | ||||
|         <p> | ||||
|             <core-format-text [text]="pageTitle" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </p> | ||||
| 
 | ||||
|         <ion-buttons slot="end"> | ||||
|             <!-- 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.userId = CoreNavigator.getRouteNumberParam('userId'); | ||||
|         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 { | ||||
|         if (typeof data == 'string') { | ||||
|             // We received the title to display.
 | ||||
|             this.title = data; | ||||
|             this.pageTitle = data; | ||||
|         } else { | ||||
|             // We received a wiki instance.
 | ||||
|             this.title = this.pageTitle || data.name || this.title; | ||||
|             super.updateData(data); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -26,7 +26,7 @@ import { | ||||
|     SimpleChange, | ||||
| } from '@angular/core'; | ||||
| import { IonSlides } from '@ionic/angular'; | ||||
| import { BackButtonEvent } from '@ionic/core'; | ||||
| import { BackButtonEvent, ScrollDetail } from '@ionic/core'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| import { Platform, Translate } from '@singletons'; | ||||
| @ -625,8 +625,8 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft | ||||
| 
 | ||||
|         content.scrollEvents = true; | ||||
|         this.scrollElements[id] = scroll; | ||||
|         content.addEventListener('ionScroll', (e: CustomEvent): void => { | ||||
|             this.showHideTabs(parseInt(e.detail.scrollTop, 10), scroll); | ||||
|         content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => { | ||||
|             this.showHideTabs(e.detail.scrollTop, scroll); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -15,14 +15,13 @@ | ||||
|     left: 0; | ||||
|     display: flex; | ||||
|     position: absolute; | ||||
|     inset: 0; | ||||
|     flex-direction: row; | ||||
|     flex-wrap: nowrap; | ||||
|     contain: strict; | ||||
| 
 | ||||
|     .menu, | ||||
|     .content-outlet { | ||||
|         top: 0; | ||||
|         top: var(--offset-top); | ||||
|         right: 0; | ||||
|         bottom: 0; | ||||
|         left: 0; | ||||
|  | ||||
							
								
								
									
										257
									
								
								src/core/directives/collapsible-header.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								src/core/directives/collapsible-header.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 { CoreOnResizeDirective } from './on-resize'; | ||||
| import { CoreDownloadFileDirective } from './download-file'; | ||||
| import { CoreCollapsibleHeaderDirective } from './collapsible-header'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
| @ -43,6 +44,7 @@ import { CoreDownloadFileDirective } from './download-file'; | ||||
|         CoreAriaButtonClickDirective, | ||||
|         CoreOnResizeDirective, | ||||
|         CoreDownloadFileDirective, | ||||
|         CoreCollapsibleHeaderDirective, | ||||
|     ], | ||||
|     exports: [ | ||||
|         CoreAutoFocusDirective, | ||||
| @ -58,6 +60,7 @@ import { CoreDownloadFileDirective } from './download-file'; | ||||
|         CoreAriaButtonClickDirective, | ||||
|         CoreOnResizeDirective, | ||||
|         CoreDownloadFileDirective, | ||||
|         CoreCollapsibleHeaderDirective, | ||||
|     ], | ||||
| }) | ||||
| 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> | ||||
|     <ion-label> | ||||
|         <h2> | ||||
|         <h1> | ||||
|             <core-format-text [text]="module.name" contextLevel="module" [component]="component" [componentId]="componentId" | ||||
|                 [contextInstanceId]="module.id" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </h2> | ||||
|         </h1> | ||||
|         <ng-content select="[title]"></ng-content> | ||||
|     </ion-label> | ||||
| </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> | ||||
| <core-course-module-info [description]="module?.description" [courseId]="courseId" [module]="module"> | ||||
| </core-course-module-info> | ||||
| 
 | ||||
| <div class="ion-padding"> | ||||
|     <h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2> | ||||
|     <h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2> | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|  | ||||
| @ -23,12 +23,12 @@ | ||||
|     // border: var(--a11y-focus-width) solid var(--a11y-focus-color); | ||||
| } | ||||
| 
 | ||||
| @mixin core-transition($where: all, $time: 500ms) { | ||||
|     -webkit-transition: $where $time ease-in-out; | ||||
|     -moz-transition: $where $time ease-in-out; | ||||
|     -ms-transition: $where $time ease-in-out; | ||||
|     -o-transition: $where $time ease-in-out; | ||||
|     transition: $where $time ease-in-out; | ||||
| @mixin core-transition($property: all, $duration: 500ms, $timing-function: ease-in-out) { | ||||
|     -webkit-transition: $property $duration $timing-function; | ||||
|     -moz-transition: $property $duration $timing-function; | ||||
|     -ms-transition: $property $duration $timing-function; | ||||
|     -o-transition: $property $duration $timing-function; | ||||
|     transition: $property $duration $timing-function; | ||||
| } | ||||
| 
 | ||||
| @mixin push-arrow-color($color: dedede, $flip-rtl: false) { | ||||
|  | ||||
| @ -119,45 +119,50 @@ body { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Some styles taken from ion-title | ||||
| ion-header h1, | ||||
| ion-header h2 { | ||||
|     display: block; | ||||
|     transform: translateZ(0); | ||||
|     --color: initial; | ||||
|     color: var(--color); | ||||
|     margin: 0; | ||||
|     width: 100%; | ||||
| ion-header ion-title{ | ||||
|     h1, h2, .subheading { | ||||
|         text-overflow: ellipsis; | ||||
|         white-space: nowrap; | ||||
|         overflow: hidden; | ||||
|     pointer-events: auto; | ||||
|         margin: 0; | ||||
|     } | ||||
| 
 | ||||
|     .filter_mathjaxloader_equation div { | ||||
|         display: inline !important; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ion-app.md ion-header h1, | ||||
| ion-app.md ion-header h2 { | ||||
| ion-app.md ion-header ion-title{ | ||||
|     @include padding(0, 20px); | ||||
| 
 | ||||
|     h1, h2, .subheading { | ||||
|         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 h2 { | ||||
|     @include position(0, null, null, 0); | ||||
|     @include padding(0, 90px, 0); | ||||
| ion-app.ios ion-header ion-title { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: center; | ||||
| 
 | ||||
|     position: absolute; | ||||
|     text-align: center; | ||||
|     h1, h2, .subheading { | ||||
|         font-size: 17px; | ||||
|         font-weight: 600; | ||||
|     line-height: var(--core-header-toolbar-height); | ||||
|     box-sizing: border-box; | ||||
|     pointer-events: none; | ||||
|     } | ||||
| 
 | ||||
|     h1 + h2, | ||||
|     h1 + .subheading { | ||||
|         font-size: 14px; | ||||
|         font-weight: 400; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @ -901,9 +906,13 @@ ion-back-button.md::part(text) { | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| // Hide close button because when present is read on voice over. | ||||
| ion-fab[core-fab] ion-fab-button::part(close-icon) { | ||||
| ion-fab[core-fab] { | ||||
|     position: fixed; | ||||
|      | ||||
|     // Hide close button because when present is read on voice over. | ||||
|     ion-fab-button::part(close-icon) { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .core-media-adapt-width { | ||||
| @ -1167,3 +1176,66 @@ iframe { | ||||
| ion-grid.core-no-grid > ion-row { | ||||
|     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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user