diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 580a431e8c..c2314a1078 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -1222,6 +1222,7 @@ declare global { isRightSide?: any, width?: any, + fireSwipeEvent?: any, side?: string } } @@ -1252,7 +1253,10 @@ declare global { mode?: string, color?: string, - close?: any + getOpenAmount?: any, + getSlidingPercent?: any, + close?: any, + closeOpened?: any } } } diff --git a/packages/core/src/components/item-sliding/item-option.tsx b/packages/core/src/components/item-sliding/item-option.tsx index 8d06996366..af4c1a26fb 100644 --- a/packages/core/src/components/item-sliding/item-option.tsx +++ b/packages/core/src/components/item-sliding/item-option.tsx @@ -1,6 +1,4 @@ import { Component, Prop } from '@stencil/core'; -import { createThemedClasses } from '../../utils/theme'; - /** * @name ItemOption @@ -10,7 +8,10 @@ import { createThemedClasses } from '../../utils/theme'; * action for the item. */ @Component({ - tag: 'ion-item-option' + tag: 'ion-item-option', + host: { + theme: 'item-option' + } }) export class ItemOption { mode: string; @@ -35,18 +36,14 @@ export class ItemOption { } protected render() { - const themedClasses = createThemedClasses(this.mode, this.color, 'item-option-button'); const TagType = this.href ? 'a' : 'button'; - - return ( - - - - -
-
- ); + return [ + , + + + + ]; } } diff --git a/packages/core/src/components/item-sliding/item-options.tsx b/packages/core/src/components/item-sliding/item-options.tsx index ffcd65cc66..aa4d219766 100644 --- a/packages/core/src/components/item-sliding/item-options.tsx +++ b/packages/core/src/components/item-sliding/item-options.tsx @@ -40,15 +40,21 @@ export class ItemOptions { */ @Event() ionSwipe: EventEmitter; - @Method() isRightSide() { - const isRTL = document.dir === 'rtl'; - return isRightSide(this.side, isRTL, true); + @Method() + isRightSide() { + return isRightSide(this.side, true); } - @Method() width(): number { + @Method() + width(): number { return this.el.offsetWidth; } + @Method() + fireSwipeEvent(value: any) { + this.ionSwipe.emit(value); + } + protected render() { return ; } diff --git a/packages/core/src/components/item-sliding/item-sliding.ios.scss b/packages/core/src/components/item-sliding/item-sliding.ios.scss index eafcaef801..106f129a80 100644 --- a/packages/core/src/components/item-sliding/item-sliding.ios.scss +++ b/packages/core/src/components/item-sliding/item-sliding.ios.scss @@ -29,13 +29,13 @@ $item-ios-sliding-button-icon-color: color-contrast($colors-ios, $item-i border-bottom: $hairlines-width solid $list-ios-border-color; } -.item-option-button-ios { +.item-option-ios { font-size: $item-ios-sliding-button-font-size; color: $item-ios-sliding-button-text-color; background-color: $item-ios-sliding-button-background-color; } -.item-option-button-ios .icon { +.item-option-ios .icon { fill: $item-ios-sliding-button-icon-color; } @@ -53,12 +53,12 @@ $item-ios-sliding-button-icon-color: color-contrast($colors-ios, $item-i @each $color-name, $color-base, $color-contrast in get-colors($colors-ios) { - .item-option-button-ios-#{$color-name} { + .item-option-ios-#{$color-name} { color: $color-contrast; background-color: $color-base; } - .item-option-button-ios-#{$color-name} .icon { + .item-option-ios-#{$color-name} .icon { fill: $color-contrast; } diff --git a/packages/core/src/components/item-sliding/item-sliding.md.scss b/packages/core/src/components/item-sliding/item-sliding.md.scss index 147f85a0ed..7e3693f684 100644 --- a/packages/core/src/components/item-sliding/item-sliding.md.scss +++ b/packages/core/src/components/item-sliding/item-sliding.md.scss @@ -29,13 +29,13 @@ $item-md-sliding-button-icon-color: color-contrast($colors-md, $item-md border-bottom: 1px solid $list-md-border-color; } -.item-option-button-md { +.item-option-md { font-size: $item-md-sliding-button-font-size; color: $item-md-sliding-button-text-color; background-color: $item-md-sliding-button-background-color; } -.item-option-button-md .icon { +.item-option-md .icon { fill: $item-md-sliding-button-icon-color; } @@ -52,12 +52,12 @@ $item-md-sliding-button-icon-color: color-contrast($colors-md, $item-md @each $color-name, $color-base, $color-contrast in get-colors($colors-md) { - .item-option-button-md-#{$color-name} { + .item-option-md-#{$color-name} { color: $color-contrast; background-color: $color-base; } - .item-option-button-md-#{$color-name} .icon { + .item-option-md-#{$color-name} .icon { fill: $color-contrast; } diff --git a/packages/core/src/components/item-sliding/item-sliding.scss b/packages/core/src/components/item-sliding/item-sliding.scss index 618975587e..988d30cd08 100644 --- a/packages/core/src/components/item-sliding/item-sliding.scss +++ b/packages/core/src/components/item-sliding/item-sliding.scss @@ -2,7 +2,7 @@ // Item Sliding // -------------------------------------------------- -// The hidden buttons that can be exposed under a list item by dragging +// The hidden right-side buttons that can be exposed under a list item with dragging. ion-item-sliding { position: relative; @@ -12,10 +12,6 @@ ion-item-sliding { width: 100%; } -ion-item-sliding ion-item { - position: static; -} - ion-item-options { position: absolute; z-index: $z-index-item-options; @@ -66,29 +62,31 @@ ion-item-options[side=left] { } } -.item-option-button { - @include margin(0); - @include border-radius(0); +ion-item-option { @include padding(0, .7em); - display: inline-flex; + position: relative; + display: flex; align-items: center; - height: 100%; + min-width: 6rem; +} + +.item-option-button { + @include position(0, 0, 0, 0); + @include margin(0); + @include padding(0); + @include border-radius(0); + + position: absolute; border: 0; - box-shadow: none; - - box-sizing: border-box; + background: none; } -.item-option-button::before { - @include margin(0, auto); -} - -ion-item-options:not([icon-start]) .item-option-button:not([icon-only]) { +ion-item-options:not([icon-start]) ion-item-option:not([icon-only]) { .button-inner { flex-direction: column; } @@ -131,7 +129,7 @@ ion-item-sliding.active-slide { // Item Expandable Animation // -------------------------------------------------- -ion-item-option[expandable] .item-option-button { +ion-item-option[expandable] { flex-shrink: 0; transition-duration: 0; @@ -139,7 +137,7 @@ ion-item-option[expandable] .item-option-button { transition-timing-function: cubic-bezier(.65, .05, .36, 1); } -ion-item-sliding.active-swipe-right ion-item-option[expandable] .item-option-button { +ion-item-sliding.active-swipe-right ion-item-option[expandable] { transition-duration: .6s; transition-property: padding-left; @@ -157,7 +155,7 @@ ion-item-sliding.active-swipe-right ion-item-option[expandable] .item-option-but } } -ion-item-sliding.active-swipe-left ion-item-option[expandable] .item-option-button { +ion-item-sliding.active-swipe-left ion-item-option[expandable] { transition-duration: .6s; transition-property: padding-right; diff --git a/packages/core/src/components/item-sliding/item-sliding.tsx b/packages/core/src/components/item-sliding/item-sliding.tsx index 10fe41c4df..d3f329ca72 100644 --- a/packages/core/src/components/item-sliding/item-sliding.tsx +++ b/packages/core/src/components/item-sliding/item-sliding.tsx @@ -8,11 +8,12 @@ import { ItemOptions } from './item-options'; const SWIPE_MARGIN = 30; const ELASTIC_FACTOR = 0.55; -const ITEM_SIDE_FLAG_NONE = 0; -const ITEM_SIDE_FLAG_LEFT = 1 << 0; -const ITEM_SIDE_FLAG_RIGHT = 1 << 1; -const ITEM_SIDE_FLAG_BOTH = ITEM_SIDE_FLAG_LEFT | ITEM_SIDE_FLAG_RIGHT; - +const enum ItemSide { + None = 0, + Left = 1 << 0, + Right = 1 << 1, + Both = Left | Right +} const enum SlidingState { Disabled = 1 << 1, @@ -135,30 +136,23 @@ const enum SlidingState { } }) export class ItemSliding { - @Element() private el: HTMLElement; private item: HTMLIonItemElement; private list: HTMLIonListElement; - - private openAmount: number = 0; - private startX: number = 0; - private optsWidthRightSide: number = 0; - private optsWidthLeftSide: number = 0; - private sides: number; + private openAmount = 0; + private initialOpenAmount = 0; + private optsWidthRightSide = 0; + private optsWidthLeftSide = 0; + private sides: ItemSide; private tmr: any = null; - private leftOptions: ItemOptions; private rightOptions: ItemOptions; - private optsDirty: boolean = true; + private gestureOptions: any; + @Element() private el: HTMLElement; @State() state: SlidingState = SlidingState.Disabled; - private preSelectedContainer: ItemSliding = null; - private selectedContainer: ItemSliding = null; - openContainer: ItemSliding = null; - private firstCoordX: number; - private firstTimestamp: number; /** * @output {event} Emitted when the sliding position changes. @@ -183,92 +177,39 @@ export class ItemSliding { */ @Event() ionDrag: EventEmitter; + constructor() { + this.gestureOptions = { + 'canStart': this.canStart.bind(this), + 'onStart': this.onDragStart.bind(this), + 'onMove': this.onDragMove.bind(this), + 'onEnd': this.onDragEnd.bind(this), + 'gestureName': 'item-swipe', + 'gesturePriority': -10, + 'type': 'pan', + 'direction': 'x', + 'maxAngle': 20, + 'threshold': 5, + 'attachTo': 'parent' + }; + } + protected ionViewDidLoad() { - const options = this.el.querySelectorAll('ion-item-options'); - - let sides = 0; - - // Reset left and right options in case they were removed - this.leftOptions = this.rightOptions = null; - - for (var i = 0; i < options.length; i++) { - let option = options[i]; - - if (option.isRightSide()) { - this.rightOptions = option; - sides |= ITEM_SIDE_FLAG_RIGHT; - } else { - this.leftOptions = option; - sides |= ITEM_SIDE_FLAG_LEFT; - } - } - this.optsDirty = true; - this.sides = sides; - this.item = this.el.querySelector('ion-item'); - - // Get the parent list to close open containers this.list = this.el.closest('ion-list') as HTMLIonListElement; + + this.updateOptions(); } - canStart(gesture: GestureDetail): boolean { - if (this.selectedContainer) { - return false; - } - // Get swiped sliding container - let container = this; - - // Close open container if it is not the selected one. - if (this.list && container !== this.list.getOpenedItem()) { - this.closeOpened(); - } - - this.preSelectedContainer = container; - this.firstCoordX = gesture.currentX; - this.firstTimestamp = Date.now(); - - return true; + protected ionViewDidUnLoad() { + this.item = this.list = this.leftOptions = this.rightOptions = null; } - onDragStart(gesture: GestureDetail) { - this.selectedContainer = this.preSelectedContainer; - this.list.setOpenedItem(this.selectedContainer); - this.selectedContainer.startSliding(gesture.currentX); - } - - onDragMove(gesture: GestureDetail) { - this.selectedContainer && this.selectedContainer.moveSliding(gesture.currentX); - } - - onDragEnd(gesture: GestureDetail) { - let coordX = gesture.currentX; - let deltaX = (coordX - this.firstCoordX); - let deltaT = (Date.now() - this.firstTimestamp); - this.selectedContainer.endSliding(deltaX / deltaT); - this.selectedContainer = null; - this.preSelectedContainer = null; - } - - closeOpened(): boolean { - this.selectedContainer = null; - - if (this.list.getOpenedItem()) { - this.list.closeSlidingItems(); - return true; - } - return false; - } - - /** - * @hidden - */ + @Method() getOpenAmount(): number { return this.openAmount; } - /** - * @hidden - */ + @Method() getSlidingPercent(): number { const openAmount = this.openAmount; if (openAmount > 0) { @@ -280,156 +221,6 @@ export class ItemSliding { } } - /** - * @hidden - */ - startSliding(startX: number) { - if (this.tmr) { - clearTimeout(this.tmr); - this.tmr = null; - } - if (this.openAmount === 0) { - this.optsDirty = true; - this.setState(SlidingState.Enabled); - } - this.startX = startX + this.openAmount; - this.item.style.transition = 'none'; - } - - /** - * @hidden - */ - moveSliding(x: number): number { - if (this.optsDirty) { - this.calculateOptsWidth(); - return 0; - } - - let openAmount = (this.startX - x); - - switch (this.sides) { - case ITEM_SIDE_FLAG_RIGHT: openAmount = Math.max(0, openAmount); break; - case ITEM_SIDE_FLAG_LEFT: openAmount = Math.min(0, openAmount); break; - case ITEM_SIDE_FLAG_BOTH: break; - case ITEM_SIDE_FLAG_NONE: return 0; - default: console.warn('invalid ItemSideFlags value', this.sides); break; - } - - let optsWidth; - if (openAmount > this.optsWidthRightSide) { - optsWidth = this.optsWidthRightSide; - openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR; - - } else if (openAmount < -this.optsWidthLeftSide) { - optsWidth = -this.optsWidthLeftSide; - openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR; - } - - this.setOpenAmount(openAmount, false); - return openAmount; - } - - /** - * @hidden - */ - endSliding(velocity: number): number { - let restingPoint = (this.openAmount > 0) - ? this.optsWidthRightSide - : -this.optsWidthLeftSide; - - // Check if the drag didn't clear the buttons mid-point - // and we aren't moving fast enough to swipe open - const isResetDirection = (this.openAmount > 0) === !(velocity < 0); - const isMovingFast = Math.abs(velocity) > 0.3; - const isOnCloseZone = Math.abs(this.openAmount) < Math.abs(restingPoint / 2); - if (swipeShouldReset(isResetDirection, isMovingFast, isOnCloseZone)) { - restingPoint = 0; - } - - this.setOpenAmount(restingPoint, true); - this.fireSwipeEvent(); - return restingPoint; - } - - /** - * @hidden - * Emit the ionSwipe event on the child options - */ - fireSwipeEvent() { - if (this.state & SlidingState.SwipeRight) { - this.rightOptions.ionSwipe.emit(this); - } else if (this.state & SlidingState.SwipeLeft) { - this.leftOptions.ionSwipe.emit(this); - } - } - - /** - * @hidden - */ - calculateOptsWidth() { - if (!this.optsDirty) { - return; - } - this.optsWidthRightSide = 0; - if (this.rightOptions) { - this.optsWidthRightSide = this.rightOptions.width(); - this.optsWidthRightSide === 0 && console.warn('optsWidthRightSide should not be zero'); - } - - this.optsWidthLeftSide = 0; - if (this.leftOptions) { - this.optsWidthLeftSide = this.leftOptions.width(); - this.optsWidthLeftSide === 0 && console.warn('optsWidthLeftSide should not be zero'); - } - this.optsDirty = false; - } - - setOpenAmount(openAmount: number, isFinal: boolean) { - if (this.tmr) { - clearTimeout(this.tmr); - this.tmr = null; - } - this.openAmount = openAmount; - - if (isFinal) { - this.item.style.transition = ''; - - } else { - var state; - if (openAmount > 0) { - state = (openAmount >= (this.optsWidthRightSide + SWIPE_MARGIN)) - ? SlidingState.Right | SlidingState.SwipeRight - : SlidingState.Right; - - this.setState(state); - - } else if (openAmount < 0) { - state = (openAmount <= (-this.optsWidthLeftSide - SWIPE_MARGIN)) - ? SlidingState.Left | SlidingState.SwipeLeft - : SlidingState.Left; - - this.setState(state); - } - } - if (openAmount === 0) { - this.tmr = setTimeout(() => { - this.setState(SlidingState.Disabled); - this.tmr = null; - }, 600); - this.item.style.transform = ''; - return; - } - - this.item.style.transform = `translate3d(${-openAmount}px,0,0)`; - this.ionDrag.emit(); - } - - private setState(state: SlidingState) { - if (state === this.state) { - return; - } - this.state = state; - } /** * Close the sliding item. Items can also be closed from the [List](../../list/List). @@ -470,7 +261,160 @@ export class ItemSliding { this.setOpenAmount(0, true); } - hostData() { + @Method() + closeOpened(): boolean { + return this.list && this.list.closeSlidingItems(); + } + + private updateOptions() { + const options = this.el.querySelectorAll('ion-item-options'); + + let sides = 0; + + // Reset left and right options in case they were removed + this.leftOptions = this.rightOptions = null; + + for (var i = 0; i < options.length; i++) { + let option = options.item(i); + + if (option.isRightSide()) { + this.rightOptions = option; + sides |= ItemSide.Right; + } else { + this.leftOptions = option; + sides |= ItemSide.Left; + } + } + this.optsDirty = true; + this.sides = sides; + } + + private canStart(): boolean { + const selected = this.list && this.list.getOpenedItem(); + if (selected && selected !== this) { + this.closeOpened(); + return false; + } + return true; + } + + private onDragStart() { + this.list && this.list.setOpenedItem(this); + + if (this.tmr) { + clearTimeout(this.tmr); + this.tmr = null; + } + if (this.openAmount === 0) { + this.optsDirty = true; + this.state = SlidingState.Enabled; + } + this.initialOpenAmount = this.openAmount; + this.item.style.transition = 'none'; + } + + private onDragMove(gesture: GestureDetail) { + if (this.optsDirty) { + this.calculateOptsWidth(); + } + let openAmount = this.initialOpenAmount - gesture.deltaX; + + switch (this.sides) { + case ItemSide.Right: openAmount = Math.max(0, openAmount); break; + case ItemSide.Left: openAmount = Math.min(0, openAmount); break; + case ItemSide.Both: break; + case ItemSide.None: return; + default: console.warn('invalid ItemSideFlags value', this.sides); break; + } + + let optsWidth; + if (openAmount > this.optsWidthRightSide) { + optsWidth = this.optsWidthRightSide; + openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR; + + } else if (openAmount < -this.optsWidthLeftSide) { + optsWidth = -this.optsWidthLeftSide; + openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR; + } + + this.setOpenAmount(openAmount, false); + } + + private onDragEnd(gesture: GestureDetail) { + const velocity = gesture.velocityX; + + let restingPoint = (this.openAmount > 0) + ? this.optsWidthRightSide + : -this.optsWidthLeftSide; + + // Check if the drag didn't clear the buttons mid-point + // and we aren't moving fast enough to swipe open + const isResetDirection = (this.openAmount > 0) === !(velocity < 0); + const isMovingFast = Math.abs(velocity) > 0.3; + const isOnCloseZone = Math.abs(this.openAmount) < Math.abs(restingPoint / 2); + if (swipeShouldReset(isResetDirection, isMovingFast, isOnCloseZone)) { + restingPoint = 0; + } + + this.setOpenAmount(restingPoint, true); + + if (this.state & SlidingState.SwipeRight) { + this.rightOptions.fireSwipeEvent(this); + } else if (this.state & SlidingState.SwipeLeft) { + this.leftOptions.fireSwipeEvent(this); + } + } + + private calculateOptsWidth() { + this.optsWidthRightSide = 0; + if (this.rightOptions) { + this.optsWidthRightSide = this.rightOptions.width(); + } + + this.optsWidthLeftSide = 0; + if (this.leftOptions) { + this.optsWidthLeftSide = this.leftOptions.width(); + } + this.optsDirty = false; + } + + private setOpenAmount(openAmount: number, isFinal: boolean) { + if (this.tmr) { + clearTimeout(this.tmr); + this.tmr = null; + } + const style = this.item.style; + this.openAmount = openAmount; + + if (isFinal) { + style.transition = ''; + + } else if (openAmount > 0) { + this.state = (openAmount >= (this.optsWidthRightSide + SWIPE_MARGIN)) + ? SlidingState.Right | SlidingState.SwipeRight + : SlidingState.Right; + + } else if (openAmount < 0) { + this.state = (openAmount <= (-this.optsWidthLeftSide - SWIPE_MARGIN)) + ? SlidingState.Left | SlidingState.SwipeLeft + : SlidingState.Left; + } + + if (openAmount === 0) { + this.tmr = setTimeout(() => { + this.state = SlidingState.Disabled; + this.tmr = null; + }, 600); + this.list && this.list.setOpenedItem(null); + style.transform = ''; + return; + } + + style.transform = `translate3d(${-openAmount}px,0,0)`; + this.ionDrag.emit(this); + } + + protected hostData() { return { class: { 'item-wrapper': true, @@ -485,19 +429,7 @@ export class ItemSliding { protected render() { return ( - + ); diff --git a/packages/core/src/components/item-sliding/item-sliding.wp.scss b/packages/core/src/components/item-sliding/item-sliding.wp.scss index 2aacca95fe..266c5452c4 100644 --- a/packages/core/src/components/item-sliding/item-sliding.wp.scss +++ b/packages/core/src/components/item-sliding/item-sliding.wp.scss @@ -29,13 +29,13 @@ $item-wp-sliding-button-icon-color: color-contrast($colors-wp, $item-wp border-bottom: 1px solid $list-wp-border-color; } -.item-option-button-wp { +.item-option-wp { font-size: $item-wp-sliding-button-font-size; color: $item-wp-sliding-button-text-color; background-color: $item-wp-sliding-button-background-color; } -.item-option-button-wp .icon { +.item-option-wp .icon { fill: $item-wp-sliding-button-icon-color; } @@ -52,12 +52,12 @@ $item-wp-sliding-button-icon-color: color-contrast($colors-wp, $item-wp @each $color-name, $color-base, $color-contrast in get-colors($colors-wp) { - .item-option-button-wp-#{$color-name} { + item-option-wp-#{$color-name} { color: $color-contrast; background-color: $color-base; } - .item-option-button-wp-#{$color-name} .icon { + item-option-wp-#{$color-name} .icon { fill: $color-contrast; } diff --git a/packages/core/src/components/item-sliding/test/basic.html b/packages/core/src/components/item-sliding/test/basic.html index 92f65a8948..1ee8e272fc 100644 --- a/packages/core/src/components/item-sliding/test/basic.html +++ b/packages/core/src/components/item-sliding/test/basic.html @@ -143,13 +143,13 @@

I think I figured out how to get more Mountain Dew

- + Unread - + Archive @@ -166,13 +166,13 @@

I think I figured out how to get more Mountain Dew

- + Unread - + Archive @@ -190,7 +190,7 @@ One Line w/ Icon, div only text - + Archive @@ -249,7 +249,7 @@

Paragraph text.

- + Archive @@ -379,6 +379,9 @@ function reload() { window.location.reload(); } + document.addEventListener('ionSwipe', (ev)=> console.log('SWIPE!!', ev.detail)); + document.addEventListener('ionDrag', (ev)=> console.log('DRAG!!', ev.detail.getOpenAmount())); +