From 9d645fe2545fa8fe87a67cfa28f0d9071ed82aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 21 Dec 2021 16:46:31 +0100 Subject: [PATCH] MOBILE-3833 swipe: Change swipe navigation component to directive --- .../swipe-navigation-items-manager.ts | 18 ++ src/core/classes/page-transition.ts | 26 ++- src/core/components/components.module.ts | 3 - .../swipe-navigation/swipe-navigation.html | 5 - .../swipe-navigation/swipe-navigation.scss | 24 --- .../swipe-navigation/swipe-navigation.ts | 54 ------ src/core/directives/directives.module.ts | 3 + src/core/directives/swipe-navigation.ts | 158 ++++++++++++++++++ 8 files changed, 199 insertions(+), 92 deletions(-) delete mode 100644 src/core/components/swipe-navigation/swipe-navigation.html delete mode 100644 src/core/components/swipe-navigation/swipe-navigation.scss delete mode 100644 src/core/components/swipe-navigation/swipe-navigation.ts create mode 100644 src/core/directives/swipe-navigation.ts diff --git a/src/core/classes/items-management/swipe-navigation-items-manager.ts b/src/core/classes/items-management/swipe-navigation-items-manager.ts index 57b1dd2c7..2548b7b7d 100644 --- a/src/core/classes/items-management/swipe-navigation-items-manager.ts +++ b/src/core/classes/items-management/swipe-navigation-items-manager.ts @@ -49,6 +49,24 @@ export class CoreSwipeNavigationItemsManager< await this.navigateToItemBy(1, 'forward'); } + /** + * Has a next item. + */ + async hasNextItem(): Promise { + const item = await this.getItemBy(-1); + + return !!item; + } + + /** + * Has a previous item. + */ + async hasPreviousItem(): Promise { + const item = await this.getItemBy(1); + + return !!item; + } + /** * @inheritdoc */ diff --git a/src/core/classes/page-transition.ts b/src/core/classes/page-transition.ts index 7443f6c55..4ae2d83c9 100644 --- a/src/core/classes/page-transition.ts +++ b/src/core/classes/page-transition.ts @@ -229,11 +229,19 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp rootAnimation.addAnimation(leavingContent); + // Check if leaving content is being translated using transform styles and decide to use fromTo or only To animation. + const hasTransformStyle = !!leavingContentEl && (leavingContentEl as HTMLElement).style.transform !== ''; if (backDirection) { // leaving content, back direction - leavingContent - .beforeClearStyles([OPACITY]) - .fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)')); + if (hasTransformStyle) { + leavingContent + .to('transform', (isRTL ? 'translateX(-100%)' : 'translateX(100%)')) + .fromTo(OPACITY, 1, OFF_OPACITY); + } else { + leavingContent + .beforeClearStyles([OPACITY]) + .fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)')); + } const leavingPage = getIonPageElement(leavingEl) as HTMLElement; rootAnimation.afterAddWrite(() => { @@ -244,9 +252,15 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp } else { // leaving content, forward direction - leavingContent - .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`) - .fromTo(OPACITY, 1, OFF_OPACITY); + if (hasTransformStyle) { + leavingContent + .to('transform', (isRTL ? 'translateX(100%)' : 'translateX(-100%)')) + .fromTo(OPACITY, 1, OFF_OPACITY); + } else { + leavingContent + .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`) + .fromTo(OPACITY, 1, OFF_OPACITY); + } } if (leavingContentEl) { diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts index ae31faa41..0bfb96dc3 100644 --- a/src/core/components/components.module.ts +++ b/src/core/components/components.module.ts @@ -50,7 +50,6 @@ import { CoreShowPasswordComponent } from './show-password/show-password'; import { CoreSitePickerComponent } from './site-picker/site-picker'; import { CoreSplitViewComponent } from './split-view/split-view'; import { CoreStyleComponent } from './style/style'; -import { CoreSwipeNavigationComponent } from './swipe-navigation/swipe-navigation'; import { CoreTabComponent } from './tabs/tab'; import { CoreTabsComponent } from './tabs/tabs'; import { CoreTabsOutletComponent } from './tabs-outlet/tabs-outlet'; @@ -94,7 +93,6 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; CoreSitePickerComponent, CoreSplitViewComponent, CoreStyleComponent, - CoreSwipeNavigationComponent, CoreSwipeSlidesComponent, CoreTabComponent, CoreTabsComponent, @@ -144,7 +142,6 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; CoreSitePickerComponent, CoreSplitViewComponent, CoreStyleComponent, - CoreSwipeNavigationComponent, CoreSwipeSlidesComponent, CoreTabComponent, CoreTabsComponent, diff --git a/src/core/components/swipe-navigation/swipe-navigation.html b/src/core/components/swipe-navigation/swipe-navigation.html deleted file mode 100644 index 1a47ecb89..000000000 --- a/src/core/components/swipe-navigation/swipe-navigation.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/core/components/swipe-navigation/swipe-navigation.scss b/src/core/components/swipe-navigation/swipe-navigation.scss deleted file mode 100644 index e1bd0f739..000000000 --- a/src/core/components/swipe-navigation/swipe-navigation.scss +++ /dev/null @@ -1,24 +0,0 @@ -ion-slides { - min-height: 100%; -} - -ion-slide { - align-items: start; -} - -:host ::ng-deep { - - .swiper-wrapper { - position: absolute; - } - - core-loading .core-loading-content { - display: block !important; - width: 100%; - } - - ion-refresher.refresher-native { - z-index: 2; - } - -} diff --git a/src/core/components/swipe-navigation/swipe-navigation.ts b/src/core/components/swipe-navigation/swipe-navigation.ts deleted file mode 100644 index c26e0ab67..000000000 --- a/src/core/components/swipe-navigation/swipe-navigation.ts +++ /dev/null @@ -1,54 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Component, Input } from '@angular/core'; -import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; -import { CoreScreen } from '@services/screen'; - -@Component({ - selector: 'core-swipe-navigation', - templateUrl: 'swipe-navigation.html', - styleUrls: ['swipe-navigation.scss'], -}) -export class CoreSwipeNavigationComponent { - - @Input() manager?: CoreSwipeNavigationItemsManager; - - get enabled(): boolean { - return CoreScreen.isMobile && !!this.manager; - } - - /** - * Swipe to previous item. - */ - swipeLeft(): void { - if (!this.enabled) { - return; - } - - this.manager?.navigateToPreviousItem(); - } - - /** - * Swipe to next item. - */ - swipeRight(): void { - if (!this.enabled) { - return; - } - - this.manager?.navigateToNextItem(); - } - -} diff --git a/src/core/directives/directives.module.ts b/src/core/directives/directives.module.ts index e1d6dc959..018ddafad 100644 --- a/src/core/directives/directives.module.ts +++ b/src/core/directives/directives.module.ts @@ -28,6 +28,7 @@ import { CoreAriaButtonClickDirective } from './aria-button'; import { CoreOnResizeDirective } from './on-resize'; import { CoreDownloadFileDirective } from './download-file'; import { CoreCollapsibleHeaderDirective } from './collapsible-header'; +import { CoreSwipeNavigationDirective } from './swipe-navigation'; @NgModule({ declarations: [ @@ -45,6 +46,7 @@ import { CoreCollapsibleHeaderDirective } from './collapsible-header'; CoreOnResizeDirective, CoreDownloadFileDirective, CoreCollapsibleHeaderDirective, + CoreSwipeNavigationDirective, ], exports: [ CoreAutoFocusDirective, @@ -61,6 +63,7 @@ import { CoreCollapsibleHeaderDirective } from './collapsible-header'; CoreOnResizeDirective, CoreDownloadFileDirective, CoreCollapsibleHeaderDirective, + CoreSwipeNavigationDirective, ], }) export class CoreDirectivesModule {} diff --git a/src/core/directives/swipe-navigation.ts b/src/core/directives/swipe-navigation.ts new file mode 100644 index 000000000..31caacaef --- /dev/null +++ b/src/core/directives/swipe-navigation.ts @@ -0,0 +1,158 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular/core'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; +import { Gesture } from '@ionic/angular'; +import { CoreScreen } from '@services/screen'; +import { GestureController } from '@singletons'; + +const ACTIVATION_THRESHOLD = 150; +const SWIPE_FRICTION = 0.6; + +/** + * Directive to enable content navigation with swipe. + * + * Example usage: + * + * + */ +@Directive({ + selector: 'ion-content[core-swipe-navigation]', +}) +export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy { + + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input('core-swipe-navigation') manager?: CoreSwipeNavigationItemsManager; + + protected element: HTMLElement; + protected swipeGesture?: Gesture; + + constructor(el: ElementRef) { + this.element = el.nativeElement; + } + + get enabled(): boolean { + return CoreScreen.isMobile && !!this.manager; + } + + /** + * @inheritdoc + */ + ngAfterViewInit(): void { + const style = this.element.style; + this.swipeGesture = GestureController.create({ + el: this.element, + gestureName: 'swipe', + threshold: 10, + gesturePriority: 10, + canStart: () => this.enabled, + onStart: () => { + style.transition = ''; + }, + onMove: (ev) => { + style.transform = `translateX(${ev.deltaX * SWIPE_FRICTION }px)`; + }, + onEnd: (ev) => { + style.transition = '.5s ease-out'; + + if (ev.deltaX > ACTIVATION_THRESHOLD) { + this.manager?.hasNextItem().then((hasNext) => { + if (hasNext) { + this.preventClickOnElement(); + + style.transform = 'translateX(100%) !important'; + this.swipeRight(); + } else { + style.transform = ''; + } + + return; + }); + + return; + } + + if (ev.deltaX < -ACTIVATION_THRESHOLD) { + this.manager?.hasPreviousItem().then((hasPrevious) => { + if (hasPrevious) { + + this.preventClickOnElement(); + + style.transform = 'translateX(-100%) !important'; + this.swipeLeft(); + } else { + style.transform = ''; + } + + return; + }); + + return; + } + + style.transform = ''; + + }, + }); + this.swipeGesture.enable(); + } + + /** + * Swipe to previous item. + */ + swipeLeft(): void { + if (!this.enabled) { + return; + } + + this.manager?.navigateToPreviousItem(); + } + + /** + * Swipe to next item. + */ + swipeRight(): void { + if (!this.enabled) { + return; + } + + this.manager?.navigateToNextItem(); + } + + /** + * Prevent click event by capturing the click before happening. + */ + protected preventClickOnElement(): void { + this.element.addEventListener( + 'click', + (ev: Event) => { + ev.preventDefault(); + ev.stopPropagation(); + + return false; + }, + true, // Register event on the capture phase. + ); + + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.swipeGesture?.destroy(); + } + +}