diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 27ea76157..dfb7b2b51 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -13,18 +13,24 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; import { CoreIconComponent } from './icon/icon'; +import { CoreLoadingComponent } from './loading/loading'; import { CoreShowPasswordComponent } from './show-password/show-password'; @NgModule({ declarations: [ CoreIconComponent, + CoreLoadingComponent, CoreShowPasswordComponent, ], - imports: [], + imports: [ + IonicModule, + ], exports: [ CoreIconComponent, + CoreLoadingComponent, CoreShowPasswordComponent, ], }) diff --git a/src/app/components/loading/core-loading.html b/src/app/components/loading/core-loading.html new file mode 100644 index 000000000..da4887db0 --- /dev/null +++ b/src/app/components/loading/core-loading.html @@ -0,0 +1,10 @@ +
+ + +

{{message}}

+
+
+
+ + +
\ No newline at end of file diff --git a/src/app/components/loading/loading.scss b/src/app/components/loading/loading.scss new file mode 100644 index 000000000..afe335a58 --- /dev/null +++ b/src/app/components/loading/loading.scss @@ -0,0 +1,67 @@ +ion-app.app-root { + core-loading { + // @todo @include core-transition(height, 200ms); + + .core-loading-container { + width: 100%; + text-align: center; + padding-top: 10px; + clear: both; + /* @todo @include darkmode() { + color: $core-dark-text-color; + } */ + } + + .core-loading-content { + display: inline; + padding-bottom: 1px; /* This makes height be real */ + } + + &.core-loading-noheight .core-loading-content { + height: auto; + } + + &.safe-area-page { + padding-left: 0 !important; + padding-right: 0 !important; + + > .core-loading-content > *:not[padding], + > .core-loading-content-loading > *:not[padding] { + // @todo @include safe-area-padding-horizontal(0px, 0px); + } + } + } + + .scroll-content > core-loading, + ion-content > .scroll-content > core-loading, + core-tab core-loading, + .core-loading-center { + position: static !important; + } + + .scroll-content > core-loading, + ion-content > .scroll-content > core-loading, + core-tab core-loading, + .core-loading-center, + core-loading.core-loading-loaded { + position: relative; + + > .core-loading-container { + position: absolute; + // @todo @include position(0, 0, 0, 0); + display: table; + height: 100%; + width: 100%; + z-index: 1; + margin: 0; + padding: 0; + clear: both; + + .core-loading-spinner { + display: table-cell; + text-align: center; + vertical-align: middle; + } + } + } +} diff --git a/src/app/components/loading/loading.ts b/src/app/components/loading/loading.ts new file mode 100644 index 000000000..d4d0d0e22 --- /dev/null +++ b/src/app/components/loading/loading.ts @@ -0,0 +1,120 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, Input, OnInit, OnChanges, SimpleChange, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; + +import { CoreEvents, CoreEventsProvider } from '@services/events'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons/core.singletons'; + +/** + * Component to show a loading spinner and message while data is being loaded. + * + * It will show a spinner with a message and hide all the content until 'hideUntil' variable is set to a truthy value (!!hideUntil). + * If 'message' isn't set, default message "Loading" is shown. + * 'message' attribute accepts hardcoded strings, variables, filters, etc. E.g. [message]="'core.loading' | translate". + * + * Usage: + * + * + * + * + * IMPORTANT: Due to how ng-content works in Angular, the content of core-loading will be executed as soon as your view + * is loaded, even if the content hidden. So if you have the following code: + * + * + * The component "my-component" will be initialized immediately, even if dataLoaded is false, but it will be hidden. If you want + * your component to be initialized only if dataLoaded is true, then you should use ngIf: + * + */ +@Component({ + selector: 'core-loading', + templateUrl: 'core-loading.html', + styleUrls: ['loading.scss'], + // @todo animations: [coreShowHideAnimation], +}) +export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { + + @Input() hideUntil: unknown; // Determine when should the contents be shown. + @Input() message?: string; // Message to show while loading. + @ViewChild('content') content: ElementRef; + + protected uniqueId: string; + protected element: HTMLElement; // Current element. + + constructor(element: ElementRef) { + this.element = element.nativeElement; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Calculate the unique ID. + this.uniqueId = 'core-loading-content-' + CoreUtils.instance.getUniqueId('CoreLoadingComponent'); + + if (!this.message) { + // Default loading message. + this.message = Translate.instance.instant('core.loading'); + } + } + + /** + * View has been initialized. + */ + ngAfterViewInit(): void { + // Add class if loaded on init. + if (this.hideUntil) { + this.element.classList.add('core-loading-loaded'); + this.content?.nativeElement.classList.add('core-loading-content'); + } else { + this.content?.nativeElement.classList.remove('core-loading-content'); + this.content?.nativeElement.classList.add('core-loading-content-loading'); + } + } + + /** + * Component input changed. + * + * @param changes Changes. + */ + ngOnChanges(changes: { [name: string]: SimpleChange }): void { + if (changes.hideUntil) { + if (this.hideUntil) { + setTimeout(() => { + // Content is loaded so, center the spinner on the content itself. + this.element.classList.add('core-loading-loaded'); + setTimeout(() => { + // Change CSS to force calculate height. + this.content?.nativeElement.classList.add('core-loading-content'); + this.content?.nativeElement.classList.remove('core-loading-content-loading'); + }, 500); + }); + } else { + this.element.classList.remove('core-loading-loaded'); + this.content?.nativeElement.classList.remove('core-loading-content'); + this.content?.nativeElement.classList.add('core-loading-content-loading'); + } + + // Trigger the event after a timeout since the elements inside ngIf haven't been added to DOM yet. + setTimeout(() => { + CoreEvents.instance.trigger(CoreEventsProvider.CORE_LOADING_CHANGED, { + loaded: !!this.hideUntil, + uniqueId: this.uniqueId, + }); + }); + } + } + +}