From 7c31e79bbdd44697d8c14def533e54bae6d68dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 20 Nov 2023 16:46:29 +0100 Subject: [PATCH] MOBILE-3947 slides: Use Swiper instead of IonSlides --- package-lock.json | 19 ++ package.json | 1 + .../components/calendar/calendar.scss | 2 +- .../calendar/components/calendar/calendar.ts | 13 +- src/addons/calendar/pages/day/day.ts | 11 +- .../mod/book/pages/contents/contents.html | 2 +- .../mod/book/pages/contents/contents.ts | 8 +- .../mod/book/tests/behat/basic_usage.feature | 16 +- src/app/app.component.ts | 3 + src/core/classes/tabs.ts | 278 ++++++++---------- src/core/components/components.module.ts | 3 +- .../components/swipe-slides/swipe-slides.html | 9 +- .../components/swipe-slides/swipe-slides.scss | 16 +- .../components/swipe-slides/swipe-slides.ts | 86 +++--- .../tabs-outlet/core-tabs-outlet.html | 8 +- .../components/tabs-outlet/tabs-outlet.ts | 13 +- src/core/components/tabs/core-tabs.html | 8 +- src/core/components/tabs/tabs.scss | 4 +- src/core/components/tabs/tabs.ts | 22 +- .../editor/components/components.module.ts | 3 +- .../core-editor-rich-text-editor.html | 60 ++-- .../rich-text-editor/rich-text-editor.scss | 2 +- .../rich-text-editor/rich-text-editor.ts | 59 ++-- .../viewer/components/components.module.ts | 3 +- .../viewer/components/image/image.html | 12 +- .../viewer/components/image/image.scss | 2 +- .../features/viewer/components/image/image.ts | 54 ++-- src/testing/services/behat-blocking.ts | 2 +- src/testing/services/behat-dom.ts | 2 +- src/testing/services/behat-runtime.ts | 8 +- src/theme/theme.base.scss | 44 ++- src/theme/theme.light.scss | 2 +- upgrade.txt | 1 + 33 files changed, 424 insertions(+), 352 deletions(-) diff --git a/package-lock.json b/package-lock.json index 729e6b7e9..7d74f0f81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "nl.kingsquare.cordova.background-audio": "^1.0.1", "ogv": "^1.8.9", "rxjs": "~7.8.0", + "swiper": "^11.0.3", "ts-md5": "^1.2.7", "tslib": "^2.3.0", "video.js": "^7.21.1", @@ -27566,6 +27567,24 @@ "es6-symbol": "^3.1.1" } }, + "node_modules/swiper": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.0.5.tgz", + "integrity": "sha512-rhCwupqSyRnWrtNzWzemnBLMoyYuoDgGgspAm/8iBD3jCvAWycPLH4Z3TB0O5520DHLzMx94yUMH/B9Efpa48w==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/package.json b/package.json index 433ff5496..7eb597346 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "nl.kingsquare.cordova.background-audio": "^1.0.1", "ogv": "^1.8.9", "rxjs": "~7.8.0", + "swiper": "^11.0.3", "ts-md5": "^1.2.7", "tslib": "^2.3.0", "video.js": "^7.21.1", diff --git a/src/addons/calendar/components/calendar/calendar.scss b/src/addons/calendar/components/calendar/calendar.scss index f3d27322f..2403bf1a1 100644 --- a/src/addons/calendar/components/calendar/calendar.scss +++ b/src/addons/calendar/components/calendar/calendar.scss @@ -142,7 +142,7 @@ } } - ion-slide { + swiper-slide { display: block; font-size: inherit; justify-content: start; diff --git a/src/addons/calendar/components/calendar/calendar.ts b/src/addons/calendar/components/calendar/calendar.ts index 1d9c2eea9..3561366a8 100644 --- a/src/addons/calendar/components/calendar/calendar.ts +++ b/src/addons/calendar/components/calendar/calendar.ts @@ -64,7 +64,7 @@ import { Translate } from '@singletons'; }) export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy { - @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent; + @ViewChild(CoreSwipeSlidesComponent) swipeSlidesComponent?: CoreSwipeSlidesComponent; @Input() initialYear?: number; // Initial year to load. @Input() initialMonth?: number; // Initial month to load. @@ -185,7 +185,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro this.hiddenDiffer = this.hidden; if (!this.hidden) { - this.slides?.slides?.getSwiper().then(swipper => swipper.update()); + this.swipeSlidesComponent?.updateSlidesComponent(); } } } @@ -248,14 +248,14 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro * Load next month. */ loadNext(): void { - this.slides?.slideNext(); + this.swipeSlidesComponent?.slideNext(); } /** * Load previous month. */ loadPrevious(): void { - this.slides?.slidePrev(); + this.swipeSlidesComponent?.slidePrev(); } /** @@ -343,8 +343,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro */ async viewMonth(month: number, year: number): Promise { const manager = this.manager; - const slides = this.slides; - if (!manager || !slides) { + if (!manager || !this.swipeSlidesComponent) { return; } @@ -360,7 +359,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro // Make sure the day is loaded. await manager.getSource().loadItem(item); - slides.slideToItem(item); + this.swipeSlidesComponent.slideToItem(item); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } finally { diff --git a/src/addons/calendar/pages/day/day.ts b/src/addons/calendar/pages/day/day.ts index 83c8e41a8..6097d8af7 100644 --- a/src/addons/calendar/pages/day/day.ts +++ b/src/addons/calendar/pages/day/day.ts @@ -60,7 +60,7 @@ import { CoreTime } from '@singletons/time'; }) export class AddonCalendarDayPage implements OnInit, OnDestroy { - @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent; + @ViewChild(CoreSwipeSlidesComponent) swipeSlidesComponent?: CoreSwipeSlidesComponent; protected currentSiteId: string; @@ -434,8 +434,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { */ async goToCurrentDay(): Promise { const manager = this.manager; - const slides = this.slides; - if (!manager || !slides) { + if (!manager || !this.swipeSlidesComponent) { return; } @@ -448,7 +447,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { // Make sure the day is loaded. await manager.getSource().loadItem(currentDay); - slides.slideToItem(currentDay); + this.swipeSlidesComponent.slideToItem(currentDay); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } finally { @@ -460,14 +459,14 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * Load next day. */ async loadNext(): Promise { - this.slides?.slideNext(); + this.swipeSlidesComponent?.slideNext(); } /** * Load previous day. */ async loadPrevious(): Promise { - this.slides?.slidePrev(); + this.swipeSlidesComponent?.slidePrev(); } /** diff --git a/src/addons/mod/book/pages/contents/contents.html b/src/addons/mod/book/pages/contents/contents.html index ac443ffd7..c776d1290 100644 --- a/src/addons/mod/book/pages/contents/contents.html +++ b/src/addons/mod/book/pages/contents/contents.html @@ -30,7 +30,7 @@ - +
[] = []; - slidesOpts: CoreSwipeSlidesOptions = { + swiperOpts: CoreSwipeSlidesOptions = { + modules: [IonicSlides], autoHeight: true, observer: true, observeParents: true, @@ -222,7 +224,7 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { return; } - this.slides?.slideToItem({ id: chapterId }); + this.swipeSlidesComponent?.slideToItem({ id: chapterId }); } /** diff --git a/src/addons/mod/book/tests/behat/basic_usage.feature b/src/addons/mod/book/tests/behat/basic_usage.feature index a20893125..7231a77c7 100755 --- a/src/addons/mod/book/tests/behat/basic_usage.feature +++ b/src/addons/mod/book/tests/behat/basic_usage.feature @@ -134,22 +134,22 @@ Feature: Test basic usage of book activity in app But I should not find "This is the first chapter" in the app # Navigate using swipe. - When I swipe to the left in "Chapt 3" "ion-slides" in the app + When I swipe to the left in "Chapt 3" "swiper-container" in the app Then I should find "Chapt 3" in the app And I should find "This is the third chapter" in the app And I should find "4 / 4" in the app - When I swipe to the right in "Chapt 3" "ion-slides" in the app + When I swipe to the right in "Chapt 3" "swiper-container" in the app Then I should find "Chapt 2" in the app And I should find "This is the second chapter" in the app And I should find "3 / 4" in the app - When I swipe to the right in "Chapt 2" "ion-slides" in the app + When I swipe to the right in "Chapt 2" "swiper-container" in the app Then I should find "Chapt 1.1" in the app And I should find "This is a subchapter" in the app And I should find "2 / 4" in the app - When I swipe to the left in "Chapt 1.1" "ion-slides" in the app + When I swipe to the left in "Chapt 1.1" "swiper-container" in the app Then I should find "Chapt 2" in the app And I should find "This is the second chapter" in the app And I should find "3 / 4" in the app @@ -208,22 +208,22 @@ Scenario: View and navigate book contents (teacher) But I should not find "This is the first chapter" in the app # Navigate using swipe. - When I swipe to the left in "Hidden subchapter" "ion-slides" in the app + When I swipe to the left in "Hidden subchapter" "swiper-container" in the app Then I should find "Chapt 3" in the app And I should find "This is the third chapter" in the app And I should find "6 / 7" in the app - When I swipe to the left in "Chapt 3" "ion-slides" in the app + When I swipe to the left in "Chapt 3" "swiper-container" in the app Then I should find "Last hidden" in the app And I should find "Another hidden subchapter" in the app And I should find "7 / 7" in the app - When I swipe to the left in "Last hidden" "ion-slides" in the app + When I swipe to the left in "Last hidden" "swiper-container" in the app Then I should find "Last hidden" in the app And I should find "Another hidden subchapter" in the app And I should find "7 / 7" in the app - When I swipe to the right in "Last hidden" "ion-slides" in the app + When I swipe to the right in "Last hidden" "swiper-container" in the app Then I should find "Chapt 3" in the app And I should find "This is the third chapter" in the app And I should find "6 / 7" in the app diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4f8005305..0c3b63393 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -35,11 +35,14 @@ import { CorePlatform } from '@services/platform'; import { CoreUrl } from '@singletons/url'; import { CoreLogger } from '@singletons/logger'; import { CorePromisedValue } from '@classes/promised-value'; +import { register } from 'swiper/element/bundle'; const MOODLE_SITE_URL_PREFIX = 'url-'; const MOODLE_VERSION_PREFIX = 'version-'; const MOODLEAPP_VERSION_PREFIX = 'moodleapp-'; +register(); + @Component({ selector: 'app-root', templateUrl: 'app.component.html', diff --git a/src/core/classes/tabs.ts b/src/core/classes/tabs.ts index f74babe68..2758cbaf6 100644 --- a/src/core/classes/tabs.ts +++ b/src/core/classes/tabs.ts @@ -25,7 +25,6 @@ import { SimpleChange, ElementRef, } from '@angular/core'; -import { IonSlides } from '@ionic/angular'; import { BackButtonEvent } from '@ionic/core'; import { Subscription } from 'rxjs'; @@ -40,6 +39,9 @@ import { CorePromisedValue } from './promised-value'; import { AsyncDirective } from './async-directive'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CorePlatform } from '@services/platform'; +import { Swiper } from 'swiper'; +import { SwiperOptions } from 'swiper/types'; +import { IonicSlides } from '@ionic/angular'; /** * Class to abstract some common code for tabs. @@ -56,7 +58,34 @@ export class CoreTabsBaseComponent implements OnInit, Aft @Input() hideUntil = false; // Determine when should the contents be shown. @Output() protected ionChange = new EventEmitter(); // Emitted when the tab changes. - @ViewChild(IonSlides) protected slides?: IonSlides; + protected swiper?: Swiper; + @ViewChild('swiperRef') + set swiperRef(swiperRef: ElementRef) { + /** + * This setTimeout waits for Ionic's async initialization to complete. + * Otherwise, an outdated swiper reference will be used. + */ + setTimeout(() => { + if (swiperRef?.nativeElement?.swiper && !this.swiper) { + this.swiper = swiperRef.nativeElement.swiper as Swiper; + + this.swiper.changeLanguageDirection(CorePlatform.isRTL ? 'rtl' : 'ltr'); + + Object.keys(this.swiperOpts).forEach((key) => { + if (this.swiper) { + this.swiper.params[key] = this.swiperOpts[key]; + } + }); + + // Subscribe to changes. + this.swiper.on('slideChangeTransitionEnd', () => { + this.slideChanged(); + }); + + this.init(); + } + }, 0); + } tabs: T[] = []; // List of tabs. @@ -66,18 +95,14 @@ export class CoreTabsBaseComponent implements OnInit, Aft showNextButton = false; maxSlides = 3; numTabsShown = 0; - direction = 'ltr'; description = ''; - slidesOpts = { - initialSlide: 0, + swiperOpts: SwiperOptions = { + modules: [IonicSlides], slidesPerView: 3, centerInsufficientSlides: true, threshold: 10, }; - protected slidesElement?: HTMLIonSlidesElement; - protected initialized = false; - protected resizeListener?: CoreEventObserver; protected isDestroyed = false; protected isCurrentView = true; @@ -87,7 +112,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft protected firstSelectedTab?: string; // ID of the first selected tab to control history. protected backButtonFunction: (event: BackButtonEvent) => void; - // Swiper 6 documentation: https://swiper6.vercel.app/ + // Swiper documentation: https://swiperjs.com/swiper-api protected isInTransition = false; // Wether Slides is in transition. protected subscriptions: Subscription[] = []; protected onReadyPromise = new CorePromisedValue(); @@ -106,12 +131,10 @@ export class CoreTabsBaseComponent implements OnInit, Aft * @inheritdoc */ async ngOnInit(): Promise { - this.direction = CorePlatform.isRTL ? 'rtl' : 'ltr'; - // Change the side when the language changes. this.subscriptions.push(Translate.onLangChange.subscribe(() => { setTimeout(() => { - this.direction = CorePlatform.isRTL ? 'rtl' : 'ltr'; + this.swiper?.changeLanguageDirection(CorePlatform.isRTL ? 'rtl' : 'ltr'); }); })); } @@ -125,6 +148,10 @@ export class CoreTabsBaseComponent implements OnInit, Aft } this.init(); + + this.resizeListener = CoreDom.onWindowResize(() => { + this.calculateSlides(); + }); } /** @@ -136,7 +163,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft } /** - * User entered the page that contains the component. + * @inheritdoc */ ionViewDidEnter(): void { this.isCurrentView = true; @@ -179,7 +206,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft } /** - * User left the page that contains the component. + * @inheritdoc */ ionViewDidLeave(): void { // Unregister the custom back button action for this component. @@ -189,16 +216,15 @@ export class CoreTabsBaseComponent implements OnInit, Aft } /** - * Calculate slides. + * Updates the number of slides to show. */ protected async calculateSlides(): Promise { - if (!this.isCurrentView || !this.initialized) { + if (!this.isCurrentView || !this.swiper) { // Don't calculate if component isn't in current view, the calculations are wrong. return; } this.numTabsShown = this.tabs.reduce((prev: number, current) => current.enabled ? prev + 1 : prev, 0); - if (this.numTabsShown <= 1) { this.hideTabs = true; @@ -209,7 +235,32 @@ export class CoreTabsBaseComponent implements OnInit, Aft await this.calculateMaxSlides(); - await this.updateSlides(); + this.swiperOpts.slidesPerView = Math.min(this.maxSlides, this.numTabsShown); + + this.slideChanged(); + + this.swiper.update(); + await CoreUtils.nextTick(); + + if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.swiper.slidesPerViewDynamic()) { + this.hasSliddenToInitial = true; + this.shouldSlideToInitial = true; + + setTimeout(() => { + if (this.shouldSlideToInitial) { + this.swiper?.slideTo(this.selectedIndex, 0); + this.shouldSlideToInitial = false; + } + }, 400); + + return; + } else if (this.selectedIndex) { + this.hasSliddenToInitial = true; + } + + setTimeout(() => { + this.slideChanged(); // Call slide changed again, sometimes the slide active index takes a while to be updated. + }, 400); } /** @@ -218,8 +269,12 @@ export class CoreTabsBaseComponent implements OnInit, Aft * @param tabId Tab ID. * @returns Selected tab. */ - protected getTabIndex(tabId: string): number { - return this.tabs.findIndex((tab) => tabId == tab.id); + protected getTabIndex(tabId?: string): number { + if (!tabId) { + return -1; + } + + return this.tabs.findIndex((tab) => tabId === tab.id); } /** @@ -228,89 +283,39 @@ export class CoreTabsBaseComponent implements OnInit, Aft * @returns Selected tab. */ getSelected(): T | undefined { - const index = this.selected && this.getTabIndex(this.selected); + const index = this.getTabIndex(this.selected); - return index !== undefined && index >= 0 ? this.tabs[index] : undefined; + return index >= 0 ? this.tabs[index] : undefined; } /** * Init the component. */ protected async init(): Promise { - if (!this.hideUntil) { + if (!this.hideUntil || !this.swiper) { // Hidden, do nothing. return; } try { - await this.initializeSlider(); - await this.initializeTabs(); + const selectedTab = this.calculateInitialTab(); + if (!selectedTab) { + // No enabled tabs, return. + throw new CoreError('No enabled tabs.'); + } + + this.firstSelectedTab = selectedTab.id; + if (this.firstSelectedTab !== undefined) { + this.selectTab(this.firstSelectedTab); + } + + // Check which arrows should be shown. + this.calculateSlides(); } catch { // Something went wrong, ignore. } } - /** - * Initialize the slider elements. - */ - protected async initializeSlider(): Promise { - if (this.initialized) { - return; - } - - if (this.slidesElement) { - // Already initializated, await for ready. - await this.slidesElement.componentOnReady(); - - return; - } - - if (!this.slides) { - await CoreUtils.nextTick(); - } - const slidesSwiper = await this.slides?.getSwiper(); - if (!slidesSwiper || !this.slides) { - throw new CoreError('Swiper not found, will try on next change.'); - } - - this.slidesElement = slidesSwiper.el; - await this.slidesElement.componentOnReady(); - - this.initialized = true; - - // Subscribe to changes. - this.subscriptions.push(this.slides.ionSlideDidChange.subscribe(() => { - this.slideChanged(); - })); - } - - /** - * Initialize the tabs, determining the first tab to be shown. - */ - protected async initializeTabs(): Promise { - if (!this.initialized || !this.slidesElement) { - return; - } - - const selectedTab = this.calculateInitialTab(); - if (!selectedTab) { - // No enabled tabs, return. - throw new CoreError('No enabled tabs.'); - } - - this.firstSelectedTab = selectedTab.id; - if (this.firstSelectedTab !== undefined) { - this.selectTab(this.firstSelectedTab); - } - - // Check which arrows should be shown. - this.calculateSlides(); - - this.resizeListener = CoreDom.onWindowResize(() => { - this.calculateSlides(); - }); - } - /** * Calculate the initial tab to load. * @@ -330,116 +335,71 @@ export class CoreTabsBaseComponent implements OnInit, Aft /** * Method executed when the slides are changed. */ - async slideChanged(): Promise { - if (!this.slidesElement) { + slideChanged(): void { + if (!this.swiper) { return; } this.isInTransition = false; - const slidesCount = await this.slides?.length() || 0; + const slidesCount = this.swiper.slides.length || 0; if (slidesCount > 0) { - this.showPrevButton = !await this.slides?.isBeginning(); - this.showNextButton = !await this.slides?.isEnd(); + this.showPrevButton = !this.swiper.isBeginning; + this.showNextButton = !this.swiper.isEnd; } else { this.showPrevButton = false; this.showNextButton = false; } - const currentIndex = await this.slides?.getActiveIndex(); + const currentIndex = this.swiper.activeIndex; if (this.shouldSlideToInitial && currentIndex != this.selectedIndex) { // Current tab has changed, don't slide to initial anymore. this.shouldSlideToInitial = false; } } - /** - * Updates the number of slides to show. - */ - protected async updateSlides(): Promise { - if (!this.slides) { - return; - } - - this.slidesOpts = { ...this.slidesOpts, slidesPerView: Math.min(this.maxSlides, this.numTabsShown) }; - - await this.slideChanged(); - - await this.slides.update(); - - if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slidesOpts.slidesPerView) { - this.hasSliddenToInitial = true; - this.shouldSlideToInitial = true; - - setTimeout(() => { - if (this.shouldSlideToInitial) { - this.slides?.slideTo(this.selectedIndex, 0); - this.shouldSlideToInitial = false; - } - }, 400); - - return; - } else if (this.selectedIndex) { - this.hasSliddenToInitial = true; - } - - setTimeout(() => { - this.slideChanged(); // Call slide changed again, sometimes the slide active index takes a while to be updated. - }, 400); - } - /** * Calculate the number of slides that can fit on the screen. */ protected async calculateMaxSlides(): Promise { - if (!this.slidesElement || !this.slides) { + if (!this.swiper) { return; } this.maxSlides = 3; await CoreUtils.nextTick(); - let width: number = this.slidesElement.getBoundingClientRect().width; - if (!width) { - const slidesSwiper = await this.slides.getSwiper(); - - await slidesSwiper.updateSize(); - await CoreUtils.nextTick(); - - width = slidesSwiper.width; - if (!width) { - - return; - } + if (!this.swiper.width) { + return; } const zoomLevel = await CoreSettingsHelper.getZoom(); - this.maxSlides = Math.floor(width / (zoomLevel / 100 * CoreTabsBaseComponent.MIN_TAB_WIDTH)); + this.maxSlides = Math.floor(this.swiper.width / (zoomLevel / 100 * CoreTabsBaseComponent.MIN_TAB_WIDTH)); } /** * Method that shows the next tab. */ - async slideNext(): Promise { + slideNext(): void { // Stop if slides are in transition. - if (!this.showNextButton || this.isInTransition || !this.slides) { + if (!this.showNextButton || this.isInTransition || !this.swiper) { return; } - if (await this.slides.isBeginning()) { + if (this.swiper.isBeginning) { // Slide to the second page. - this.slides.slideTo(this.maxSlides); + this.swiper.slideTo(this.maxSlides); } else { - const currentIndex = await this.slides.getActiveIndex(); + const currentIndex = this.swiper.activeIndex; if (currentIndex !== undefined) { const nextSlideIndex = currentIndex + this.maxSlides; this.isInTransition = true; if (nextSlideIndex < this.numTabsShown) { // Slide to the next page. - await this.slides.slideTo(nextSlideIndex); + this.swiper.slideTo(nextSlideIndex); } else { // Slide to the latest slide. - await this.slides.slideTo(this.numTabsShown - 1); + this.swiper.slideTo(this.numTabsShown - 1); } } @@ -449,26 +409,26 @@ export class CoreTabsBaseComponent implements OnInit, Aft /** * Method that shows the previous tab. */ - async slidePrev(): Promise { + slidePrev(): void { // Stop if slides are in transition. - if (!this.showPrevButton || this.isInTransition || !this.slides) { + if (!this.showPrevButton || this.isInTransition || !this.swiper) { return; } - if (await this.slides.isEnd()) { - this.slides.slideTo(this.numTabsShown - this.maxSlides * 2); + if (this.swiper.isEnd) { + this.swiper.slideTo(this.numTabsShown - this.maxSlides * 2); // Slide to the previous of the latest page. } else { - const currentIndex = await this.slides.getActiveIndex(); + const currentIndex = this.swiper.activeIndex; if (currentIndex !== undefined) { const prevSlideIndex = currentIndex - this.maxSlides; this.isInTransition = true; if (prevSlideIndex >= 0) { // Slide to the previous page. - await this.slides.slideTo(prevSlideIndex); + this.swiper.slideTo(prevSlideIndex); } else { // Slide to the first page. - await this.slides.slideTo(0); + this.swiper.slideTo(0); } } } @@ -517,12 +477,12 @@ export class CoreTabsBaseComponent implements OnInit, Aft return; } - if (this.selected && this.slides) { + if (this.selected && this.swiper) { // Check if we need to slide to the tab because it's not visible. - const firstVisibleTab = await this.slides.getActiveIndex(); - const lastVisibleTab = firstVisibleTab + this.slidesOpts.slidesPerView - 1; + const firstVisibleTab = this.swiper.activeIndex; + const lastVisibleTab = firstVisibleTab + this.swiper.slidesPerViewDynamic() - 1; if (index < firstVisibleTab || index > lastVisibleTab) { - await this.slides.slideTo(index, 0, true); + this.swiper.slideTo(index, 0, true); } } diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts index fd78701a6..8e35e23e2 100644 --- a/src/core/components/components.module.ts +++ b/src/core/components/components.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IonicModule } from '@ionic/angular'; import { TranslateModule } from '@ngx-translate/core'; @@ -170,5 +170,6 @@ import { CoreSitesListComponent } from './sites-list/sites-list'; CoreSheetModalComponent, CoreSitesListComponent, ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class CoreComponentsModule {} diff --git a/src/core/components/swipe-slides/swipe-slides.html b/src/core/components/swipe-slides/swipe-slides.html index 65ab4c92a..088172613 100644 --- a/src/core/components/swipe-slides/swipe-slides.html +++ b/src/core/components/swipe-slides/swipe-slides.html @@ -1,6 +1,7 @@ - - + + - - + + diff --git a/src/core/components/swipe-slides/swipe-slides.scss b/src/core/components/swipe-slides/swipe-slides.scss index 90b467e57..4f221edf4 100644 --- a/src/core/components/swipe-slides/swipe-slides.scss +++ b/src/core/components/swipe-slides/swipe-slides.scss @@ -1,13 +1,13 @@ :host { - ion-slides { + swiper-container { height: 100%; - } - ion-slide { - display: block; - font-size: inherit; - justify-content: start; - align-items: start; - text-align: start; + swiper-slide { + display: block; + font-size: inherit; + justify-content: start; + align-items: start; + text-align: start; + } } } diff --git a/src/core/components/swipe-slides/swipe-slides.ts b/src/core/components/swipe-slides/swipe-slides.ts index e411d6748..a2569739e 100644 --- a/src/core/components/swipe-slides/swipe-slides.ts +++ b/src/core/components/swipe-slides/swipe-slides.ts @@ -13,16 +13,17 @@ // limitations under the License. import { - Component, ContentChild, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, TemplateRef, ViewChild, + Component, ContentChild, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChange, TemplateRef, ViewChild, } from '@angular/core'; import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager'; -import { IonContent, IonSlides } from '@ionic/angular'; +import { IonContent } from '@ionic/angular'; import { CoreDomUtils, VerticalPoint } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; import { CoreMath } from '@singletons/math'; - +import { Swiper } from 'swiper'; +import { SwiperOptions } from 'swiper/types'; /** * Helper component to display swipable slides. */ @@ -38,13 +39,31 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe @Output() onWillChange = new EventEmitter>(); @Output() onDidChange = new EventEmitter>(); - @ViewChild(IonSlides) slides?: IonSlides; + protected swiper?: Swiper; + @ViewChild('swiperRef') + set swiperRef(swiperRef: ElementRef) { + /** + * This setTimeout waits for Ionic's async initialization to complete. + * Otherwise, an outdated swiper reference will be used. + */ + setTimeout(() => { + if (swiperRef?.nativeElement?.swiper) { + this.swiper = swiperRef.nativeElement.swiper as Swiper; + + Object.keys(this.options).forEach((key) => { + if (this.swiper) { + this.swiper.params[key] = this.options[key]; + } + }); + } + }, 0); + } + @ContentChild(TemplateRef) template?: TemplateRef; // Template defined by the content. protected hostElement: HTMLElement; protected unsubscribe?: () => void; protected resizeListener: CoreEventObserver; - protected updateSlidesPromise?: Promise; protected activeSlideIndexes: number[] = []; constructor( @@ -53,18 +72,26 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe ) { this.hostElement = elementRef.nativeElement; - this.resizeListener = CoreDom.onWindowResize(async () => { - await this.updateSlidesComponent(); + this.resizeListener = CoreDom.onWindowResize(() => { + this.updateSlidesComponent(); }); } /** * @inheritdoc */ - ngOnChanges(): void { + ngOnChanges(changes: { [name: string]: SimpleChange }): void { if (!this.unsubscribe && this.manager) { this.initialize(this.manager); } + + if (changes.options) { + Object.keys(this.options).forEach((key) => { + if (this.swiper) { + this.swiper.params[key] = this.options[key]; + } + }); + } } get items(): Item[] { @@ -133,23 +160,20 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe * @param speed Animation speed. * @param runCallbacks Whether to run callbacks. */ - async slideToIndex(index: number, speed?: number, runCallbacks?: boolean): Promise { + slideToIndex(index: number, speed?: number, runCallbacks?: boolean): void { // If slides are being updated, wait for the update to finish. - await this.updateSlidesPromise; - - const slides = this.slides; - if (!slides) { + if (!this.swiper) { return; } // Verify that the number of slides matches the number of items. - const slidesLength = await slides.length(); + const slidesLength = this.swiper.slides.length; if (slidesLength !== this.items.length) { // Number doesn't match, do a new update to try to match them. - await this.updateSlidesComponent(); + this.updateSlidesComponent(); } - this.slides?.slideTo(index, speed, runCallbacks); + this.swiper?.slideTo(index, speed, runCallbacks); } /** @@ -173,7 +197,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe * @param runCallbacks Whether to run callbacks. */ slideNext(speed?: number, runCallbacks?: boolean): void { - this.slides?.slideNext(speed, runCallbacks); + this.swiper?.slideNext(speed, runCallbacks); } /** @@ -183,7 +207,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe * @param runCallbacks Whether to run callbacks. */ slidePrev(speed?: number, runCallbacks?: boolean): void { - this.slides?.slidePrev(speed, runCallbacks); + this.swiper?.slidePrev(speed, runCallbacks); } /** @@ -194,7 +218,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe await CoreUtils.nextTick(); // Update the slides component so the slides list reflects the new items. - await this.updateSlidesComponent(); + this.updateSlidesComponent(); const currentItem = this.manager?.getSelectedItem(); @@ -205,7 +229,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe // Keep the same slide in case the list has changed. const newIndex = this.manager.getSource().getItemIndex(currentItem) ?? -1; if (newIndex != -1) { - this.slides?.slideTo(newIndex, 0, false); + this.swiper?.slideTo(newIndex, 0, false); } } @@ -270,11 +294,11 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe * @returns Promise resolved with current item data. Null if not found. */ protected async getCurrentSlideItemData(): Promise | null> { - if (!this.slides || !this.manager) { + if (!this.swiper || !this.manager) { return null; } - const index = await this.slides.getActiveIndex(); + const index = this.swiper.activeIndex; const items = this.manager.getSource().getItems(); const currentItem = items && items[index]; @@ -291,19 +315,8 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe /** * Update slides component. */ - protected async updateSlidesComponent(): Promise { - if (!this.slides) { - return; - } - - const promise = this.slides.update(); - this.updateSlidesPromise = promise; - - await promise; - - if (this.updateSlidesPromise === promise) { - delete this.updateSlidesPromise; - } + updateSlidesComponent(): void { + this.swiper?.update(); } /** @@ -321,8 +334,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe * * @todo Change unknown with the right type once Swiper library is used. */ -export type CoreSwipeSlidesOptions = Record & { - initialSlide?: number; +export type CoreSwipeSlidesOptions = SwiperOptions & { scrollOnChange?: 'top' | 'none'; // Scroll behaviour on change slide. By default, none. }; diff --git a/src/core/components/tabs-outlet/core-tabs-outlet.html b/src/core/components/tabs-outlet/core-tabs-outlet.html index 0f2af04ea..f7f2fdea5 100644 --- a/src/core/components/tabs-outlet/core-tabs-outlet.html +++ b/src/core/components/tabs-outlet/core-tabs-outlet.html @@ -6,9 +6,9 @@ [attr.aria-label]="'core.previous' | translate"> - + - - + - + diff --git a/src/core/components/tabs-outlet/tabs-outlet.ts b/src/core/components/tabs-outlet/tabs-outlet.ts index 725b92b29..34b6a2724 100644 --- a/src/core/components/tabs-outlet/tabs-outlet.ts +++ b/src/core/components/tabs-outlet/tabs-outlet.ts @@ -23,7 +23,6 @@ import { SimpleChange, } from '@angular/core'; import { IonRouterOutlet, IonTabs, ViewDidEnter, ViewDidLeave } from '@ionic/angular'; -import { Subscription } from 'rxjs'; import { CoreUtils } from '@services/utils/utils'; import { Params } from '@angular/router'; @@ -63,8 +62,6 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent; protected existsInNavigationStack = false; @@ -90,7 +87,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent { + this.subscriptions.push(this.ionTabs.outlet.stackDidChange.subscribe(async (stackEvent: StackDidChangeEvent) => { if (!this.isCurrentView) { return; } @@ -110,10 +107,10 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent { + })); + this.subscriptions.push(this.ionTabs.outlet.activateEvents.subscribe(() => { this.lastActiveComponent = this.ionTabs.outlet.component; - }); + })); } /** @@ -221,8 +218,6 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent - + - - + - + diff --git a/src/core/components/tabs/tabs.scss b/src/core/components/tabs/tabs.scss index 66a57c342..8784d0e4a 100644 --- a/src/core/components/tabs/tabs.scss +++ b/src/core/components/tabs/tabs.scss @@ -44,12 +44,12 @@ } } - ion-slides { + swiper-container { text-align: center; line-height: 1.6rem; flex-grow: 1; - ion-slide { + swiper-slide { border-bottom: 2px solid transparent; min-width: 100px; height: var(--height); diff --git a/src/core/components/tabs/tabs.ts b/src/core/components/tabs/tabs.ts index 9d213583b..ae8782a56 100644 --- a/src/core/components/tabs/tabs.ts +++ b/src/core/components/tabs/tabs.ts @@ -47,7 +47,18 @@ export class CoreTabsComponent extends CoreTabsBaseComponent i @Input() parentScrollable = false; // Determine if the scroll should be in the parent content or the tab itself. @Input() layout: 'icon-top' | 'icon-start' | 'icon-end' | 'icon-bottom' | 'icon-hide' | 'label-hide' = 'icon-hide'; - @ViewChild('originalTabs') originalTabsRef?: ElementRef; + @ViewChild('originalTabs') + set originalTabs(originalTabs: ElementRef) { + /** + * This setTimeout waits for Ionic's async initialization to complete. + * Otherwise, an outdated swiper reference will be used. + */ + setTimeout(() => { + if (originalTabs.nativeElement && !this.originalTabsContainer) { + this.originalTabsContainer = this.originalTabs?.nativeElement; + } + }, 0); + } protected originalTabsContainer?: HTMLElement; // The container of the original tabs. It will include each tab's content. @@ -60,15 +71,6 @@ export class CoreTabsComponent extends CoreTabsBaseComponent i if (this.isDestroyed) { return; } - - this.originalTabsContainer = this.originalTabsRef?.nativeElement; - } - - /** - * Initialize the tabs, determining the first tab to be shown. - */ - protected async initializeTabs(): Promise { - await super.initializeTabs(); } /** diff --git a/src/core/features/editor/components/components.module.ts b/src/core/features/editor/components/components.module.ts index 3c46c84a7..8bc9e4105 100644 --- a/src/core/features/editor/components/components.module.ts +++ b/src/core/features/editor/components/components.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CoreEditorRichTextEditorComponent } from './rich-text-editor/rich-text-editor'; import { CoreSharedModule } from '@/core/shared.module'; @@ -29,5 +29,6 @@ import { CoreSharedModule } from '@/core/shared.module'; exports: [ CoreEditorRichTextEditorComponent, ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class CoreEditorComponentsModule {} diff --git a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html index 2d260dbd3..023a13ea2 100644 --- a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html +++ b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html @@ -21,103 +21,103 @@ [attr.aria-label]="'core.previous' | translate" [tabindex]="toolbarPrevHidden ? -1 : 0"> - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + +