feat(item): two-way sliding of items

closes #5073
This commit is contained in:
Manu Mtz.-Almeida
2016-05-28 12:16:20 +02:00
parent 9ea89ea83c
commit c28aa53478
8 changed files with 588 additions and 286 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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,