diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts
index 0137d6548..e06d19a46 100644
--- a/src/core/components/components.module.ts
+++ b/src/core/components/components.module.ts
@@ -30,6 +30,7 @@ import { CoreRecaptchaModalComponent } from './recaptcha/recaptchamodal';
import { CoreShowPasswordComponent } from './show-password/show-password';
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
import { CoreTabsComponent } from './tabs/tabs';
+import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
@@ -49,6 +50,7 @@ import { CorePipesModule } from '@pipes/pipes.module';
CoreShowPasswordComponent,
CoreEmptyBoxComponent,
CoreTabsComponent,
+ CoreInfiniteLoadingComponent,
],
imports: [
CommonModule,
@@ -71,6 +73,7 @@ import { CorePipesModule } from '@pipes/pipes.module';
CoreShowPasswordComponent,
CoreEmptyBoxComponent,
CoreTabsComponent,
+ CoreInfiniteLoadingComponent,
],
})
export class CoreComponentsModule {}
diff --git a/src/core/components/infinite-loading/core-infinite-loading.html b/src/core/components/infinite-loading/core-infinite-loading.html
new file mode 100644
index 000000000..cb99cd201
--- /dev/null
+++ b/src/core/components/infinite-loading/core-infinite-loading.html
@@ -0,0 +1,29 @@
+
+
+
+ {{ 'core.loadmore' | translate }}
+
+
+ {{ 'core.tryagain' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.loadmore' | translate }}
+
+
+ {{ 'core.tryagain' | translate }}
+
+
+
+
+
+
+
diff --git a/src/core/components/infinite-loading/infinite-loading.ts b/src/core/components/infinite-loading/infinite-loading.ts
new file mode 100644
index 000000000..4be22c507
--- /dev/null
+++ b/src/core/components/infinite-loading/infinite-loading.ts
@@ -0,0 +1,158 @@
+// (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, Output, EventEmitter, OnChanges, SimpleChange, Optional, ViewChild, ElementRef } from '@angular/core';
+import { IonContent, IonInfiniteScroll } from '@ionic/angular';
+import { CoreDomUtils } from '@services/utils/dom';
+
+/**
+ * Component to show a infinite loading trigger and spinner while more data is being loaded.
+ *
+ * Usage:
+ *
+ */
+@Component({
+ selector: 'core-infinite-loading',
+ templateUrl: 'core-infinite-loading.html',
+})
+export class CoreInfiniteLoadingComponent implements OnChanges {
+
+ @Input() enabled!: boolean;
+ @Input() error = false;
+ @Input() position: 'top' | 'bottom' = 'bottom';
+ @Output() action: EventEmitter<() => void>; // Will emit an event when triggered.
+
+ @ViewChild('topbutton') topButton?: ElementRef;
+ @ViewChild('infinitescroll') infiniteEl?: ElementRef;
+ @ViewChild('bottombutton') bottomButton?: ElementRef;
+ @ViewChild('spinnercontainer') spinnerContainer?: ElementRef;
+ @ViewChild(IonInfiniteScroll) infiniteScroll?: IonInfiniteScroll;
+
+ loadingMore = false; // Hide button and avoid loading more.
+
+ protected threshold = parseFloat('15%') / 100;
+
+ constructor(
+ protected element: ElementRef,
+ @Optional() protected content: IonContent,
+ ) {
+ this.action = new EventEmitter();
+ }
+
+ /**
+ * Detect changes on input properties.
+ *
+ * @param changes Changes.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}): void {
+ if (changes.enabled && this.enabled && this.position == 'bottom') {
+
+ // Infinite scroll enabled. If the list doesn't fill the full height, infinite scroll isn't triggered automatically.
+ this.checkScrollDistance();
+ }
+ }
+
+ /**
+ * Checks scroll distance to the beginning/end to load more items if needed.
+ *
+ * Previously, this function what firing an scroll event but now we have to calculate the distance
+ * like the Ionic component does.
+ */
+ protected async checkScrollDistance(): Promise {
+ if (this.enabled) {
+ const scrollElement = await this.content.getScrollElement();
+
+ const infiniteHeight = this.element.nativeElement.getBoundingClientRect().height;
+
+ const scrollTop = scrollElement.scrollTop;
+ const height = scrollElement.offsetHeight;
+ const threshold = height * this.threshold;
+
+ const distanceFromInfinite = (this.position === 'bottom')
+ ? scrollElement.scrollHeight - infiniteHeight - scrollTop - threshold - height
+ : scrollTop - infiniteHeight - threshold;
+
+ if (distanceFromInfinite < 0 && !this.loadingMore && this.enabled) {
+ this.loadMore();
+ }
+ }
+ }
+
+
+ /**
+ * Load More items calling the action provided.
+ */
+ loadMore(): void {
+ if (this.loadingMore) {
+ return;
+ }
+
+ this.loadingMore = true;
+ this.action.emit(this.complete.bind(this));
+ }
+
+ /**
+ * Complete loading.
+ */
+ complete(): void {
+ if (this.position == 'top') {
+ // Wait a bit before allowing loading more, otherwise it could be re-triggered automatically when it shouldn't.
+ setTimeout(this.completeLoadMore.bind(this), 400);
+ } else {
+ this.completeLoadMore();
+ }
+ }
+
+ /**
+ * Complete loading.
+ */
+ protected async completeLoadMore(): Promise {
+ this.loadingMore = false;
+ await this.infiniteScroll?.complete();
+
+ // More items loaded. If the list doesn't fill the full height, infinite scroll isn't triggered automatically.
+ this.checkScrollDistance();
+ }
+
+ /**
+ * Get the height of the element.
+ *
+ * @return Height.
+ * @todo erase is not needed: I'm depreacating it because if not needed or getBoundingClientRect has the same result, it should
+ * be erased, also with getElementHeight
+ * @deprecated
+ */
+ getHeight(): number {
+ // return this.element.nativeElement.getBoundingClientRect().height;
+
+ return (this.position == 'top' ? this.getElementHeight(this.topButton): this.getElementHeight(this.bottomButton)) +
+ this.getElementHeight(this.infiniteEl) +
+ this.getElementHeight(this.spinnerContainer);
+ }
+
+ /**
+ * Get the height of an element.
+ *
+ * @param element Element ref.
+ * @return Height.
+ */
+ protected getElementHeight(element?: ElementRef): number {
+ if (element && element.nativeElement) {
+ return CoreDomUtils.instance.getElementHeight(element.nativeElement, true, true, true);
+ }
+
+ return 0;
+ }
+
+}