diff --git a/ionic/components/item/item-sliding-gesture.ts b/ionic/components/item/item-sliding-gesture.ts new file mode 100644 index 0000000000..ab0128a1b5 --- /dev/null +++ b/ionic/components/item/item-sliding-gesture.ts @@ -0,0 +1,187 @@ +import {Hammer} from 'ionic/gestures/hammer'; +import {DragGesture} from 'ionic/gestures/drag-gesture'; +import {List} from '../list/list'; + +import * as util from 'ionic/util'; +import {CSS, raf, closest} from 'ionic/util/dom'; + + +export class ItemSlidingGesture extends DragGesture { + constructor(list: List, listEle) { + super(listEle, { + direction: 'x', + threshold: list.width() + }); + + this.data = {}; + this.openItems = 0; + + this.list = list; + this.listEle = listEle; + this.canDrag = true; + this.listen(); + + this.on('tap', ev => { + if (!isFromOptionButtons(ev.target)) { + let didClose = this.closeOpened(); + if (didClose) { + ev.preventDefault(); + } + } + }); + } + + onDragStart(ev) { + let itemContainerEle = getItemConatiner(ev.target); + if (!itemContainerEle) return; + + this.closeOpened(ev, itemContainerEle); + + let openAmout = this.getOpenAmount(itemContainerEle); + let itemData = this.getData(itemContainerEle); + + if (openAmout) { + return ev.preventDefault(); + } + + itemContainerEle.classList.add('active-slide'); + + this.setData(itemContainerEle, 'offsetX', openAmout); + this.setData(itemContainerEle, 'startX', ev.center[this.direction]); + } + + onDrag(ev) { + let itemContainerEle = getItemConatiner(ev.target); + if (!itemContainerEle || !isActive(itemContainerEle)) return; + + let itemData = this.getData(itemContainerEle); + + if (!itemData.optsWidth) { + itemData.optsWidth = getOptionsWidth(itemContainerEle); + if (!itemData.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))); + } + + this.open(itemContainerEle, newX, false); + } + + onDragEnd(ev) { + let itemContainerEle = getItemConatiner(ev.target); + if (!itemContainerEle || !isActive(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.getData(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 & Hammer.DIRECTION_RIGHT) { + // Left + restingPoint = 0; + + } else if (Math.abs(ev.velocityX) < 0.3) { + // Right + restingPoint = 0; + } + } + + this.setData(itemContainerEle, 'opened', restingPoint > 0); + + raf(() => { + this.open(itemContainerEle, restingPoint, true); + }); + } + + closeOpened(ev, doNotCloseEle) { + 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; + } + } + } + return didClose; + } + + open(itemContainerEle, openAmount, animate) { + let slidingEle = itemContainerEle.querySelector('ion-item'); + if (!slidingEle) return; + + this.setData(itemContainerEle, 'openAmount', openAmount); + + clearTimeout(this.getData(itemContainerEle).timerId); + + if (openAmount > 0) { + this.openItems++; + + } else { + let timerId = setTimeout(() => { + if (slidingEle.style[CSS.transform] === '') { + itemContainerEle.classList.remove('active-slide'); + this.openItems--; + } + }, 400); + this.setData(itemContainerEle, 'timerId', timerId); + } + + slidingEle.style[CSS.transform] = (openAmount === 0 ? '' : 'translate3d(' + -openAmount + 'px,0,0)'); + slidingEle.style[CSS.transition] = (animate ? '' : 'none'); + } + + getOpenAmount(itemContainerEle) { + return this.getData(itemContainerEle).openAmount || 0; + } + + getData(itemContainerEle) { + return this.data[itemContainerEle && itemContainerEle.$ionSlide] || {}; + } + + setData(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 getItemConatiner(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 isActive(itemContainerEle) { + return itemContainerEle.classList.contains('active-slide'); +} diff --git a/ionic/components/item/item-sliding.scss b/ionic/components/item/item-sliding.scss new file mode 100644 index 0000000000..f27705b017 --- /dev/null +++ b/ionic/components/item/item-sliding.scss @@ -0,0 +1,36 @@ + +/** + * The hidden right-side buttons that can be exposed under a list item + * with dragging. + */ + +ion-item-sliding { + display: block; + position: relative; + overflow: hidden; + + .item { + position: static; + } +} + +ion-item-options { + display: none; + position: absolute; + top: 0; + right: 0; + z-index: $z-index-item-options; + height: 100%; +} + +ion-item-sliding.active-slide { + + .item { + position: relative; + z-index: $z-index-item-options + 1; + } + + ion-item-options { + display: flex; + } +} diff --git a/ionic/components/item/item-sliding.ts b/ionic/components/item/item-sliding.ts index ea04bacbfd..4e7fcdae43 100644 --- a/ionic/components/item/item-sliding.ts +++ b/ionic/components/item/item-sliding.ts @@ -1,31 +1,7 @@ -import {Component, Directive, ElementRef, NgIf, Host, Optional, Renderer, NgZone} from 'angular2/angular2'; +import {Component, ElementRef, Optional} from 'angular2/angular2'; -import {Gesture} from 'ionic/gestures/gesture'; -import {DragGesture} from 'ionic/gestures/drag-gesture'; -import {Hammer} from 'ionic/gestures/hammer'; -import {List} from 'ionic/components/list/list'; +import {List} from '../list/list'; -import * as util from 'ionic/util'; - -import {CSS, raf} from 'ionic/util/dom'; - - - -@Directive({ - selector: 'ion-item-options > button,ion-item-options > [button]', - host: { - '(click)': 'clicked($event)' - } -}) -export class ItemSlidingOptionButton { - constructor(elementRef: ElementRef) { - } - clicked(event) { - // Don't allow the click to propagate - event.preventDefault(); - event.stopPropagation(); - } -} /** * @name ionItem @@ -36,238 +12,35 @@ export class ItemSlidingOptionButton { * @usage * ```html * - * - * {{item.title}} - *
- * {{item.note}} - *
+ * + * + * {{item.title}} + * + * + * + * + * * *
- * ``` + * ``` */ @Component({ selector: 'ion-item-sliding', - inputs: [ - 'sliding' - ], template: - '' + - '' + - '' + - '' + - '' + - ''+ - '' + - '' + '' + + '' }) export class ItemSliding { - /** - * TODO - * @param {ElementRef} elementRef A reference to the component's DOM element. - */ - constructor(elementRef: ElementRef, renderer: Renderer, @Optional() @Host() list: List, zone: NgZone) { - this._zone = zone; - renderer.setElementClass(elementRef, 'item', true); - renderer.setElementAttribute(elementRef, 'tappable', ''); - this._isOpen = false; - this._isSlideActive = false; - this._isTransitioning = false; - this._transform = ''; - - this.list = list; - - this.elementRef = elementRef; - this.swipeButtons = {}; - this.optionButtons = {}; + constructor(@Optional() private list: List, elementRef: ElementRef) { + list.enableSlidingItems(true); + elementRef.nativeElement.$ionSlide = ++slideIds; } - onInit() { - let ele = this.elementRef.nativeElement; - this.itemSlidingContent = ele.querySelector('ion-item-sliding-content'); - this.itemOptions = ele.querySelector('ion-item-options'); - this.openAmount = 0; - this._zone.runOutsideAngular(() => { - this.gesture = new ItemSlideGesture(this, this.itemSlidingContent, this._zone); - }); + close() { + this.list.closeSlidingItems(); } - onDestroy() { - this.gesture && this.gesture.unlisten(); - this.itemSlidingContent = this.itemOptionsContent = null; - } - - close(andStopDrag) { - this.openAmount = 0; - - // Enable it once, it'll get disabled on the next drag - raf(() => { - this.enableAnimation(); - if (this.itemSlidingContent) { - this.itemSlidingContent.style[CSS.transform] = 'translateX(0)'; - } - }); - } - - open(amt) { - let el = this.itemSlidingContent; - this.openAmount = amt || 0; - - if (this.list) { - this.list.setOpenItem(this); - } - - if (amt === '') { - el.style[CSS.transform] = ''; - } else { - el.style[CSS.transform] = 'translateX(' + -amt + 'px)'; - } - } - - isOpen() { - return this.openAmount > 0; - } - - getOpenAmt() { - return this.openAmount; - } - - disableAnimation() { - this.itemSlidingContent.style[CSS.transition] = 'none'; - } - - enableAnimation() { - // Clear the explicit transition, allow for CSS one to take over - this.itemSlidingContent.style[CSS.transition] = ''; - } - - /** - * User did a touchstart - */ - didTouch() { - if (this.isOpen()) { - this.close(); - this.didClose = true; - - } else { - let openItem = this.list.getOpenItem(); - if (openItem && openItem !== this) { - this.didClose = true; - } - if (this.list) { - this.list.closeOpenItem(); - } - } - } } -class ItemSlideGesture extends DragGesture { - constructor(item: ItemSliding, el: Element, zone) { - super(el, { - direction: 'x', - threshold: el.offsetWidth - }); - this.item = item; - this.canDrag = true; - this.listen(); - - zone.runOutsideAngular(() => { - let touchStart = (e) => { - this.item.didTouch(); - raf(() => { - this.item.itemOptionsWidth = this.item.itemOptions && this.item.itemOptions.offsetWidth || 0; - }) - }; - el.addEventListener('touchstart', touchStart); - el.addEventListener('mousedown', touchStart); - - let touchEnd = (e) => { - // If we have a touch end and the item is closing, - // prevent default to stop a click from triggering - if(this.item.didClose) { - e.preventDefault(); - } - this.item.didClose = false; - }; - el.addEventListener('touchend', touchEnd); - el.addEventListener('mouseup', touchEnd); - el.addEventListener('mouseout', touchEnd); - el.addEventListener('mouseleave', touchEnd); - el.addEventListener('touchcancel', touchEnd); - }); - } - - onDragStart(ev) { - if (this.item.didClose) { return; } - - if (!this.item.itemOptionsWidth) { return; } - - this.slide = {}; - - this.slide.offsetX = this.item.getOpenAmt(); - this.slide.startX = ev.center[this.direction]; - this.slide.started = true; - - this.item.disableAnimation(); - } - - onDrag(ev) { - if (!this.slide || !this.slide.started) return; - - this.slide.x = ev.center[this.direction]; - this.slide.delta = this.slide.x - this.slide.startX; - - let newX = Math.max(0, this.slide.offsetX - this.slide.delta); - - let buttonsWidth = this.item.itemOptionsWidth; - - if (newX > this.item.itemOptionsWidth) { - // Calculate the new X position, capped at the top of the buttons - newX = -Math.min(-buttonsWidth, -buttonsWidth + (((this.slide.delta + buttonsWidth) * 0.4))); - } - - this.item.open(newX); - } - - onDragEnd(ev) { - if (!this.slide || !this.slide.started) return; - - let buttonsWidth = this.item.itemOptionsWidth; - - // 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 - var restingPoint = this.item.itemOptionsWidth; - - // Check if the drag didn't clear the buttons mid-point - // and we aren't moving fast enough to swipe open - if (this.item.openAmount < (buttonsWidth / 2)) { - - // If we are going left but too slow, or going right, go back to resting - if (ev.direction & Hammer.DIRECTION_RIGHT) { - // Left - restingPoint = 0; - } else if (Math.abs(ev.velocityX) < 0.3) { - // Right - restingPoint = 0; - } - } - - raf(() => { - if (restingPoint === 0) { - // Reset to zero - this.item.open(''); - var buttons = this.item.itemOptions; - clearTimeout(this.hideButtonsTimeout); - this.hideButtonsTimeout = setTimeout(() => { - buttons && buttons.classList.add('invisible'); - }, 250); - - } else { - this.item.open(restingPoint); - } - this.item.enableAnimation(); - - this.slide = null; - }); - } -} +let slideIds = 0; diff --git a/ionic/components/item/item.scss b/ionic/components/item/item.scss index a047c5bccd..61ffdc307f 100644 --- a/ionic/components/item/item.scss +++ b/ionic/components/item/item.scss @@ -134,24 +134,6 @@ ion-input.item { align-items: flex-start; } -/** - * The hidden right-side buttons that can be exposed under a list item - * with dragging. - */ -ion-item-sliding-content { - display: block; - z-index: $z-index-item-options + 1; - flex: 1; -} -ion-item-options { - display: flex; - position: absolute; - top: 0; - right: 0; - z-index: $z-index-item-options; - height: 100%; -} - // TEMP hack for https://github.com/angular/angular/issues/4582 [item-right] { diff --git a/ionic/components/item/modes/ios.scss b/ionic/components/item/modes/ios.scss index 1de425d967..6215e46607 100644 --- a/ionic/components/item/modes/ios.scss +++ b/ionic/components/item/modes/ios.scss @@ -163,26 +163,6 @@ ion-note { } } - - ion-item-sliding.item { - padding-left: 0; - padding-right: 0; - } - - ion-item-sliding-content { - background-color: $item-ios-sliding-content-bg; - padding-right: ($item-ios-padding-right / 2); - padding-left: ($item-ios-padding-left / 2); - display: flex; - min-height: 42px; - justify-content: center; - - transition: $item-ios-sliding-transition; - - // To allow the hairlines through - margin-top: 1px; - margin-bottom: 1px; - } ion-item-options { button, [button] { min-height: calc(100% - 2px); @@ -204,8 +184,7 @@ ion-note { .item.activated, a.item.activated, -button.item.activated, -.item.activated ion-item-sliding-content { +button.item.activated { background-color: $item-ios-activated-background-color; transition-duration: 0ms; } @@ -219,25 +198,13 @@ button.item { .list, ion-card { button[ion-item]:not([detail-none]), - a[ion-item]:not([detail-none]), - [detail-push]:not(ion-item-sliding) { + a[ion-item]:not([detail-none]) { @include ios-detail-push-icon($item-ios-detail-push-color); background-repeat: no-repeat; background-position: right ($item-ios-padding-right - 2) center; background-size: 14px 14px; margin-right: 32px; } - - ion-item-sliding[detail-push] { - - ion-item-sliding-content { - @include ios-detail-push-icon($item-ios-detail-push-color); - background-repeat: no-repeat; - background-position: right ($item-ios-padding-right - 2) center; - background-size: 14px 14px; - margin-right: 32px; - } - } } // Hairlines for iOS need to be set at 0.55px to show on iPhone 6 and 6 Plus @@ -252,11 +219,6 @@ ion-card { } } - ion-item-sliding-content { - margin-top: 0.55px; - margin-bottom: 0.55px; - } - ion-header + .item { border-top-width: 0.55px; diff --git a/ionic/components/item/modes/md.scss b/ionic/components/item/modes/md.scss index 4afc28a385..9f38858d62 100644 --- a/ionic/components/item/modes/md.scss +++ b/ionic/components/item/modes/md.scss @@ -207,24 +207,6 @@ ion-note { box-shadow: none; } - ion-item-sliding.item { - padding-left: 0; - padding-right: 0; - } - ion-item-sliding-content { - background-color: $item-md-sliding-content-bg; - padding-right: ($item-md-padding-right / 2); - padding-left: ($item-md-padding-left / 2); - display: flex; - min-height: 42px; - justify-content: center; - - transition: $item-md-sliding-transition; - - // To allow the hairlines through - margin-top: 1px; - margin-bottom: 1px; - } ion-item-options { button, [button] { height: calc(100% - 2px); @@ -246,15 +228,13 @@ ion-note { .item, a.item, -button.item, -.item ion-item-sliding-content { - transition: background-color $button-md-transition-duration $button-md-animation-curve; +button.item { + transition: background-color $button-md-transition-duration $button-md-animation-curve, transform 300ms; } .item.activated, a.item.activated, -button.item.activated, -.item.activated ion-item-sliding-content { +button.item.activated { background-color: $item-md-activated-background-color; box-shadow: none; } diff --git a/ionic/components/item/test/sliding/index.ts b/ionic/components/item/test/sliding/index.ts index edf8bf5718..96d33e8aca 100644 --- a/ionic/components/item/test/sliding/index.ts +++ b/ionic/components/item/test/sliding/index.ts @@ -1,29 +1,35 @@ -import {App} from 'ionic/ionic'; +import {App, IonicApp} from 'ionic/ionic'; @App({ templateUrl: 'main.html' }) class E2EApp { - constructor() { + constructor(private app: IonicApp) { setTimeout(() => { this.shouldShow = true; }, 10); } + closeOpened() { + this.app.getComponent('myList').closeSlidingItems(); + } + getItems() { - console.log('getItems'); return [0,1]; } - didClick(e) { - console.log('CLICK', e.defaultPrevented, e) + didClick(ev, item) { + console.log('CLICK', ev.defaultPrevented, ev) } - archive(e) { - console.log('Accept', e); + archive(ev, item) { + console.log('Archive', ev, item); + item.close(); } - del(e) { - console.log('Delete', e); + + del(ev, item) { + console.log('Delete', ev, item); + item.close(); } } diff --git a/ionic/components/item/test/sliding/main.html b/ionic/components/item/test/sliding/main.html index 5f13ed5840..7b3fd83541 100644 --- a/ionic/components/item/test/sliding/main.html +++ b/ionic/components/item/test/sliding/main.html @@ -1,77 +1,99 @@ Sliding Items - + + + - -

Max Lynch

-

- Hey do you want to go to the game tonight? -

- - - - -
+ + +

Max Lynch

+

+ Hey do you want to go to the game tonight? +

+
+ + + + +
- -

Adam Bradley

-

- I think I figured out how to get more Mountain Dew -

- - - - -
+ + +

Adam Bradley

+

+ I think I figured out how to get more Mountain Dew +

+
+ + + + +
- -

Ben Sperry

-

- I like paper -

- - - - -
+ + +

Ben Sperry

+

+ I like paper +

+
+ + + + +
- - - One Line w/ Icon, div only text - - - - + + + + One Line w/ Icon, div only text + + + + + - - - - - One Line w/ Avatar, div only text - - - - + + + + + + One Line w/ Avatar, div only text + + + + + - - - - -

Two Lines w/ Thumbnail, H2 Header

-

Paragraph text.

- - - -
+ + + + + +

Two Lines w/ Thumbnail, H2 Header

+

Paragraph text.

+
+ + + +
- -

ng-for {{item}}

- - - -
+ + +

ng-for {{item}}

+
+ + + +
-
+
+ +

+ +

+ +