MOBILE-3947 slides: Use Swiper instead of IonSlides

main
Pau Ferrer Ocaña 2023-11-20 16:46:29 +01:00
parent 34147fceb7
commit 7c31e79bbd
33 changed files with 424 additions and 352 deletions

19
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -142,7 +142,7 @@
}
}
ion-slide {
swiper-slide {
display: block;
font-size: inherit;
justify-content: start;

View File

@ -64,7 +64,7 @@ import { Translate } from '@singletons';
})
export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy {
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent<PreloadedMonth>;
@ViewChild(CoreSwipeSlidesComponent) swipeSlidesComponent?: CoreSwipeSlidesComponent<PreloadedMonth>;
@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<void> {
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 {

View File

@ -60,7 +60,7 @@ import { CoreTime } from '@singletons/time';
})
export class AddonCalendarDayPage implements OnInit, OnDestroy {
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent<PreloadedDay>;
@ViewChild(CoreSwipeSlidesComponent) swipeSlidesComponent?: CoreSwipeSlidesComponent<PreloadedDay>;
protected currentSiteId: string;
@ -434,8 +434,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
*/
async goToCurrentDay(): Promise<void> {
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<void> {
this.slides?.slideNext();
this.swipeSlidesComponent?.slideNext();
}
/**
* Load previous day.
*/
async loadPrevious(): Promise<void> {
this.slides?.slidePrev();
this.swipeSlidesComponent?.slidePrev();
}
/**

View File

@ -30,7 +30,7 @@
</ion-item>
</ion-card>
<core-swipe-slides [manager]="manager" [options]="slidesOpts">
<core-swipe-slides [manager]="manager" [options]="swiperOpts">
<ng-template let-chapter="item" let-active="active">
<div class="ion-padding">
<core-format-text [component]="component" [componentId]="cmId" [text]="chapter.content" contextLevel="module"

View File

@ -41,6 +41,7 @@ import {
} from '../../services/book';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { CoreUrlUtils } from '@services/utils/url';
import { IonicSlides } from '@ionic/angular';
/**
* Page that displays a book contents.
@ -52,7 +53,7 @@ import { CoreUrlUtils } from '@services/utils/url';
})
export class AddonModBookContentsPage implements OnInit, OnDestroy {
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent;
@ViewChild(CoreSwipeSlidesComponent) swipeSlidesComponent?: CoreSwipeSlidesComponent;
title = '';
cmId!: number;
@ -63,7 +64,8 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy {
warning = '';
displayNavBar = true;
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
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 });
}
/**

View File

@ -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

View File

@ -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',

View File

@ -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<T extends CoreTabBase> implements OnInit, Aft
@Input() hideUntil = false; // Determine when should the contents be shown.
@Output() protected ionChange = new EventEmitter<T>(); // 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<T extends CoreTabBase> 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<T extends CoreTabBase> 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<void>();
@ -106,12 +131,10 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
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<T extends CoreTabBase> implements OnInit, Aft
}
this.init();
this.resizeListener = CoreDom.onWindowResize(() => {
this.calculateSlides();
});
}
/**
@ -136,7 +163,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
}
/**
* User entered the page that contains the component.
* @inheritdoc
*/
ionViewDidEnter(): void {
this.isCurrentView = true;
@ -179,7 +206,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> 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<T extends CoreTabBase> implements OnInit, Aft
}
/**
* Calculate slides.
* Updates the number of slides to show.
*/
protected async calculateSlides(): Promise<void> {
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<T extends CoreTabBase> 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<T extends CoreTabBase> 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,70 +283,21 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> 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<void> {
if (!this.hideUntil) {
if (!this.hideUntil || !this.swiper) {
// Hidden, do nothing.
return;
}
try {
await this.initializeSlider();
await this.initializeTabs();
} catch {
// Something went wrong, ignore.
}
}
/**
* Initialize the slider elements.
*/
protected async initializeSlider(): Promise<void> {
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 = <HTMLIonSlidesElement>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<void> {
if (!this.initialized || !this.slidesElement) {
return;
}
const selectedTab = this.calculateInitialTab();
if (!selectedTab) {
// No enabled tabs, return.
@ -305,10 +311,9 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
// Check which arrows should be shown.
this.calculateSlides();
this.resizeListener = CoreDom.onWindowResize(() => {
this.calculateSlides();
});
} catch {
// Something went wrong, ignore.
}
}
/**
@ -330,116 +335,71 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
/**
* Method executed when the slides are changed.
*/
async slideChanged(): Promise<void> {
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<void> {
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<void> {
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) {
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<void> {
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<T extends CoreTabBase> implements OnInit, Aft
/**
* Method that shows the previous tab.
*/
async slidePrev(): Promise<void> {
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<T extends CoreTabBase> 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);
}
}

View File

@ -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 {}

View File

@ -1,6 +1,7 @@
<ion-slides *ngIf="loaded" (ionSlideWillChange)="slideWillChange()" (ionSlideDidChange)="slideDidChange()" [options]="options">
<ion-slide *ngFor="let item of items; index as index" [attr.aria-hidden]="!isActive(index)">
<swiper-container #swiperRef *ngIf="loaded" (slidechangetransitionstart)="slideWillChange()" (slidechangetransitionend)="slideDidChange()"
[initialSlide]="options.initialSlide" [runCallbacksOnInit]="options.runCallbacksOnInit">
<swiper-slide *ngFor="let item of items; index as index" [attr.aria-hidden]="!isActive(index)">
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item, active: isActive(index)}">
</ng-container>
</ion-slide>
</ion-slides>
</swiper-slide>
</swiper-container>

View File

@ -1,9 +1,8 @@
:host {
ion-slides {
swiper-container {
height: 100%;
}
ion-slide {
swiper-slide {
display: block;
font-size: inherit;
justify-content: start;
@ -11,3 +10,4 @@
text-align: start;
}
}
}

View File

@ -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<Item = unknown> implements OnChanges, OnDe
@Output() onWillChange = new EventEmitter<CoreSwipeCurrentItemData<Item>>();
@Output() onDidChange = new EventEmitter<CoreSwipeCurrentItemData<Item>>();
@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<unknown>; // Template defined by the content.
protected hostElement: HTMLElement;
protected unsubscribe?: () => void;
protected resizeListener: CoreEventObserver;
protected updateSlidesPromise?: Promise<void>;
protected activeSlideIndexes: number[] = [];
constructor(
@ -53,18 +72,26 @@ export class CoreSwipeSlidesComponent<Item = unknown> 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<Item = unknown> implements OnChanges, OnDe
* @param speed Animation speed.
* @param runCallbacks Whether to run callbacks.
*/
async slideToIndex(index: number, speed?: number, runCallbacks?: boolean): Promise<void> {
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<Item = unknown> 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<Item = unknown> 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<Item = unknown> 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<Item = unknown> 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<Item = unknown> implements OnChanges, OnDe
* @returns Promise resolved with current item data. Null if not found.
*/
protected async getCurrentSlideItemData(): Promise<CoreSwipeCurrentItemData<Item> | 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<Item = unknown> implements OnChanges, OnDe
/**
* Update slides component.
*/
protected async updateSlidesComponent(): Promise<void> {
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<Item = unknown> implements OnChanges, OnDe
*
* @todo Change unknown with the right type once Swiper library is used.
*/
export type CoreSwipeSlidesOptions = Record<string, unknown> & {
initialSlide?: number;
export type CoreSwipeSlidesOptions = SwiperOptions & {
scrollOnChange?: 'top' | 'none'; // Scroll behaviour on change slide. By default, none.
};

View File

@ -6,9 +6,9 @@
[attr.aria-label]="'core.previous' | translate">
<ion-icon *ngIf="showPrevButton" name="fas-chevron-left" aria-hidden="true" slot="icon-only"></ion-icon>
</ion-button>
<ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description">
<swiper-container #swiperRef [slidesPerView]="swiperOpts.slidesPerView" role="tablist" [attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<ion-slide *ngIf="tab.id" role="presentation" [id]="tab.id + '-tab'" tabindex="-1"
<swiper-slide *ngIf="tab.id" role="presentation" [id]="tab.id + '-tab'" tabindex="-1"
[class.selected]="selected === tab.id" class="{{tab.class}}">
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown(tab.id, $event)"
(keyup)="tabAction.keyUp(tab.id, $event)" [tab]="tab.page" [layout]="layout" role="tab"
@ -25,9 +25,9 @@
</ion-badge>
</ion-label>
</ion-tab-button>
</ion-slide>
</swiper-slide>
</ng-container>
</ion-slides>
</swiper-container>
<ion-button fill="clear" class="arrow-button" (click)="slideNext()" [disabled]="!showNextButton"
[attr.aria-label]="'core.next' | translate">
<ion-icon *ngIf="showNextButton" name="fas-chevron-right" aria-hidden="true" slot="icon-only"></ion-icon>

View File

@ -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<CoreTabsOutle
@ViewChild(IonTabs) protected ionTabs!: IonTabs;
protected stackEventsSubscription?: Subscription;
protected outletActivatedSubscription?: Subscription;
protected lastActiveComponent?: Partial<ViewDidLeave>;
protected existsInNavigationStack = false;
@ -90,7 +87,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
return;
}
this.stackEventsSubscription = this.ionTabs.outlet.stackDidChange.subscribe(async (stackEvent: StackDidChangeEvent) => {
this.subscriptions.push(this.ionTabs.outlet.stackDidChange.subscribe(async (stackEvent: StackDidChangeEvent) => {
if (!this.isCurrentView) {
return;
}
@ -110,10 +107,10 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
}
this.showHideNavBarButtons(stackEvent.enteringView.element.tagName);
});
this.outletActivatedSubscription = this.ionTabs.outlet.activateEvents.subscribe(() => {
}));
this.subscriptions.push(this.ionTabs.outlet.activateEvents.subscribe(() => {
this.lastActiveComponent = this.ionTabs.outlet.component;
});
}));
}
/**
@ -221,8 +218,6 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
*/
ngOnDestroy(): void {
super.ngOnDestroy();
this.stackEventsSubscription?.unsubscribe();
this.outletActivatedSubscription?.unsubscribe();
this.existsInNavigationStack = false;
}

View File

@ -5,9 +5,9 @@
[attr.aria-label]="'core.previous' | translate">
<ion-icon *ngIf="showPrevButton" name="fas-chevron-left" aria-hidden="true" slot="icon-only"></ion-icon>
</ion-button>
<ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description">
<swiper-container #swiperRef [slidesPerView]="swiperOpts.slidesPerView" role="tablist" [attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<ion-slide *ngIf="tab.enabled" role="presentation" [id]="tab.id! + '-tab'" [class.selected]="selected === tab.id"
<swiper-slide *ngIf="tab.enabled" role="presentation" [id]="tab.id! + '-tab'" [class.selected]="selected === tab.id"
class="{{tab.class}}">
<ion-tab-button (click)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown(tab.id, $event)"
(keyup)="tabAction.keyUp(tab.id, $event)" [layout]="layout" role="tab" [attr.aria-controls]="tab.id"
@ -23,9 +23,9 @@
</ion-badge>
</ion-label>
</ion-tab-button>
</ion-slide>
</swiper-slide>
</ng-container>
</ion-slides>
</swiper-container>
<ion-button fill="clear" class="arrow-button" (click)="slideNext()" [disabled]="!showNextButton"
[attr.aria-label]="'core.next' | translate">
<ion-icon *ngIf="showNextButton" name="fas-chevron-right" aria-hidden="true" slot="icon-only"></ion-icon>

View File

@ -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);

View File

@ -47,7 +47,18 @@ export class CoreTabsComponent extends CoreTabsBaseComponent<CoreTabComponent> 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<CoreTabComponent> i
if (this.isDestroyed) {
return;
}
this.originalTabsContainer = this.originalTabsRef?.nativeElement;
}
/**
* Initialize the tabs, determining the first tab to be shown.
*/
protected async initializeTabs(): Promise<void> {
await super.initializeTabs();
}
/**

View File

@ -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 {}

View File

@ -21,103 +21,103 @@
[attr.aria-label]="'core.previous' | translate" [tabindex]="toolbarPrevHidden ? -1 : 0">
<ion-icon name="fas-chevron-left" aria-hidden="true"></ion-icon>
</button>
<ion-slides [options]="slidesOpts" [dir]="direction" (ionSlideDidChange)="updateToolbarArrows()">
<swiper-container #swiperRef [slidesPerView]="swiperOpts.slidesPerView" (slidechangetransitionend)="updateToolbarArrows()">
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
<ion-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strong" [title]="'core.editor.bold' | translate"
(click)="buttonAction($event, 'bold', 'strong')" (keyup)="buttonAction($event, 'bold', 'strong')"
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-bold" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.em" [title]="'core.editor.italic' | translate"
(click)="buttonAction($event, 'italic', 'em')" (keyup)="buttonAction($event, 'italic', 'em')"
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-italic" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.u" [title]="'core.editor.underline' | translate"
(click)="buttonAction($event, 'underline', 'u')" (keyup)="buttonAction($event, 'underline', 'u')"
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-underline" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strike" [title]="'core.editor.strikethrough' | translate"
(click)="buttonAction($event, 'strikethrough', 'strike')" (keyup)="buttonAction($event, 'strikethrough', 'strike')"
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-strikethrough" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.p" [title]="'core.editor.p' | translate"
(click)="buttonAction($event, 'p', 'block')" (keyup)="buttonAction($event, 'p', 'block')" (mousedown)="downAction($event)"
(keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-paragraph" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h3" [title]="'core.editor.h3' | translate"
(click)="buttonAction($event, 'h3', 'block')" (keyup)="buttonAction($event, 'h3', 'block')" (mousedown)="downAction($event)"
(keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-heading" aria-hidden="true"></ion-icon><span aria-hidden="true">3</span>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h4" [title]="'core.editor.h4' | translate"
(click)="buttonAction($event, 'h4', 'block')" (keyup)="buttonAction($event, 'h4', 'block')" (mousedown)="downAction($event)"
(keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-heading" aria-hidden="true"></ion-icon><span aria-hidden="true">4</span>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h5" [title]="'core.editor.h5' | translate"
(click)="buttonAction($event, 'h5', 'block')" (keyup)="buttonAction($event, 'h5', 'block')" (mousedown)="downAction($event)"
(keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-heading" aria-hidden="true"></ion-icon><span aria-hidden="true">5</span>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ul" [title]="'core.editor.unorderedlist' | translate"
(click)="buttonAction($event, 'insertUnorderedList')" (mousedown)="downAction($event)" (keydown)="downAction($event)"
tabindex="0">
<ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ol" [title]="'core.editor.orderedlist' | translate"
(click)="buttonAction($event, 'insertOrderedList')" (keyup)="buttonAction($event, 'insertOrderedList')"
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-list-ol" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'removeFormat')" (keyup)="buttonAction($event, 'removeFormat')"
(mousedown)="downAction($event)" (keydown)="downAction($event)" [title]="'core.editor.clear' | translate">
<ion-icon name="fas-eraser" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide *ngIf="canScanQR">
</swiper-slide>
<swiper-slide *ngIf="canScanQR">
<button [disabled]="!rteEnabled" (click)="scanQR($event)" (keyup)="scanQR($event)" (mousedown)="stopBubble($event)"
(keydown)="stopBubble($event)" [title]="'core.scanqr' | translate">
<ion-icon name="fas-qrcode" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
</swiper-slide>
<swiper-slide>
<button [attr.aria-pressed]="!rteEnabled" [title]="'core.editor.toggle' | translate" (click)="toggleEditor($event)"
(keyup)="toggleEditor($event)" (mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-code" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide *ngIf="isPhone">
</swiper-slide>
<swiper-slide *ngIf="isPhone">
<button [title]="'core.editor.hidetoolbar' | translate" (click)="hideToolbar($event, true)" (keyup)="hideToolbar($event, true)"
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-xmark" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
</ion-slides>
</swiper-slide>
</swiper-container>
<button *ngIf="toolbarArrows" class="toolbar-arrow" [attr.disabled]="toolbarNextHidden ? 'true' : null"
[attr.aria-label]="'core.next' | translate" (click)="toolbarNext($event)" (keyup)="toolbarNext($event)"
(mousedown)="downAction($event)" (keydown)="downAction($event)" [tabindex]="toolbarNextHidden ? -1 : 0">

View File

@ -108,7 +108,7 @@
padding-top: 5px;
border-top: 1px solid var(--stroke);
ion-slides {
swiper-container {
width: 240px;
flex-grow: 1;
flex-shrink: 1;

View File

@ -25,7 +25,7 @@ import {
AfterViewInit,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { IonTextarea, IonContent, IonSlides } from '@ionic/angular';
import { IonTextarea, IonContent, IonicSlides } from '@ionic/angular';
import { Subscription } from 'rxjs';
import { CoreSites } from '@services/sites';
@ -42,6 +42,8 @@ import { CoreScreen } from '@services/screen';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreDom } from '@singletons/dom';
import { CorePlatform } from '@services/platform';
import { Swiper } from 'swiper';
import { SwiperOptions } from 'swiper/types';
/**
* Component to display a rich text editor if enabled.
@ -78,7 +80,27 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
@ViewChild('editor') editor?: ElementRef; // WYSIWYG editor.
@ViewChild('textarea') textarea?: IonTextarea; // Textarea editor.
@ViewChild('toolbar') toolbar?: ElementRef;
@ViewChild(IonSlides) toolbarSlides?: IonSlides;
protected toolbarSlides?: 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.toolbarSlides = swiperRef.nativeElement.swiper as Swiper;
this.toolbarSlides.changeLanguageDirection(CorePlatform.isRTL ? 'rtl' : 'ltr');
Object.keys(this.swiperOpts).forEach((key) => {
if (this.toolbarSlides) {
this.toolbarSlides.params[key] = this.swiperOpts[key];
}
});
}
}, 0);
}
protected readonly DRAFT_AUTOSAVE_FREQUENCY = 30000;
protected readonly RESTORE_MESSAGE_CLEAR_TIME = 6000;
@ -119,7 +141,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
canScanQR = false;
ariaLabelledBy?: string;
infoMessage?: string;
direction = 'ltr';
toolbarStyles = {
strong: 'false',
em: 'false',
@ -133,11 +154,11 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
ol: 'false',
};
slidesOpts = {
initialSlide: 0,
swiperOpts: SwiperOptions = {
modules: [IonicSlides],
slidesPerView: 6,
centerInsufficientSlides: true,
watchSlidesVisibility: true,
watchSlidesProgress: true,
};
constructor(
@ -156,7 +177,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
this.canScanQR = CoreUtils.canScanQR();
this.isPhone = CoreScreen.isMobile;
this.toolbarHidden = this.isPhone;
this.direction = CorePlatform.isRTL ? 'rtl' : 'ltr';
}
/**
@ -265,7 +285,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
// Change the side when the language changes.
this.languageChangedSubscription = Translate.onLangChange.subscribe(() => {
setTimeout(() => {
this.direction = CorePlatform.isRTL ? 'rtl' : 'ltr';
this.toolbarSlides?.changeLanguageDirection(CorePlatform.isRTL ? 'rtl' : 'ltr');
});
});
}
@ -774,8 +794,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
this.stopBubble(event);
if (!this.toolbarNextHidden) {
const currentIndex = await this.toolbarSlides?.getActiveIndex();
this.toolbarSlides?.slideTo((currentIndex || 0) + this.slidesOpts.slidesPerView);
const currentIndex = this.toolbarSlides?.activeIndex;
this.toolbarSlides?.slideTo((currentIndex || 0) + this.toolbarSlides.slidesPerViewDynamic());
}
await this.updateToolbarArrows();
@ -792,8 +812,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
this.stopBubble(event);
if (!this.toolbarPrevHidden) {
const currentIndex = await this.toolbarSlides?.getActiveIndex();
this.toolbarSlides?.slideTo((currentIndex || 0) - this.slidesOpts.slidesPerView);
const currentIndex = this.toolbarSlides?.activeIndex;
this.toolbarSlides?.slideTo((currentIndex || 0) - this.toolbarSlides.slidesPerViewDynamic());
}
await this.updateToolbarArrows();
@ -808,7 +828,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
return;
}
const length = await this.toolbarSlides.length();
const length = this.toolbarSlides.slides.length;
// Cancel previous one, if any.
this.buttonsDomPromise?.cancel();
@ -818,17 +838,16 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
const width = this.toolbar.nativeElement.getBoundingClientRect().width;
if (length > 0 && width > length * this.toolbarButtonWidth) {
this.slidesOpts = { ...this.slidesOpts, slidesPerView: length };
this.swiperOpts.slidesPerView = length;
this.toolbarArrows = false;
} else {
const slidesPerView = Math.floor((width - this.toolbarArrowWidth * 2) / this.toolbarButtonWidth);
this.slidesOpts = { ...this.slidesOpts, slidesPerView };
this.swiperOpts.slidesPerView = Math.floor((width - this.toolbarArrowWidth * 2) / this.toolbarButtonWidth);
this.toolbarArrows = true;
}
await CoreUtils.nextTick();
await this.toolbarSlides.update();
this.toolbarSlides.update();
await this.updateToolbarArrows();
}
@ -841,10 +860,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
return;
}
const currentIndex = await this.toolbarSlides.getActiveIndex();
const length = await this.toolbarSlides.length();
const currentIndex = this.toolbarSlides.activeIndex;
const length = this.toolbarSlides.slides.length;
this.toolbarPrevHidden = currentIndex <= 0;
this.toolbarNextHidden = currentIndex + this.slidesOpts.slidesPerView >= length;
this.toolbarNextHidden = currentIndex + this.toolbarSlides.slidesPerViewDynamic() >= length;
}
/**

View File

@ -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 { CoreSharedModule } from '@/core/shared.module';
import { CoreViewerImageComponent } from './image/image';
@ -33,5 +33,6 @@ import { CoreViewerTextComponent } from './text/text';
CoreViewerQRScannerComponent,
CoreViewerTextComponent,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class CoreViewerComponentsModule {}

View File

@ -1,11 +1,11 @@
<ion-content>
<ion-slides [options]="slidesOpts">
<ion-slide>
<swiper-container #swiperRef>
<swiper-slide>
<div class="swiper-zoom-container">
<img [src]="image" [alt]="title" core-external-content [component]="component" [componentId]="componentId">
</div>
</ion-slide>
</ion-slides>
</swiper-slide>
</swiper-container>
</ion-content>
<ion-footer>
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding">
@ -15,12 +15,12 @@
</ion-button>
</ion-col>
<ion-col></ion-col>
<ion-col size="auto" *ngIf="slidesSwiper">
<ion-col size="auto" *ngIf="swiper">
<ion-button fill="clear" [attr.aria-label]="'core.zoomout' | translate" (click)="zoom(false)">
<ion-icon name="fas-magnifying-glass-minus" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-col>
<ion-col size="auto" *ngIf="slidesSwiper">
<ion-col size="auto" *ngIf="swiper">
<ion-button fill="clear" [attr.aria-label]="'core.zoomin' | translate" (click)="zoom(true)">
<ion-icon name="fas-magnifying-glass-plus" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>

View File

@ -1,4 +1,4 @@
ion-slides {
swiper-container {
height: 100%;
}

View File

@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { IonSlides } from '@ionic/angular';
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { ModalController, Translate } from '@singletons';
import { CoreMath } from '@singletons/math';
import { Swiper } from 'swiper';
import { SwiperOptions } from 'swiper/types';
import { IonicSlides } from '@ionic/angular';
/**
* Modal component to view an image.
@ -25,29 +27,49 @@ import { CoreMath } from '@singletons/math';
templateUrl: 'image.html',
styleUrls: ['image.scss'],
})
export class CoreViewerImageComponent implements OnInit, AfterViewInit {
export class CoreViewerImageComponent implements OnInit {
@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 = swiperRef.nativeElement.swiper as Swiper;
Object.keys(this.swiperOpts).forEach((key) => {
if (this.swiper) {
this.swiper.params[key] = this.swiperOpts[key];
}
});
}
}, 0);
}
@Input() title = ''; // Modal title.
@Input() image = ''; // Image URL.
@Input() component?: string; // Component to use in external-content.
@Input() componentId?: string | number; // Component ID to use in external-content.
slidesOpts = {
private static readonly MAX_RATIO = 8;
protected swiperOpts: SwiperOptions = {
modules: [IonicSlides],
freeMode: true,
slidesPerView: 1,
centerInsufficientSlides: true,
centerSlides: true,
centeredSlides: true,
zoom: {
maxRatio: 8,
maxRatio: CoreViewerImageComponent.MAX_RATIO,
minRatio: 0.5, // User can zoom out to 0.5 only using pinch gesture.
},
};
protected zoomRatio = 1;
slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any
constructor(protected element: ElementRef<HTMLElement>) {
}
@ -58,12 +80,6 @@ export class CoreViewerImageComponent implements OnInit, AfterViewInit {
this.title = this.title || Translate.instant('core.imageviewer');
}
/**
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
this.slidesSwiper = await this.slides?.getSwiper();
}
/**
* Close modal.
@ -80,7 +96,7 @@ export class CoreViewerImageComponent implements OnInit, AfterViewInit {
zoom(zoomIn = true): void {
const imageElement = this.element.nativeElement.querySelector('img');
if (!this.slidesSwiper || !imageElement) {
if (!this.swiper || !imageElement) {
return;
}
@ -89,15 +105,15 @@ export class CoreViewerImageComponent implements OnInit, AfterViewInit {
: this.zoomRatio /= 2;
// Using 1 as minimum for manual zoom.
this.zoomRatio = CoreMath.clamp(this.zoomRatio, 1, this.slidesOpts.zoom.maxRatio);
this.zoomRatio = CoreMath.clamp(this.zoomRatio, 1, CoreViewerImageComponent.MAX_RATIO);
if (this.zoomRatio > 1) {
this.slidesSwiper.zoom.in();
this.swiper.zoom.in();
imageElement.style.transform =
'translate3d(0px, 0px, 0px) scale(' + this.zoomRatio + ')';
} else {
this.slidesSwiper.zoom.out();
this.swiper.zoom.out();
}
}

View File

@ -204,7 +204,7 @@ export class TestingBehatBlockingService {
return false;
}
const slide = element.closest('ion-slide');
const slide = element.closest('swiper-slide');
if (slide && !slide.classList.contains('swiper-slide-active')) {
return false;
}

View File

@ -48,7 +48,7 @@ export class TestingBehatDomUtilsService {
return false;
}
if (element.tagName === 'ION-SLIDE') {
if (element.tagName === 'SWIPER-SLIDE') {
// Check if the slide is visible (in the viewport).
const bounding = element.getBoundingClientRect();
if (bounding.right <= 0 || bounding.left >= window.innerWidth) {

View File

@ -29,7 +29,7 @@ import { Injectable } from '@angular/core';
import { CoreSites, CoreSitesProvider } from '@services/sites';
import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
import { CoreSwipeNavigationDirective } from '@directives/swipe-navigation';
import { IonSlides } from '@ionic/angular';
import { Swiper } from 'swiper';
/**
* Behat runtime servive with public API.
@ -631,8 +631,8 @@ export class TestingBehatRuntimeService {
this.log('Action - Swipe', { direction, locator });
if (locator) {
// Locator specified, try to find ion-slides first.
const instance = this.getAngularInstance<IonSlides>('ion-slides', 'IonSlides', locator);
// Locator specified, try to find swiper-container first.
const instance = this.getAngularInstance<Swiper>('swiper-container', 'Swiper', locator);
if (instance) {
direction === 'left' ? instance.slideNext() : instance.slidePrev();
@ -640,7 +640,7 @@ export class TestingBehatRuntimeService {
}
}
// No locator specified or ion-slides not found, search swipe navigation now.
// No locator specified or swiper-container not found, search swipe navigation now.
const instance = this.getAngularInstance<CoreSwipeNavigationDirective>(
'ion-content',
'CoreSwipeNavigationDirective',

View File

@ -151,7 +151,7 @@ ion-toolbar {
// Header.
ion-header {
z-index: 12; // To hide ion-slides on scroll.
z-index: 12; // To hide swiper-container on scroll.
ion-toolbar {
ion-spinner {
@ -1807,13 +1807,53 @@ ion-header.no-title {
flex-direction: column;
flex-grow: 1;
ion-slides {
swiper-container {
flex-grow: 1;
max-width: 100%;
}
}
}
swiper-container {
--swiper-theme-color: var(--ion-color-primary, #3880ff);
--swiper-pagination-bullet-inactive-color: var(--ion-color-step-200, #cccccc);
--swiper-pagination-color: var(--swiper-theme-color);
--swiper-pagination-progressbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.25);
--swiper-scrollbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.1);
--swiper-scrollbar-drag-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.5);
width: 100%;
max-width: 100%;
max-height: 100vh;
// CSS Grid/Flexbox bug size workaround
// @see https://github.com/kenwheeler/slick/issues/982
// @see https://github.com/nolimits4web/swiper/issues/3599
min-height: 0;
min-width: 0;
swiper-slide {
display: flex;
position: relative;
flex-direction: column;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: 18px;
text-align: center;
box-sizing: border-box;
img {
width: auto;
max-width: 100%;
height: auto;
max-height: 100%;
}
}
}
.has-spacer,

View File

@ -247,7 +247,7 @@ html {
--core-tab-font-weight-active: normal;
--core-tabs-height: 48px;
core-tabs, core-tabs-outlet {
ion-slide {
swiper-slide {
--background: var(--core-tab-background);
--color: var(--core-tab-color);
--border-color: var(--core-tab-border-color);

View File

@ -8,6 +8,7 @@ For more information about upgrading, read the official documentation: https://m
- CoreCache has been deprecated, use plain object as in-memory stores instead.
- Renamed CoreLoginSitesComponent to CoreLoginSitesModalComponent to make it clear that it's a modal and to avoid confusing it with the new CoreSitesListComponent.
- Removed CoreToLocaleStringPipe deprecated since 3.6.0
- With the upgrade to Ionic 7 ion-slides is no longer supported and now you need to use swiper-container and swiper-slide. More info here: https://ionicframework.com/docs/angular/slides
=== 4.3.0 ===