mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
feat(item): sliding items
This commit is contained in:
@ -10,6 +10,7 @@ export * from 'ionic/components/form/input'
|
|||||||
export * from 'ionic/components/icon/icon'
|
export * from 'ionic/components/icon/icon'
|
||||||
export * from 'ionic/components/item/item'
|
export * from 'ionic/components/item/item'
|
||||||
export * from 'ionic/components/item/item-group'
|
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'
|
||||||
export * from 'ionic/components/menu/menu-types'
|
export * from 'ionic/components/menu/menu-types'
|
||||||
export * from 'ionic/components/menu/menu-toggle'
|
export * from 'ionic/components/menu/menu-toggle'
|
||||||
|
@ -22,6 +22,7 @@ $z-index-backdrop: 1;
|
|||||||
$z-index-overlay-wrapper: 10;
|
$z-index-overlay-wrapper: 10;
|
||||||
|
|
||||||
|
|
||||||
|
$z-index-item-options: 1;
|
||||||
|
|
||||||
// Flex Order
|
// Flex Order
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
204
ionic/components/item/item-sliding.ts
Normal file
204
ionic/components/item/item-sliding.ts
Normal file
@ -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
|
||||||
|
* <ion-list>
|
||||||
|
* <ion-item-sliding *ng-for="#item of items" (click)="itemTapped($event, item)">
|
||||||
|
* {{item.title}}
|
||||||
|
* <div class="item-note" item-right>
|
||||||
|
* {{item.note}}
|
||||||
|
* </div>
|
||||||
|
* </ion-item>
|
||||||
|
* </ion-list>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ion-item-sliding,[ion-item-sliding]',
|
||||||
|
host: {
|
||||||
|
'class': 'item'
|
||||||
|
},
|
||||||
|
properties: [
|
||||||
|
'sliding'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
@View({
|
||||||
|
template:
|
||||||
|
'<ng-content select="[item-left]"></ng-content>' +
|
||||||
|
'<ion-item-sliding-content>' +
|
||||||
|
'<ion-item-content>' +
|
||||||
|
'<ng-content></ng-content>'+
|
||||||
|
'</ion-item-content>' +
|
||||||
|
'<ng-content select="[item-right]"></ng-content>' +
|
||||||
|
'</ion-item-sliding-content>' +
|
||||||
|
'<ng-content select="ion-item-options"></ng-content>',
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -137,3 +137,36 @@ ion-input.item {
|
|||||||
.item.item.item.no-border-bottom + .item {
|
.item.item.item.no-border-bottom + .item {
|
||||||
margin-top: -13px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {Component, Directive, View, ElementRef, NgIf, ViewQuery, QueryList} from 'angular2/angular2';
|
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';
|
|||||||
'<ion-item-content>' +
|
'<ion-item-content>' +
|
||||||
'<ng-content></ng-content>'+
|
'<ng-content></ng-content>'+
|
||||||
'</ion-item-content>' +
|
'</ion-item-content>' +
|
||||||
|
'<ng-content select="ion-item-options"></ng-content>' +
|
||||||
'<ng-content select="[item-right]"></ng-content>',
|
'<ng-content select="[item-right]"></ng-content>',
|
||||||
directives: [NgIf]
|
directives: [NgIf]
|
||||||
})
|
})
|
||||||
@ -50,55 +51,7 @@ export class Item {
|
|||||||
this.ele = elementRef.nativeElement;
|
this.ele = elementRef.nativeElement;
|
||||||
this.swipeButtons = {};
|
this.swipeButtons = {};
|
||||||
this.optionButtons = {};
|
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 {
|
|
||||||
}
|
|
||||||
|
@ -26,6 +26,8 @@ $item-ios-divider-bg: #f5f5f5 !default;
|
|||||||
$item-ios-divider-color: #222 !default;
|
$item-ios-divider-color: #222 !default;
|
||||||
$item-ios-divider-padding: 5px 15px !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 {
|
.item-group-title {
|
||||||
padding: $item-ios-padding-top $item-ios-padding-right $item-ios-padding-bottom $item-ios-padding-left;
|
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,
|
.item.activated,
|
||||||
|
1
ionic/components/item/test/sliding/e2e.ts
Normal file
1
ionic/components/item/test/sliding/e2e.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
7
ionic/components/item/test/sliding/index.ts
Normal file
7
ionic/components/item/test/sliding/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {App} from 'ionic/ionic';
|
||||||
|
|
||||||
|
|
||||||
|
@App({
|
||||||
|
templateUrl: 'main.html'
|
||||||
|
})
|
||||||
|
class E2EApp {}
|
24
ionic/components/item/test/sliding/main.html
Normal file
24
ionic/components/item/test/sliding/main.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<ion-toolbar><ion-title>Sliding Items</ion-title></ion-toolbar>
|
||||||
|
|
||||||
|
<ion-list>
|
||||||
|
|
||||||
|
<ion-item-sliding class="item-text-wrap">
|
||||||
|
<h3>Max Lynch</h3>
|
||||||
|
<p>
|
||||||
|
Hey do you want to go to the game tonight?
|
||||||
|
</p>
|
||||||
|
<ion-item-options>
|
||||||
|
<button primary>Archive</button>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
|
||||||
|
<ion-item-sliding class="item-text-wrap">
|
||||||
|
<h3>Adam Bradley</h3>
|
||||||
|
<p>
|
||||||
|
I think I figured out how to get more Mountain Dew
|
||||||
|
</p>
|
||||||
|
<ion-item-options>
|
||||||
|
<button primary>Archive</button>
|
||||||
|
</ion-item-options>
|
||||||
|
</ion-item-sliding>
|
||||||
|
</ion-list>
|
@ -8,7 +8,7 @@ import {
|
|||||||
Button, Content, Scroll, Refresher,
|
Button, Content, Scroll, Refresher,
|
||||||
Slides, Slide, SlideLazy,
|
Slides, Slide, SlideLazy,
|
||||||
Tabs, Tab,
|
Tabs, Tab,
|
||||||
Card, List, ListHeader, Item, ItemGroup, ItemGroupTitle,
|
Card, List, ListHeader, Item, ItemGroup, ItemGroupTitle, ItemSliding
|
||||||
Toolbar, ToolbarTitle, ToolbarItem,
|
Toolbar, ToolbarTitle, ToolbarItem,
|
||||||
Icon,
|
Icon,
|
||||||
Checkbox, Switch,
|
Checkbox, Switch,
|
||||||
@ -49,6 +49,7 @@ export const IONIC_DIRECTIVES = [
|
|||||||
forwardRef(() => Item),
|
forwardRef(() => Item),
|
||||||
forwardRef(() => ItemGroup),
|
forwardRef(() => ItemGroup),
|
||||||
forwardRef(() => ItemGroupTitle),
|
forwardRef(() => ItemGroupTitle),
|
||||||
|
forwardRef(() => ItemSliding),
|
||||||
|
|
||||||
// Slides
|
// Slides
|
||||||
forwardRef(() => Slides),
|
forwardRef(() => Slides),
|
||||||
|
Reference in New Issue
Block a user