diff --git a/ionic/components.ts b/ionic/components.ts index 0e4346cb04..94eb8ee941 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -10,6 +10,7 @@ export * from 'ionic/components/form/input' export * from 'ionic/components/icon/icon' export * from 'ionic/components/item/item' export * from 'ionic/components/item/item-group' +export * from 'ionic/components/item/item-sliding' export * from 'ionic/components/menu/menu' export * from 'ionic/components/menu/menu-types' export * from 'ionic/components/menu/menu-toggle' diff --git a/ionic/components/app/structure.scss b/ionic/components/app/structure.scss index ec12bdc77a..c57ee0fc69 100644 --- a/ionic/components/app/structure.scss +++ b/ionic/components/app/structure.scss @@ -22,6 +22,7 @@ $z-index-backdrop: 1; $z-index-overlay-wrapper: 10; +$z-index-item-options: 1; // Flex Order // -------------------------------------------------- diff --git a/ionic/components/item/item-sliding.ts b/ionic/components/item/item-sliding.ts new file mode 100644 index 0000000000..1e3345c1f0 --- /dev/null +++ b/ionic/components/item/item-sliding.ts @@ -0,0 +1,204 @@ +import {Component, Directive, View, ElementRef, NgIf, ViewQuery, QueryList} from 'angular2/angular2'; + +import {Gesture} from 'ionic/gestures/gesture'; +import {DragGesture} from 'ionic/gestures/drag-gesture'; +import {Hammer} from 'ionic/gestures/hammer'; + +import * as util from 'ionic/util'; + +import {CSS, raf} from 'ionic/util/dom'; + + +/** + * @name ionItem + * @description + * Creates a list-item that can easily be swiped, + * deleted, reordered, edited, and more. + * + * @usage + * ```html + * + * + * {{item.title}} + * + * {{item.note}} + * + * + * + * ``` + */ +@Component({ + selector: 'ion-item-sliding,[ion-item-sliding]', + host: { + 'class': 'item' + }, + properties: [ + 'sliding' + ] +}) +@View({ + template: + '' + + '' + + '' + + ''+ + '' + + '' + + '' + + '', + directives: [NgIf] +}) +export class ItemSliding { + /** + * TODO + * @param {ElementRef} elementRef A reference to the component's DOM element. + */ + constructor(elementRef: ElementRef) { + this._isOpen = false; + this._isSlideActive = false; + this._isTransitioning = false; + this._transform = ''; + + this.ele = elementRef.nativeElement; + this.swipeButtons = {}; + this.optionButtons = {}; + + } + + onInit() { + this._initSliding(); + } + + _initSliding() { + var itemSlidingContent = this.ele.querySelector('ion-item-sliding-content'); + var itemOptionsContent = this.ele.querySelector('ion-item-options'); + + this.itemSlidingContent = itemSlidingContent; + this.itemOptions = itemOptionsContent; + + this.itemWidth = itemSlidingContent.offsetWidth; + this.itemOptionsWidth = itemOptionsContent && itemOptionsContent.offsetWidth || 0; + + this.openAmount = 0; + + this.gesture = new ItemSlideGesture(this, itemSlidingContent); + } + + close() { + this.openAmount = 0; + + // Enable it once, it'll get disabled on the next drag + this.enableAnimation(); + el.style[CSS.transform] = 'translateX(' + -amt + 'px)'; + } + open(amt) { + let el = this.itemSlidingContent; + this.openAmount = amt || 0; + + if(amt === '') { + el.style[CSS.transform] = ''; + } else { + el.style[CSS.transform] = 'translateX(' + -amt + 'px)'; + } + } + get isOpen() { + return this.openAmount > 0; + } + getOpenAmt() { + return this.openAmount; + } + getItemWidth() { + return this.itemWidth; + } + disableAnimation() { + this.itemSlidingContent.style[CSS.transition] = 'none'; + } + enableAnimation() { + // Clear the explicit transition, allow for CSS one to take over + this.itemSlidingContent.style[CSS.transition] = ''; + } +} + +class ItemSlideGesture extends DragGesture { + constructor(item: ItemSliding, el: Element) { + + super(el, { + direction: 'x', + threshold: el.offsetWidth + }); + + this.el = el; + this.item = item; + this.listen(); + } + + onDragStart(ev) { + + 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; + }); + } +} diff --git a/ionic/components/item/item.scss b/ionic/components/item/item.scss index 95041526d9..2f77bff4b8 100644 --- a/ionic/components/item/item.scss +++ b/ionic/components/item/item.scss @@ -137,3 +137,36 @@ ion-input.item { .item.item.item.no-border-bottom + .item { margin-top: -13px; } + +/** + * 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: block; + position: absolute; + top: 0; + right: 0; + z-index: $z-index-item-options; + height: 100%; + + button, [button] { + height: calc(100% - 2px); + margin: 1px 0 1px 0; + + border: none; + border-radius: 0; + display: inline-flex; + align-items: center; + box-sizing: border-box; + + &:before{ + margin: 0 auto; + } + } +} diff --git a/ionic/components/item/item.ts b/ionic/components/item/item.ts index c68d3afea2..19906608ad 100644 --- a/ionic/components/item/item.ts +++ b/ionic/components/item/item.ts @@ -1,6 +1,6 @@ import {Component, Directive, View, ElementRef, NgIf, ViewQuery, QueryList} from 'angular2/angular2'; -import {dom} from 'ionic/util'; +import * as util from 'ionic/util'; /** @@ -33,6 +33,7 @@ import {dom} from 'ionic/util'; '' + ''+ '' + + '' + '', directives: [NgIf] }) @@ -50,55 +51,7 @@ export class Item { this.ele = elementRef.nativeElement; this.swipeButtons = {}; this.optionButtons = {}; + } } - - -class Slideable { - constructor(slideElement: Element) { - } - - // override - onTransform(str: String) {} - // override - onTransitionActive(active: Boolean) {} - //override - onSlideActive(active: boolean) {} - - transform(str: String) { - if (arguments.length && str !== this._transform) { - this.onTransform() - } - } - - isTransitionActive(active: Boolean) { - if (arguments.length && active !== this._isTransitionActive) { - this._isTransitionActive = active - this.onSetTransitionActive(active) - } - return this._isTransitioning - } - - isSlideActive(active: Boolean) { - if (arguments.length && active !== this._isSlideActive) { - this._isSlideActive = active - this.onSetDragActive(active) - } - return this._isSlideActive - } - - isOpen(open: Boolean) { - if (arguments.length && open !== this._isOpen) { - this.isTransitionActive(true) - dom.raf(() => { - this.isOpen = isOpen - this.onSetIsOpen(open) - }) - } - } - -} - -class ItemSlideGesture { -} diff --git a/ionic/components/item/modes/ios.scss b/ionic/components/item/modes/ios.scss index 3e9b89a20f..90f0239c3b 100644 --- a/ionic/components/item/modes/ios.scss +++ b/ionic/components/item/modes/ios.scss @@ -26,6 +26,8 @@ $item-ios-divider-bg: #f5f5f5 !default; $item-ios-divider-color: #222 !default; $item-ios-divider-padding: 5px 15px !default; +$item-ios-sliding-content-bg: $background-color !default; +$item-ios-sliding-transition: transform 250ms ease-in-out !default; .item-group-title { padding: $item-ios-padding-top $item-ios-padding-right $item-ios-padding-bottom $item-ios-padding-left; @@ -157,6 +159,25 @@ $item-ios-divider-padding: 5px 15px !default; } } + + 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; + } } .item.activated, diff --git a/ionic/components/item/test/sliding/e2e.ts b/ionic/components/item/test/sliding/e2e.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/ionic/components/item/test/sliding/e2e.ts @@ -0,0 +1 @@ + diff --git a/ionic/components/item/test/sliding/index.ts b/ionic/components/item/test/sliding/index.ts new file mode 100644 index 0000000000..43aed36502 --- /dev/null +++ b/ionic/components/item/test/sliding/index.ts @@ -0,0 +1,7 @@ +import {App} from 'ionic/ionic'; + + +@App({ + templateUrl: 'main.html' +}) +class E2EApp {} diff --git a/ionic/components/item/test/sliding/main.html b/ionic/components/item/test/sliding/main.html new file mode 100644 index 0000000000..bccfc8fe5f --- /dev/null +++ b/ionic/components/item/test/sliding/main.html @@ -0,0 +1,24 @@ +Sliding Items + + + + + Max Lynch + + Hey do you want to go to the game tonight? + + + Archive + + + + + Adam Bradley + + I think I figured out how to get more Mountain Dew + + + Archive + + + diff --git a/ionic/config/decorators.ts b/ionic/config/decorators.ts index 19012481aa..170c760323 100644 --- a/ionic/config/decorators.ts +++ b/ionic/config/decorators.ts @@ -8,7 +8,7 @@ import { Button, Content, Scroll, Refresher, Slides, Slide, SlideLazy, Tabs, Tab, - Card, List, ListHeader, Item, ItemGroup, ItemGroupTitle, + Card, List, ListHeader, Item, ItemGroup, ItemGroupTitle, ItemSliding Toolbar, ToolbarTitle, ToolbarItem, Icon, Checkbox, Switch, @@ -49,6 +49,7 @@ export const IONIC_DIRECTIVES = [ forwardRef(() => Item), forwardRef(() => ItemGroup), forwardRef(() => ItemGroupTitle), + forwardRef(() => ItemSliding), // Slides forwardRef(() => Slides),
+ Hey do you want to go to the game tonight? +
+ I think I figured out how to get more Mountain Dew +