MOBILE-3947 slides: Use Swiper instead of IonSlides
parent
34147fceb7
commit
7c31e79bbd
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -142,7 +142,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
ion-slide {
|
||||
swiper-slide {
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
justify-content: start;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
ion-slides {
|
||||
swiper-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ===
|
||||
|
||||
|
|
Loading…
Reference in New Issue