mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
@@ -1,221 +1,103 @@
|
||||
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;
|
||||
const MAX_ATTACK_ANGLE = 20;
|
||||
|
||||
export class ItemSlidingGesture extends DragGesture {
|
||||
canDrag: boolean = true;
|
||||
data = {};
|
||||
openItems: number = 0;
|
||||
onTap: any;
|
||||
onMouseOut: any;
|
||||
preventDrag: boolean = false;
|
||||
dragEnded: boolean = true;
|
||||
selectedContainer: ItemSliding = null;
|
||||
openContainer: ItemSliding = null;
|
||||
|
||||
constructor(public list: List, public listEle: HTMLElement) {
|
||||
super(listEle, {
|
||||
direction: 'x',
|
||||
threshold: DRAG_THRESHOLD
|
||||
});
|
||||
|
||||
this.listen();
|
||||
}
|
||||
|
||||
this.onTap = (ev: UIEvent) => {
|
||||
if (!isFromOptionButtons(ev.target)) {
|
||||
let didClose = this.closeOpened();
|
||||
if (didClose) {
|
||||
console.debug('tap close sliding item');
|
||||
preventDefault(ev);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.onMouseOut = (ev: any) => {
|
||||
if (ev.target.tagName === 'ION-ITEM-SLIDING') {
|
||||
console.debug('tap close sliding item');
|
||||
this.onDragEnd(ev);
|
||||
}
|
||||
};
|
||||
onTapCallback(ev: any) {
|
||||
if (isFromOptionButtons(ev.target)) {
|
||||
return;
|
||||
}
|
||||
let didClose = this.closeOpened();
|
||||
if (didClose) {
|
||||
console.debug('tap close sliding item, preventDefault');
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onDragStart(ev: any): boolean {
|
||||
let itemContainerEle = getItemContainer(ev.target);
|
||||
if (!itemContainerEle) {
|
||||
let angle = Math.abs(ev.angle);
|
||||
if (angle > MAX_ATTACK_ANGLE && Math.abs(angle - 180) > MAX_ATTACK_ANGLE) {
|
||||
this.closeOpened();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.selectedContainer) {
|
||||
console.debug('onDragStart, another container is already selected');
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (this.preventDrag) {
|
||||
// Close open container if it is not the selected one.
|
||||
if (container !== this.openContainer) {
|
||||
this.closeOpened();
|
||||
console.debug('onDragStart, preventDefault');
|
||||
preventDefault(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
itemContainerEle.classList.add('active-slide');
|
||||
|
||||
this.set(itemContainerEle, 'offsetX', openAmout);
|
||||
this.set(itemContainerEle, 'startX', ev.center[this.direction]);
|
||||
|
||||
this.dragEnded = false;
|
||||
// Close all item sliding containers but the selected one
|
||||
this.selectedContainer = container;
|
||||
this.openContainer = container;
|
||||
container.startSliding(ev.center.x);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onDrag(ev: any): boolean {
|
||||
if (this.dragEnded || this.preventDrag || Math.abs(ev.deltaY) > 30) {
|
||||
console.debug('onDrag preventDrag, dragEnded:', this.dragEnded, 'preventDrag:', this.preventDrag, 'ev.deltaY:', Math.abs(ev.deltaY));
|
||||
this.preventDrag = true;
|
||||
return;
|
||||
if (this.selectedContainer) {
|
||||
this.selectedContainer.moveSliding(ev.center.x);
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
let itemContainerEle = getItemContainer(ev.target);
|
||||
if (!itemContainerEle || !isActive(itemContainerEle)) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onDragEnd(ev: any) {
|
||||
this.preventDrag = false;
|
||||
this.dragEnded = true;
|
||||
if (this.selectedContainer) {
|
||||
let openAmount = this.selectedContainer.endSliding(ev.velocityX);
|
||||
this.selectedContainer = null;
|
||||
|
||||
let itemContainerEle = getItemContainer(ev.target);
|
||||
if (!itemContainerEle || !isActive(itemContainerEle)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
itemContainerEle.removeEventListener('mouseout', this.onMouseOut);
|
||||
itemData.hasMouseOut = false;
|
||||
|
||||
nativeRaf(() => {
|
||||
this.open(itemContainerEle, restingPoint, true);
|
||||
});
|
||||
}
|
||||
|
||||
closeOpened(doNotCloseEle?: HTMLElement) {
|
||||
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: any, openAmount: number, isFinal: boolean) {
|
||||
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 {
|
||||
// TODO: I am not sure listening for a tap event is the best idea
|
||||
// we should try mousedown/touchstart
|
||||
if (openAmount === 0) {
|
||||
this.openContainer = null;
|
||||
this.off('tap', this.onTap);
|
||||
this.onTap = null;
|
||||
} else if (!this.onTap) {
|
||||
this.onTap = (event: any) => this.onTapCallback(event);
|
||||
this.on('tap', this.onTap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getOpenAmount(itemContainerEle: any) {
|
||||
return this.get(itemContainerEle).openAmount || 0;
|
||||
}
|
||||
|
||||
get(itemContainerEle: any) {
|
||||
return this.data[itemContainerEle && itemContainerEle.$ionSlide] || {};
|
||||
}
|
||||
|
||||
set(itemContainerEle: any, key: any, value: any) {
|
||||
if (!this.data[itemContainerEle.$ionSlide]) {
|
||||
this.data[itemContainerEle.$ionSlide] = {};
|
||||
closeOpened(): boolean {
|
||||
if (this.openContainer) {
|
||||
this.openContainer.close();
|
||||
this.openContainer = null;
|
||||
this.selectedContainer = null;
|
||||
this.off('tap', this.onTap);
|
||||
this.onTap = null;
|
||||
return true;
|
||||
}
|
||||
this.data[itemContainerEle.$ionSlide][key] = value;
|
||||
return false;
|
||||
}
|
||||
|
||||
unlisten() {
|
||||
@@ -224,34 +106,14 @@ export class ItemSlidingGesture extends DragGesture {
|
||||
}
|
||||
}
|
||||
|
||||
function isItemActive(ele: any, isActive: boolean) {
|
||||
ele.classList[isActive ? 'add' : 'remove']('active-slide');
|
||||
ele.classList[isActive ? 'add' : 'remove']('active-options');
|
||||
function getContainer(ev: any): ItemSliding {
|
||||
let ele = closest(ev.target, 'ion-item-sliding', true);
|
||||
if (ele) {
|
||||
return ele['$ionComponent'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function preventDefault(ev: any) {
|
||||
console.debug('sliding item preventDefault', ev.type);
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function getItemContainer(ele: any) {
|
||||
return closest(ele, 'ion-item-sliding', true);
|
||||
}
|
||||
|
||||
function isFromOptionButtons(ele: any) {
|
||||
function isFromOptionButtons(ele: HTMLElement): boolean {
|
||||
return !!closest(ele, 'ion-item-options', true);
|
||||
}
|
||||
|
||||
function getOptionsWidth(itemContainerEle: any) {
|
||||
let optsEle = itemContainerEle.querySelector('ion-item-options');
|
||||
if (optsEle) {
|
||||
return optsEle.offsetWidth;
|
||||
}
|
||||
}
|
||||
|
||||
function isActive(itemContainerEle: any) {
|
||||
return itemContainerEle.classList.contains('active-slide');
|
||||
}
|
||||
|
||||
|
||||
const DRAG_THRESHOLD = 20;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// --------------------------------------------------
|
||||
// The hidden right-side buttons that can be exposed under a list item with dragging.
|
||||
|
||||
|
||||
ion-item-sliding {
|
||||
position: relative;
|
||||
display: block;
|
||||
@@ -22,23 +21,35 @@ ion-item-options {
|
||||
z-index: $z-index-item-options;
|
||||
display: none;
|
||||
|
||||
justify-content: flex-end;
|
||||
|
||||
height: 100%;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
ion-item-options[side=left] {
|
||||
right: auto;
|
||||
left: 0;
|
||||
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
ion-item-options .button {
|
||||
margin: 0;
|
||||
padding: 0 .7em;
|
||||
|
||||
height: 100%;
|
||||
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
ion-item-options:not([icon-left]) .button-icon-left {
|
||||
font-size: 14px;
|
||||
|
||||
.button-inner {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -60,15 +71,54 @@ ion-item-sliding.active-slide {
|
||||
opacity: 1;
|
||||
transition: all 300ms cubic-bezier(.36, .66, .04, 1);
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
ion-item-options {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Item Swipeable Animation
|
||||
// --------------------------------------------------
|
||||
|
||||
button[swipeable] {
|
||||
flex-shrink: 0;
|
||||
|
||||
transition-duration: 0;
|
||||
transition-property: none;
|
||||
transition-timing-function: cubic-bezier(.65, .05, .36, 1);
|
||||
}
|
||||
|
||||
ion-item-sliding.active-swipe-right,
|
||||
ion-item-sliding.active-swipe-left {
|
||||
ion-item-options {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
ion-item-sliding.active-swipe-right button[swipeable] {
|
||||
order: 1;
|
||||
|
||||
padding-left: 90%;
|
||||
|
||||
transition-duration: .6s;
|
||||
transition-property: padding-left;
|
||||
}
|
||||
|
||||
ion-item-sliding.active-swipe-left button[swipeable] {
|
||||
order: -1;
|
||||
|
||||
padding-right: 90%;
|
||||
|
||||
transition-duration: .6s;
|
||||
transition-property: padding-right;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,61 @@
|
||||
import {Component, ElementRef, Optional, ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, ContentChildren, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, Input, Optional, Output, QueryList, 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';
|
||||
|
||||
const SWIPE_FACTOR = 1.1;
|
||||
const ELASTIC_FACTOR = 0.55;
|
||||
|
||||
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;
|
||||
@Output() ionSwipe: EventEmitter<ItemSliding> = new EventEmitter();
|
||||
|
||||
constructor(elementRef: ElementRef, private _renderer: Renderer) {
|
||||
super(elementRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setCssStyle(property: string, value: string) {
|
||||
this._renderer.setElementStyle(this.elementRef.nativeElement, property, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getSides(): SideFlags {
|
||||
if (isPresent(this.side) && this.side === 'left') {
|
||||
return SideFlags.Left;
|
||||
} else {
|
||||
return SideFlags.Right;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const enum SlidingState {
|
||||
Disabled = 0,
|
||||
Enabled = 1,
|
||||
Right = 2,
|
||||
Left = 3
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -11,14 +66,22 @@ import {List} from '../list/list';
|
||||
* an [Item](../Item) component as a child and a [List](../../list/List) component as
|
||||
* a parent. All buttons to reveal can be placed in the `<ion-item-options>` element.
|
||||
*
|
||||
* ### Button Layout
|
||||
* If an icon is placed with text in the option button, by default it will
|
||||
* display the icon on top of the text. This can be changed to display the icon
|
||||
* to the left of the text by setting `icon-left` as an attribute on the
|
||||
* `<ion-item-options>` element.
|
||||
* ### Swipe Direction
|
||||
* By default, the buttons are revealed when the sliding item is swiped from right to left,
|
||||
* so the buttons are placed in the right side. But it's also possible to reveal them
|
||||
* in the right side (sliding from left to right) by setting the `side` attribute
|
||||
* on the `ion-item-options` element. Up to 2 `ion-item-options` can used at the same time
|
||||
* in order to reveal two different sets of buttons depending the swipping direction.
|
||||
*
|
||||
* ```html
|
||||
* <ion-item-options icon-left>
|
||||
* <ion-item-options side="right">
|
||||
* <button (click)="archive(item)">
|
||||
* <ion-icon name="archive"></ion-icon>
|
||||
* Archive
|
||||
* </button>
|
||||
* </ion-item-options>
|
||||
|
||||
* <ion-item-options>
|
||||
* <button (click)="archive(item)">
|
||||
* <ion-icon name="archive"></ion-icon>
|
||||
* Archive
|
||||
@@ -26,11 +89,45 @@ import {List} from '../list/list';
|
||||
* </ion-item-options>
|
||||
* ```
|
||||
*
|
||||
* ### Listening for events (ionDrag) and (ionSwipe)
|
||||
* It's possible to know the current relative position of the sliding item by subscribing
|
||||
* to the (ionDrag)` event.
|
||||
*
|
||||
* ```html
|
||||
* <ion-item-options side="right">
|
||||
* <button (click)="archive(item)">
|
||||
* <ion-icon name="archive"></ion-icon>
|
||||
* Archive
|
||||
* </button>
|
||||
* </ion-item-options>
|
||||
|
||||
* <ion-item-options>
|
||||
* <button (click)="archive(item)">
|
||||
* <ion-icon name="archive"></ion-icon>
|
||||
* Archive
|
||||
* </button>
|
||||
* </ion-item-options>
|
||||
* ```
|
||||
*
|
||||
* ### Button Layout
|
||||
* If an icon is placed with text in the option button, by default it will
|
||||
* display the icon on top of the text. This can be changed to display the icon
|
||||
* to the left of the text by setting `icon-left` as an attribute on the
|
||||
* `<ion-item-options>` element.
|
||||
*
|
||||
* ```html
|
||||
* <ion-item-sliding (ionDrag)="ondrag($event)">
|
||||
* <ion-item>Item</ion-item>
|
||||
* <ion-item-options>
|
||||
* <button>Favorite</button>
|
||||
* </ion-item-options>
|
||||
* </ion-item-sliding>
|
||||
* ```
|
||||
*
|
||||
* @usage
|
||||
* ```html
|
||||
* <ion-list>
|
||||
* <ion-item-sliding>
|
||||
* <ion-item-sliding #item>
|
||||
* <ion-item>
|
||||
* Item
|
||||
* </ion-item>
|
||||
@@ -38,6 +135,10 @@ import {List} from '../list/list';
|
||||
* <button (click)="favorite(item)">Favorite</button>
|
||||
* <button danger (click)="share(item)">Share</button>
|
||||
* </ion-item-options>
|
||||
|
||||
* <ion-item-options side="right">
|
||||
* <button (click)="unread(item)">Unread</button>
|
||||
* </ion-item-options>
|
||||
* </ion-item-sliding>
|
||||
* </ion-list>
|
||||
* ```
|
||||
@@ -53,13 +154,210 @@ import {List} from '../list/list';
|
||||
'<ng-content select="ion-item,[ion-item]"></ng-content>' +
|
||||
'<ng-content select="ion-item-options"></ng-content>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class ItemSliding {
|
||||
private _openAmount: number = 0;
|
||||
private _startX: 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;
|
||||
private _state: SlidingState = SlidingState.Disabled;
|
||||
slidingPercent: number = 0;
|
||||
|
||||
constructor(@Optional() private _list: List, elementRef: ElementRef) {
|
||||
@ContentChild(Item) private item: Item;
|
||||
|
||||
|
||||
/**
|
||||
* @output {event} Expression to evaluate when the sliding position changes.
|
||||
* It reports the relative position.
|
||||
*
|
||||
* ```ts
|
||||
* ondrag(percent) {
|
||||
* if (percent > 0) {
|
||||
* // positive
|
||||
* console.log('right side');
|
||||
* } else {
|
||||
* // negative
|
||||
* console.log('left side');
|
||||
* }
|
||||
* if (Math.abs(percent) > 1) {
|
||||
* console.log('overscroll');
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
@Output() ionDrag: EventEmitter<number> = new EventEmitter();
|
||||
|
||||
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: QueryList<ItemOptions>) {
|
||||
let 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) {
|
||||
if (this._openAmount === 0) {
|
||||
this._optsDirty = true;
|
||||
this._setState(SlidingState.Enabled);
|
||||
}
|
||||
this._startX = startX + this._openAmount;
|
||||
this.item.setCssStyle(CSS.transition, 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
moveSliding(x: number): number {
|
||||
this.calculateOptsWidth();
|
||||
|
||||
let openAmount = this._startX - x;
|
||||
switch (this._sides) {
|
||||
case SideFlags.Right: openAmount = Math.max(0, openAmount); break;
|
||||
case SideFlags.Left: openAmount = Math.min(0, openAmount); break;
|
||||
case SideFlags.Both: break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
if (openAmount > this._optsWidthRightSide) {
|
||||
var optsWidth = this._optsWidthRightSide;
|
||||
openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR;
|
||||
|
||||
} else if (openAmount < -this._optsWidthLeftSide) {
|
||||
var optsWidth = -this._optsWidthLeftSide;
|
||||
openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR;
|
||||
}
|
||||
|
||||
this._setOpenAmount(openAmount, false);
|
||||
return openAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
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
|
||||
let isOnResetZone = Math.abs(this._openAmount) < Math.abs(restingPoint / 2);
|
||||
let isMovingSlow = Math.abs(velocity) < 0.3;
|
||||
let isDirection = (this._openAmount > 0) === (velocity > 0);
|
||||
if (isOnResetZone && (isMovingSlow || isDirection)) {
|
||||
restingPoint = 0;
|
||||
}
|
||||
|
||||
this.fireSwipeEvent();
|
||||
this._setOpenAmount(restingPoint, true);
|
||||
return restingPoint;
|
||||
}
|
||||
|
||||
fireSwipeEvent() {
|
||||
if (this.slidingPercent > SWIPE_FACTOR) {
|
||||
this._rightOptions.ionSwipe.emit(this);
|
||||
} else if (this.slidingPercent < -SWIPE_FACTOR) {
|
||||
this._leftOptions.ionSwipe.emit(this);
|
||||
}
|
||||
}
|
||||
|
||||
calculateOptsWidth() {
|
||||
if (this._optsDirty) {
|
||||
if (this._rightOptions) {
|
||||
this._optsWidthRightSide = this._rightOptions.width();
|
||||
}
|
||||
if (this._leftOptions) {
|
||||
this._optsWidthLeftSide = this._leftOptions.width();
|
||||
}
|
||||
this._optsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private _setOpenAmount(openAmount: number, isFinal: boolean) {
|
||||
if (this._timer) {
|
||||
clearTimeout(this._timer);
|
||||
this._timer = null;
|
||||
}
|
||||
this._openAmount = 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._setState(SlidingState.Disabled);
|
||||
this._timer = null;
|
||||
}, 400);
|
||||
this.slidingPercent = 0;
|
||||
|
||||
} else if (openAmount > 0) {
|
||||
this._setState(SlidingState.Right);
|
||||
this.slidingPercent = openAmount / this._optsWidthRightSide;
|
||||
} else if (openAmount < 0) {
|
||||
this._setState(SlidingState.Left);
|
||||
this.slidingPercent = openAmount / this._optsWidthLeftSide;
|
||||
}
|
||||
if (!isFinal) {
|
||||
this.setClass('active-swipe-right', this.slidingPercent > SWIPE_FACTOR);
|
||||
this.setClass('active-swipe-left', this.slidingPercent < -SWIPE_FACTOR);
|
||||
} else {
|
||||
this.item.setCssStyle(CSS.transition, '');
|
||||
}
|
||||
|
||||
this.ionDrag.emit(this.slidingPercent);
|
||||
this.item.setCssStyle(CSS.transform, (didEnd ? '' : 'translate3d(' + -openAmount + 'px,0,0)'));
|
||||
}
|
||||
|
||||
private _setState(state: SlidingState) {
|
||||
if (state !== this._state) {
|
||||
this._state = state;
|
||||
this.setClass('active-slide', state !== SlidingState.Disabled);
|
||||
this.setClass('active-options-right', state === SlidingState.Right);
|
||||
this.setClass('active-options-left', state === SlidingState.Left);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setClass(className: string, add: boolean) {
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, className, add);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getOpenAmount(): number {
|
||||
return this._openAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,9 +395,7 @@ export class ItemSliding {
|
||||
* ```
|
||||
*/
|
||||
close() {
|
||||
this._list.closeSlidingItems();
|
||||
this._setOpenAmount(0, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let slideIds = 0;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Component, ViewChild} from '@angular/core';
|
||||
import {ionicBootstrap, App, Alert, NavController, List, ItemSliding} from '../../../../../src';
|
||||
import {ionicBootstrap, App, Alert, NavController, List, ItemSliding, Toast} from '../../../../../src';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -8,7 +8,7 @@ import {ionicBootstrap, App, Alert, NavController, List, ItemSliding} from '../.
|
||||
class E2EPage {
|
||||
@ViewChild('myList', {read: List}) list: List;
|
||||
|
||||
items = [];
|
||||
items: number[] = [];
|
||||
shouldShow: boolean = true;
|
||||
|
||||
constructor(private app: App, private nav: NavController) {
|
||||
@@ -21,7 +21,14 @@ class E2EPage {
|
||||
this.list.closeSlidingItems();
|
||||
}
|
||||
|
||||
didClick(item) {
|
||||
unread(item: ItemSliding) {
|
||||
if (item) {
|
||||
item.close();
|
||||
}
|
||||
console.log('UNREAD', item);
|
||||
}
|
||||
|
||||
didClick(item: ItemSliding) {
|
||||
console.log('Clicked, ion-item');
|
||||
|
||||
let alert = Alert.create({
|
||||
@@ -31,7 +38,7 @@ class E2EPage {
|
||||
this.nav.present(alert);
|
||||
}
|
||||
|
||||
archive(item) {
|
||||
archive(item: ItemSliding) {
|
||||
console.log('Archive, ion-item-options button', item);
|
||||
|
||||
let alert = Alert.create({
|
||||
@@ -46,7 +53,7 @@ class E2EPage {
|
||||
this.nav.present(alert);
|
||||
}
|
||||
|
||||
del(item) {
|
||||
del(item: ItemSliding) {
|
||||
console.log('Delete ion-item-options button', item);
|
||||
|
||||
let alert = Alert.create({
|
||||
@@ -61,6 +68,21 @@ class E2EPage {
|
||||
this.nav.present(alert);
|
||||
}
|
||||
|
||||
download(item: ItemSliding) {
|
||||
item.setClass('downloading', true);
|
||||
setTimeout(() => {
|
||||
const toast = Toast.create({
|
||||
message: 'Item was downloaded!'
|
||||
});
|
||||
this.nav.present(toast);
|
||||
item.setClass('downloading', false);
|
||||
item.close();
|
||||
setTimeout(() => {
|
||||
toast.dismiss();
|
||||
}, 2000);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
reload() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
<ion-toolbar>
|
||||
<ion-title>Sliding Items</ion-title>
|
||||
<ion-buttons end>
|
||||
@@ -11,10 +12,8 @@
|
||||
|
||||
<ion-item-sliding #item>
|
||||
<button ion-item text-wrap (click)="didClick(item)">
|
||||
<h3>Max Lynch</h3>
|
||||
<p>
|
||||
Hey do you want to go to the game tonight?
|
||||
</p>
|
||||
<h2>RIGHT side - no icons</h2>
|
||||
<p>Hey do you want to go to the game tonight?</p>
|
||||
</button>
|
||||
<ion-item-options>
|
||||
<button primary (click)="archive(item)">Archive</button>
|
||||
@@ -22,51 +21,76 @@
|
||||
</ion-item-options>
|
||||
</ion-item-sliding>
|
||||
|
||||
<ion-item-sliding #item>
|
||||
<ion-item-sliding #item1>
|
||||
<ion-item text-wrap detail-push class="activated">
|
||||
<h3>Adam Bradley</h3>
|
||||
<p>
|
||||
I think I figured out how to get more Mountain Dew
|
||||
</p>
|
||||
<h2>LEFT side - no icons</h2>
|
||||
<p>I think I figured out how to get more Mountain Dew</p>
|
||||
</ion-item>
|
||||
<ion-item-options>
|
||||
<button primary (click)="archive(item)">
|
||||
<ion-icon name="mail"></ion-icon>
|
||||
Archive
|
||||
</button>
|
||||
<button danger (click)="del(item)">
|
||||
<ion-icon name="trash"></ion-icon>
|
||||
Delete
|
||||
</button>
|
||||
<ion-item-options side="left">
|
||||
<button primary (click)="archive(item1)">Archive</button>
|
||||
<button danger (click)="del(item1)" swipeable>Delete</button>
|
||||
</ion-item-options>
|
||||
</ion-item-sliding>
|
||||
|
||||
<ion-item-sliding *ngIf="shouldShow" #item>
|
||||
|
||||
<ion-item-sliding #item2 *ngIf="shouldShow">
|
||||
<ion-item text-wrap detail-push>
|
||||
<h3>Ben Sperry</h3>
|
||||
<p>
|
||||
I like paper
|
||||
</p>
|
||||
<h2>RIGHT/LEFT side - icons</h2>
|
||||
<p>I think I figured out how to get more Mountain Dew</p>
|
||||
</ion-item>
|
||||
<ion-item-options>
|
||||
<button primary (click)="archive(item)">Archive</button>
|
||||
<button danger (click)="del(item)">Delete</button>
|
||||
<ion-item-options side="left" (ionSwipe)="unread($event)">
|
||||
<button secondary swipeable (click)="unread(item2)">
|
||||
<ion-icon name="ios-checkmark"></ion-icon>Unread
|
||||
</button>
|
||||
</ion-item-options>
|
||||
|
||||
<ion-item-options side="right" (ionSwipe)="del(item2)">
|
||||
<button primary (click)="archive(item2)">
|
||||
<ion-icon name="mail"></ion-icon>Archive
|
||||
</button>
|
||||
<button danger (click)="del(item2)" swipeable>
|
||||
<ion-icon name="trash"></ion-icon>Delete
|
||||
</button>
|
||||
</ion-item-options>
|
||||
</ion-item-sliding>
|
||||
|
||||
<ion-item-sliding #item>
|
||||
|
||||
<ion-item-sliding #item3 *ngIf="shouldShow">
|
||||
<ion-item text-wrap detail-push>
|
||||
<h2>RIGHT/LEFT side - icons (item-left)</h2>
|
||||
<p>I think I figured out how to get more Mountain Dew</p>
|
||||
</ion-item>
|
||||
<ion-item-options side="left" icon-left (ionSwipe)="unread($event)">
|
||||
<button secondary swipeable (click)="unread(item3)">
|
||||
<ion-icon name="ios-checkmark"></ion-icon>Unread
|
||||
</button>
|
||||
</ion-item-options>
|
||||
|
||||
<ion-item-options icon-left (ionSwipe)="del(item3)">
|
||||
<button primary (click)="archive(item3)">
|
||||
<ion-icon name="mail"></ion-icon>Archive
|
||||
</button>
|
||||
<button danger (click)="del(item3)" swipeable>
|
||||
<ion-icon name="trash"></ion-icon>Delete
|
||||
</button>
|
||||
</ion-item-options>
|
||||
</ion-item-sliding>
|
||||
|
||||
|
||||
<ion-item-sliding #item4>
|
||||
<ion-item>
|
||||
<ion-icon name="mail" item-left></ion-icon>
|
||||
One Line w/ Icon, div only text
|
||||
</ion-item>
|
||||
<ion-item-options icon-left>
|
||||
<button primary (click)="archive(item)">
|
||||
<ion-item-options icon-left (ionSwipe)="archive($event)">
|
||||
<button primary (click)="archive(item4)" swipeable>
|
||||
<ion-icon name="archive"></ion-icon>Archive
|
||||
</button>
|
||||
</ion-item-options>
|
||||
</ion-item-sliding>
|
||||
|
||||
<ion-item-sliding #item>
|
||||
|
||||
<ion-item-sliding #item5>
|
||||
<ion-item>
|
||||
<ion-avatar item-left>
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw==">
|
||||
@@ -74,45 +98,76 @@
|
||||
One Line w/ Avatar, div only text
|
||||
</ion-item>
|
||||
<ion-item-options>
|
||||
<button primary>
|
||||
<button primary swipeable>
|
||||
<ion-icon name="more"></ion-icon>More
|
||||
</button>
|
||||
<button secondary (click)="archive(item)">
|
||||
<button secondary (click)="archive(item5)">
|
||||
<ion-icon name="archive"></ion-icon>Archive
|
||||
</button>
|
||||
<button danger (click)="del(item)">
|
||||
<button danger (click)="del(item5)">
|
||||
<ion-icon name="trash"></ion-icon>Delete
|
||||
</button>
|
||||
|
||||
</ion-item-options>
|
||||
</ion-item-sliding>
|
||||
|
||||
<ion-item-sliding #item>
|
||||
<ion-item-sliding #item6>
|
||||
<ion-item>
|
||||
<ion-thumbnail item-left>
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw==">
|
||||
</ion-thumbnail>
|
||||
<h2>Two Lines w/ Thumbnail, H2 Header</h2>
|
||||
<h2>DOWNLOAD</h2>
|
||||
<p>Paragraph text.</p>
|
||||
</ion-item>
|
||||
<ion-item-options>
|
||||
<button primary (click)="archive(item)">Archive</button>
|
||||
<ion-item-options (ionSwipe)="download($event)">
|
||||
<button primary (click)="archive(item6)">
|
||||
<ion-icon name="archive"></ion-icon>Archive
|
||||
</button>
|
||||
<button secondary swipeable (click)="download(item6)">
|
||||
<ion-icon name="download" class="download-hide"></ion-icon>
|
||||
<div class="download-hide">Download</div>
|
||||
<ion-spinner id="download-spinner"></ion-spinner>
|
||||
</button>
|
||||
</ion-item-options>
|
||||
</ion-item-sliding>
|
||||
|
||||
<ion-item-sliding *ngFor="let data of items" #item>
|
||||
<ion-item-sliding>
|
||||
<ion-item>
|
||||
<ion-thumbnail item-left>
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw==">
|
||||
</ion-thumbnail>
|
||||
<h2>ion-item-sliding without options (no sliding)</h2>
|
||||
<p>Paragraph text.</p>
|
||||
</ion-item>
|
||||
</ion-item-sliding>
|
||||
|
||||
<ion-item text-wrap>
|
||||
<h2>Normal ion-item (no sliding)</h2>
|
||||
<p>Paragraph text.</p>
|
||||
</ion-item>
|
||||
|
||||
<button ion-item text-wrap (click)="didClick(item)">
|
||||
<h2>Normal button (no sliding)</h2>
|
||||
<p>Hey do you want to go to the game tonight?</p>
|
||||
</button>
|
||||
|
||||
</ion-list>
|
||||
|
||||
|
||||
<ion-list>
|
||||
<ion-item-sliding *ngFor="let data of items" #item8>
|
||||
<ion-item text-wrap detail-push>
|
||||
<h3>ng-for {{data}}</h3>
|
||||
</ion-item>
|
||||
<ion-item-options>
|
||||
<button primary (click)="archive(item)">Archive</button>
|
||||
<button primary (click)="archive(item8)">Archive</button>
|
||||
</ion-item-options>
|
||||
</ion-item-sliding>
|
||||
|
||||
</ion-list>
|
||||
|
||||
<p>
|
||||
<button (click)="closeOpened()">Close opened items</button>
|
||||
<button block (click)="closeOpened()">Close opened items</button>
|
||||
</p>
|
||||
|
||||
</ion-content>
|
||||
@@ -121,4 +176,17 @@
|
||||
img {
|
||||
height: 100px;
|
||||
}
|
||||
#download-spinner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
svg circle {
|
||||
stroke: white;
|
||||
}
|
||||
.downloading #download-spinner {
|
||||
display: block;
|
||||
}
|
||||
.downloading .download-hide {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 (this._enableSliding === shouldEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldEnable) {
|
||||
console.debug('enableSlidingItems');
|
||||
this._zone.runOutsideAngular(() => {
|
||||
setTimeout(() => {
|
||||
this.slidingGesture = new ItemSlidingGesture(this, this.ele);
|
||||
});
|
||||
});
|
||||
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();
|
||||
}
|
||||
} 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;
|
||||
|
||||
@@ -19,7 +19,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';
|
||||
@@ -137,6 +137,7 @@ export const IONIC_DIRECTIVES: any[] = [
|
||||
ListHeader,
|
||||
Item,
|
||||
ItemSliding,
|
||||
ItemOptions,
|
||||
VirtualScroll,
|
||||
VirtualItem,
|
||||
VirtualHeader,
|
||||
|
||||
Reference in New Issue
Block a user