diff --git a/src/components/item/item-sliding-gesture.ts b/src/components/item/item-sliding-gesture.ts index 4adc4d43814..04cacdd9066 100644 --- a/src/components/item/item-sliding-gesture.ts +++ b/src/components/item/item-sliding-gesture.ts @@ -1,16 +1,14 @@ -import {DIRECTION_RIGHT} from '../../gestures/hammer'; import {DragGesture} from '../../gestures/drag-gesture'; +import {ItemSliding} from './item-sliding'; import {List} from '../list/list'; -import {CSS, nativeRaf, closest} from '../../util/dom'; +import {closest} from '../../util/dom'; +const DRAG_THRESHOLD = 20; export class ItemSlidingGesture extends DragGesture { - canDrag: boolean = true; - data = {}; - openItems: number = 0; onTap; - onMouseOut; + canDrag: boolean = true; preventDrag: boolean = false; dragEnded: boolean = true; @@ -19,54 +17,43 @@ export class ItemSlidingGesture extends DragGesture { direction: 'x', threshold: DRAG_THRESHOLD }); - + this.onTap = (event) => this.onTapCallback(event); this.listen(); + } - this.onTap = (ev) => { - if (!isFromOptionButtons(ev.target)) { - let didClose = this.closeOpened(); - if (didClose) { - console.debug('tap close sliding item'); - preventDefault(ev); - } - } - }; - - this.onMouseOut = (ev) => { - if (ev.target.tagName === 'ION-ITEM-SLIDING') { - console.debug('tap close sliding item'); - this.onDragEnd(ev); - } - }; + onTapCallback(ev) { + if (isFromOptionButtons(ev.target)) { + return; + } + let didClose = this.closeOpened(); + if (didClose) { + console.debug('tap close sliding item, preventDefault'); + ev.preventDefault(); + } } onDragStart(ev): boolean { - let itemContainerEle = getItemContainer(ev.target); - if (!itemContainerEle) { + let container = getContainer(ev); + if (!container) { console.debug('onDragStart, no itemContainerEle'); return false; } - this.closeOpened(itemContainerEle); - - let openAmout = this.getOpenAmount(itemContainerEle); - let itemData = this.get(itemContainerEle); - this.preventDrag = (openAmout > 0); - + // Close all item sliding containers but the selected one + this.preventDrag = container.getOpenAmount() > 0; if (this.preventDrag) { this.closeOpened(); console.debug('onDragStart, preventDefault'); - preventDefault(ev); - return; + ev.preventDefault(); + return false; } - itemContainerEle.classList.add('active-slide'); - - this.set(itemContainerEle, 'offsetX', openAmout); - this.set(itemContainerEle, 'startX', ev.center[this.direction]); - + // Close all item sliding containers but the selected one + this.closeOpened(container); this.dragEnded = false; + container.startSliding(ev.center.x); + return true; } @@ -77,181 +64,60 @@ export class ItemSlidingGesture extends DragGesture { return; } - let itemContainerEle = getItemContainer(ev.target); - if (!itemContainerEle || !isActive(itemContainerEle)) { + let container = getContainer(ev); + if (!container) { console.debug('onDrag, no itemContainerEle'); return; } - let itemData = this.get(itemContainerEle); - - if (!itemData.optsWidth) { - itemData.optsWidth = getOptionsWidth(itemContainerEle); - if (!itemData.optsWidth) { - console.debug('onDrag, no optsWidth'); - return; - } - } - - let x = ev.center[this.direction]; - let delta = x - itemData.startX; - - let newX = Math.max(0, itemData.offsetX - delta); - - if (newX > itemData.optsWidth) { - // Calculate the new X position, capped at the top of the buttons - newX = -Math.min(-itemData.optsWidth, -itemData.optsWidth + (((delta + itemData.optsWidth) * 0.4))); - } - - if (newX > 5 && ev.srcEvent.type.indexOf('mouse') > -1 && !itemData.hasMouseOut) { - itemContainerEle.addEventListener('mouseout', this.onMouseOut); - itemData.hasMouseOut = true; - } - - nativeRaf(() => { - if (!this.dragEnded && !this.preventDrag) { - isItemActive(itemContainerEle, true); - this.open(itemContainerEle, newX, false); - } - }); + container.moveSliding(ev.center.x); } onDragEnd(ev) { this.preventDrag = false; this.dragEnded = true; - let itemContainerEle = getItemContainer(ev.target); - if (!itemContainerEle || !isActive(itemContainerEle)) { + let container = getContainer(ev); + if (!container) { console.debug('onDragEnd, no itemContainerEle'); return; } - // If we are currently dragging, we want to snap back into place - // The final resting point X will be the width of the exposed buttons - let itemData = this.get(itemContainerEle); - - var restingPoint = itemData.optsWidth; - - // Check if the drag didn't clear the buttons mid-point - // and we aren't moving fast enough to swipe open - - if (this.getOpenAmount(itemContainerEle) < (restingPoint / 2)) { - - // If we are going left but too slow, or going right, go back to resting - if (ev.direction & DIRECTION_RIGHT || Math.abs(ev.velocityX) < 0.3) { - restingPoint = 0; - } + let openAmount = container.endSliding(ev.velocityX); + if (openAmount === 0) { + this.off('tap', this.onTap); + } else { + this.on('tap', this.onTap); } - - itemContainerEle.removeEventListener('mouseout', this.onMouseOut); - itemData.hasMouseOut = false; - - nativeRaf(() => { - this.open(itemContainerEle, restingPoint, true); - }); } - closeOpened(doNotCloseEle?: HTMLElement) { + closeOpened(doNotClose?: ItemSliding): boolean { let didClose = false; - if (this.openItems) { - let openItemElements = this.listEle.querySelectorAll('.active-slide'); - for (let i = 0; i < openItemElements.length; i++) { - if (openItemElements[i] !== doNotCloseEle) { - this.open(openItemElements[i], 0, true); - didClose = true; - } + let openItemElements = this.listEle.querySelectorAll('.active-slide'); + for (var i = 0; i < openItemElements.length; i++) { + var component = openItemElements[i]['$ionComponent']; + if (component && component !== doNotClose) { + component.close(); + didClose = true; } } return didClose; } - open(itemContainerEle, openAmount, isFinal) { - let slidingEle = itemContainerEle.querySelector('ion-item,[ion-item]'); - if (!slidingEle) { - console.debug('open, no slidingEle, openAmount:', openAmount); - return; - } - - this.set(itemContainerEle, 'openAmount', openAmount); - - clearTimeout(this.get(itemContainerEle).timerId); - - if (openAmount) { - this.openItems++; - - } else { - let timerId = setTimeout(() => { - if (slidingEle.style[CSS.transform] === '') { - isItemActive(itemContainerEle, false); - this.openItems--; - } - }, 400); - this.set(itemContainerEle, 'timerId', timerId); - } - - slidingEle.style[CSS.transition] = (isFinal ? '' : 'none'); - slidingEle.style[CSS.transform] = (openAmount ? 'translate3d(' + -openAmount + 'px,0,0)' : ''); - - if (isFinal) { - if (openAmount) { - isItemActive(itemContainerEle, true); - this.on('tap', this.onTap); - - } else { - this.off('tap', this.onTap); - } - } - } - - getOpenAmount(itemContainerEle) { - return this.get(itemContainerEle).openAmount || 0; - } - - get(itemContainerEle) { - return this.data[itemContainerEle && itemContainerEle.$ionSlide] || {}; - } - - set(itemContainerEle, key, value) { - if (!this.data[itemContainerEle.$ionSlide]) { - this.data[itemContainerEle.$ionSlide] = {}; - } - this.data[itemContainerEle.$ionSlide][key] = value; - } - unlisten() { super.unlisten(); this.listEle = null; } } -function isItemActive(ele, isActive) { - ele.classList[isActive ? 'add' : 'remove']('active-slide'); - ele.classList[isActive ? 'add' : 'remove']('active-options'); -} - -function preventDefault(ev) { - console.debug('sliding item preventDefault', ev.type); - ev.preventDefault(); -} - -function getItemContainer(ele) { - return closest(ele, 'ion-item-sliding', true); -} - -function isFromOptionButtons(ele) { - return !!closest(ele, 'ion-item-options', true); -} - -function getOptionsWidth(itemContainerEle) { - let optsEle = itemContainerEle.querySelector('ion-item-options'); - if (optsEle) { - return optsEle.offsetWidth; +function getContainer(ev): ItemSliding { + let ele = closest(ev.target, 'ion-item-sliding', true); + if (ele) { + return ele['$ionComponent']; } + return null; } -function isActive(itemContainerEle) { - return itemContainerEle.classList.contains('active-slide'); +function isFromOptionButtons(ele: HTMLElement): boolean { + return !!closest(ele, 'ion-item-options', true); } - - -const DRAG_THRESHOLD = 20; diff --git a/src/components/item/item-sliding.scss b/src/components/item/item-sliding.scss index 93d0b41d5a1..6669ea356ca 100644 --- a/src/components/item/item-sliding.scss +++ b/src/components/item/item-sliding.scss @@ -27,6 +27,11 @@ ion-item-options { visibility: hidden; } +ion-item-options[side=left] { + right: auto; + left: 0; +} + ion-item-options .button { margin: 0; @@ -68,7 +73,11 @@ ion-item-sliding.active-slide { display: flex; } - &.active-options ion-item-options { + &.active-options-left ion-item-options[side=left] { + visibility: visible; + } + + &.active-options-right ion-item-options:not([side=left]) { visibility: visible; } } diff --git a/src/components/item/item-sliding.ts b/src/components/item/item-sliding.ts index 647f2b75285..cf02dad7914 100644 --- a/src/components/item/item-sliding.ts +++ b/src/components/item/item-sliding.ts @@ -1,8 +1,44 @@ -import {Component, ElementRef, Optional, ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core'; +import {Component, ContentChildren, ContentChild, ChangeDetectionStrategy, Directive, ElementRef, Input, Optional, Renderer, ViewEncapsulation} from '@angular/core'; import {List} from '../list/list'; +import {Ion} from '../ion'; +import {Item} from './item'; +import {isPresent} from '../../util/util'; +import {CSS} from '../../util/dom'; +export const enum SideFlags { + None = 0, + Left = 1 << 0, + Right = 1 << 1, + Both = Left | Right +} + +/** + * @private + */ +@Directive({ + selector: 'ion-item-options', +}) +export class ItemOptions extends Ion { + @Input() side: string; + + constructor(elementRef: ElementRef) { + super(elementRef); + } + + /** + * @private + */ + getSides(): SideFlags { + if (isPresent(this.side) && this.side === 'left') { + return SideFlags.Left; + } else { + return SideFlags.Right; + } + } +} + /** * @name ItemSliding * @@ -53,13 +89,138 @@ import {List} from '../list/list'; '' + '', changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, + encapsulation: ViewEncapsulation.None }) export class ItemSliding { + private _openAmount: number = 0; + private _startX: number = 0; + private _offsetX: number = 0; + private _optsWidthRightSide: number = 0; + private _optsWidthLeftSide: number = 0; + private _sides: SideFlags; + private _timer: number = null; + private _leftOptions: ItemOptions; + private _rightOptions: ItemOptions; + private _optsDirty: boolean = true; + + @ContentChild(Item) item; - constructor(@Optional() private _list: List, elementRef: ElementRef) { + constructor(@Optional() private _list: List, private _renderer: Renderer, private _elementRef: ElementRef) { _list.enableSlidingItems(true); - elementRef.nativeElement.$ionSlide = ++slideIds; + _elementRef.nativeElement.$ionComponent = this; + } + + /** + * @private + */ + @ContentChildren(ItemOptions) + set _itemOptions(itemOptions) { + var sides = 0; + for (var item of itemOptions.toArray()) { + var side = item.getSides(); + if (side === SideFlags.Left) { + this._leftOptions = item; + } else { + this._rightOptions = item; + } + sides |= item.getSides(); + } + this._optsDirty = true; + this._sides = sides; + } + + /** + * @private + */ + startSliding(startX: number) { + this._setClass('active-slide', true); + this._offsetX = this._openAmount; + this._optsDirty = true; + this._startX = startX; + } + + /** + * @private + */ + moveSliding(x: number): number { + if (this._optsDirty) { + if (this._rightOptions) { + this._optsWidthRightSide = this._rightOptions.width(); + } + if (this._leftOptions) { + this._optsWidthLeftSide = this._leftOptions.width(); + } + this._optsDirty = false; + } + + this._openAmount = computeOpenAmount( + x, + this._startX, + this._offsetX, + this._optsWidthRightSide, + this._optsWidthLeftSide, + this._sides + ); + this._updateVisual(false); + return this._openAmount; + } + + /** + * @private + */ + endSliding(velocity: number): number { + this._openAmount = computeRestingPoint( + this._openAmount, + this._optsWidthRightSide, + this._optsWidthLeftSide, + velocity + ); + this._updateVisual(true); + return this._openAmount; + } + + /** + * @private + */ + private _updateVisual(isFinal: boolean) { + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + } + let openAmount = this._openAmount; + let didEnd = openAmount === 0; + if (didEnd) { + // TODO: refactor. there must exist a better way + // if sliding ended, we wait 400ms until animation finishes + this._timer = setTimeout(() => { + this._setClass('active-slide', false); + this._setClass('active-options-right', false); + this._setClass('active-options-left', false); + this._timer = null; + }, 400); + + } else if (openAmount > 0) { + this._setClass('active-options-right', true); + } else if (openAmount < 0) { + this._setClass('active-options-left', true); + } + + this.item.setCssStyle(CSS.transition, (isFinal ? '' : 'none')); + this.item.setCssStyle(CSS.transform, (didEnd ? '' : 'translate3d(' + -openAmount + 'px,0,0)')); + } + + /** + * @private + */ + private _setClass(className: string, add: boolean) { + this._renderer.setElementClass(this._elementRef.nativeElement, className, add); + } + + /** + * @private + */ + getOpenAmount(): number { + return this._openAmount; } /** @@ -97,9 +258,44 @@ export class ItemSliding { * ``` */ close() { - this._list.closeSlidingItems(); + this._openAmount = 0; + this._updateVisual(true); } } -let slideIds = 0; +function computeOpenAmount(eventX: number, startX: number, offsetX: number, optsWidthRight: number, optsWidthLeft: number, sides: SideFlags): number { + let delta = eventX - startX; + let openAmount = offsetX - delta; + switch (sides) { + case SideFlags.Right: openAmount = Math.max(0, openAmount); break; + case SideFlags.Left: openAmount = Math.min(0, openAmount); break; + case SideFlags.Both: break; + default: openAmount = 0; break; + } + + let optsWidth = (openAmount > 0) + ? optsWidthRight + : -optsWidthLeft; + + if (Math.abs(openAmount) > Math.abs(optsWidth)) { + openAmount = optsWidth - (optsWidth + delta) * 0.4; + } + return openAmount; +} + +function computeRestingPoint(openAmount: number, optsWidthRight: number, optsWidthLeft: number, velocityX: number): number { + let restingPoint = (openAmount > 0) + ? optsWidthRight + : -optsWidthLeft; + + // Check if the drag didn't clear the buttons mid-point + // and we aren't moving fast enough to swipe open + let isOnResetZone = Math.abs(openAmount) < Math.abs(restingPoint / 2); + let isMovingSlow = Math.abs(velocityX) < 0.3; + let isDirection = (openAmount > 0) === (velocityX > 0); + if (isOnResetZone && (isMovingSlow || isDirection)) { + restingPoint = 0; + } + return restingPoint; +} diff --git a/src/components/item/item.ts b/src/components/item/item.ts index 30cb0edf0b1..d7459fb1d92 100644 --- a/src/components/item/item.ts +++ b/src/components/item/item.ts @@ -108,6 +108,13 @@ export class Item { this._renderer.setElementClass(this._elementRef.nativeElement, cssClass, shouldAdd); } + /** + * @private + */ + setCssStyle(property: string, value: string) { + this._renderer.setElementStyle(this._elementRef.nativeElement, property, value); + } + /** * @private */ diff --git a/src/components/item/test/sliding/main.html b/src/components/item/test/sliding/main.html index 15495fc7c3a..d04f4a12f95 100644 --- a/src/components/item/test/sliding/main.html +++ b/src/components/item/test/sliding/main.html @@ -11,12 +11,12 @@ - + @@ -24,7 +24,7 @@

Max Lynch

-

Adam Bradley

+

RIGHT side sliding only

I think I figured out how to get more Mountain Dew

@@ -43,15 +43,18 @@

Adam Bradley

-

Ben Sperry

+

RIGHT/LEFT sliding

I like paper

- + + + +
diff --git a/src/components/list/list.ts b/src/components/list/list.ts index dbd44a3fb78..2abc477af6a 100644 --- a/src/components/list/list.ts +++ b/src/components/list/list.ts @@ -69,20 +69,19 @@ export class List extends Ion { * @param {boolean} shouldEnable whether the item-sliding should be enabled or not */ enableSlidingItems(shouldEnable: boolean) { - if (this._enableSliding !== shouldEnable) { - this._enableSliding = shouldEnable; - - if (shouldEnable) { - console.debug('enableSlidingItems'); - this._zone.runOutsideAngular(() => { - setTimeout(() => { - this.slidingGesture = new ItemSlidingGesture(this, this.ele); - }); - }); - - } else { - this.slidingGesture && this.slidingGesture.unlisten(); - } + if (this._enableSliding === shouldEnable) { + return; + } + + this._enableSliding = shouldEnable; + if (shouldEnable) { + console.debug('enableSlidingItems'); + this._zone.runOutsideAngular(() => { + setTimeout(() => this.slidingGesture = new ItemSlidingGesture(this, this.ele)); + }); + + } else { + this.slidingGesture && this.slidingGesture.unlisten(); } } @@ -119,11 +118,8 @@ export class List extends Ion { selector: 'ion-list-header' }) export class ListHeader { - private _id: string; - constructor(private _renderer: Renderer, private _elementRef: ElementRef, @Attribute('id') id: string) { - this._id = id; - } + constructor(private _renderer: Renderer, private _elementRef: ElementRef, @Attribute('id') private _id: string) { } public get id(): string { return this._id; diff --git a/src/config/directives.ts b/src/config/directives.ts index 6a040f07466..924ae7b1cf1 100644 --- a/src/config/directives.ts +++ b/src/config/directives.ts @@ -18,7 +18,7 @@ import {Tabs} from '../components/tabs/tabs'; import {Tab} from '../components/tabs/tab'; import {List, ListHeader} from '../components/list/list'; import {Item} from '../components/item/item'; -import {ItemSliding} from '../components/item/item-sliding'; +import {ItemSliding, ItemOptions} from '../components/item/item-sliding'; import {VirtualScroll} from '../components/virtual-scroll/virtual-scroll'; import {VirtualItem, VirtualHeader, VirtualFooter} from '../components/virtual-scroll/virtual-item'; import {Toolbar, ToolbarTitle, ToolbarItem} from '../components/toolbar/toolbar'; @@ -134,6 +134,7 @@ export const IONIC_DIRECTIVES: any[] = [ ListHeader, Item, ItemSliding, + ItemOptions, VirtualScroll, VirtualItem, VirtualHeader,