refactor(item-sliding)

This commit is contained in:
Adam Bradley
2015-11-05 15:03:53 -06:00
parent c8640150f7
commit b1438d9cf4
17 changed files with 453 additions and 466 deletions

View File

@ -0,0 +1,187 @@
import {Hammer} from 'ionic/gestures/hammer';
import {DragGesture} from 'ionic/gestures/drag-gesture';
import {List} from '../list/list';
import * as util from 'ionic/util';
import {CSS, raf, closest} from 'ionic/util/dom';
export class ItemSlidingGesture extends DragGesture {
constructor(list: List, listEle) {
super(listEle, {
direction: 'x',
threshold: list.width()
});
this.data = {};
this.openItems = 0;
this.list = list;
this.listEle = listEle;
this.canDrag = true;
this.listen();
this.on('tap', ev => {
if (!isFromOptionButtons(ev.target)) {
let didClose = this.closeOpened();
if (didClose) {
ev.preventDefault();
}
}
});
}
onDragStart(ev) {
let itemContainerEle = getItemConatiner(ev.target);
if (!itemContainerEle) return;
this.closeOpened(ev, itemContainerEle);
let openAmout = this.getOpenAmount(itemContainerEle);
let itemData = this.getData(itemContainerEle);
if (openAmout) {
return ev.preventDefault();
}
itemContainerEle.classList.add('active-slide');
this.setData(itemContainerEle, 'offsetX', openAmout);
this.setData(itemContainerEle, 'startX', ev.center[this.direction]);
}
onDrag(ev) {
let itemContainerEle = getItemConatiner(ev.target);
if (!itemContainerEle || !isActive(itemContainerEle)) return;
let itemData = this.getData(itemContainerEle);
if (!itemData.optsWidth) {
itemData.optsWidth = getOptionsWidth(itemContainerEle);
if (!itemData.optsWidth) return;
}
let x = ev.center[this.direction];
let delta = x - itemData.startX;
let newX = Math.max(0, itemData.offsetX - delta);
if (newX > itemData.optsWidth) {
// Calculate the new X position, capped at the top of the buttons
newX = -Math.min(-itemData.optsWidth, -itemData.optsWidth + (((delta + itemData.optsWidth) * 0.4)));
}
this.open(itemContainerEle, newX, false);
}
onDragEnd(ev) {
let itemContainerEle = getItemConatiner(ev.target);
if (!itemContainerEle || !isActive(itemContainerEle)) return;
// If we are currently dragging, we want to snap back into place
// The final resting point X will be the width of the exposed buttons
let itemData = this.getData(itemContainerEle);
var restingPoint = itemData.optsWidth;
// Check if the drag didn't clear the buttons mid-point
// and we aren't moving fast enough to swipe open
if (this.getOpenAmount(itemContainerEle) < (restingPoint / 2)) {
// If we are going left but too slow, or going right, go back to resting
if (ev.direction & Hammer.DIRECTION_RIGHT) {
// Left
restingPoint = 0;
} else if (Math.abs(ev.velocityX) < 0.3) {
// Right
restingPoint = 0;
}
}
this.setData(itemContainerEle, 'opened', restingPoint > 0);
raf(() => {
this.open(itemContainerEle, restingPoint, true);
});
}
closeOpened(ev, doNotCloseEle) {
let didClose = false;
if (this.openItems) {
let openItemElements = this.listEle.querySelectorAll('.active-slide');
for (let i = 0; i < openItemElements.length; i++) {
if (openItemElements[i] !== doNotCloseEle) {
this.open(openItemElements[i], 0, true);
didClose = true;
}
}
}
return didClose;
}
open(itemContainerEle, openAmount, animate) {
let slidingEle = itemContainerEle.querySelector('ion-item');
if (!slidingEle) return;
this.setData(itemContainerEle, 'openAmount', openAmount);
clearTimeout(this.getData(itemContainerEle).timerId);
if (openAmount > 0) {
this.openItems++;
} else {
let timerId = setTimeout(() => {
if (slidingEle.style[CSS.transform] === '') {
itemContainerEle.classList.remove('active-slide');
this.openItems--;
}
}, 400);
this.setData(itemContainerEle, 'timerId', timerId);
}
slidingEle.style[CSS.transform] = (openAmount === 0 ? '' : 'translate3d(' + -openAmount + 'px,0,0)');
slidingEle.style[CSS.transition] = (animate ? '' : 'none');
}
getOpenAmount(itemContainerEle) {
return this.getData(itemContainerEle).openAmount || 0;
}
getData(itemContainerEle) {
return this.data[itemContainerEle && itemContainerEle.$ionSlide] || {};
}
setData(itemContainerEle, key, value) {
if (!this.data[itemContainerEle.$ionSlide]) {
this.data[itemContainerEle.$ionSlide] = {};
}
this.data[itemContainerEle.$ionSlide][key] = value;
}
unlisten() {
super.unlisten();
this.listEle = null;
}
}
function getItemConatiner(ele) {
return closest(ele, 'ion-item-sliding', true);
}
function isFromOptionButtons(ele) {
return !!closest(ele, 'ion-item-options', true);
}
function getOptionsWidth(itemContainerEle) {
let optsEle = itemContainerEle.querySelector('ion-item-options');
if (optsEle) {
return optsEle.offsetWidth;
}
}
function isActive(itemContainerEle) {
return itemContainerEle.classList.contains('active-slide');
}

View File

@ -0,0 +1,36 @@
/**
* The hidden right-side buttons that can be exposed under a list item
* with dragging.
*/
ion-item-sliding {
display: block;
position: relative;
overflow: hidden;
.item {
position: static;
}
}
ion-item-options {
display: none;
position: absolute;
top: 0;
right: 0;
z-index: $z-index-item-options;
height: 100%;
}
ion-item-sliding.active-slide {
.item {
position: relative;
z-index: $z-index-item-options + 1;
}
ion-item-options {
display: flex;
}
}

View File

@ -1,31 +1,7 @@
import {Component, Directive, ElementRef, NgIf, Host, Optional, Renderer, NgZone} from 'angular2/angular2'; import {Component, ElementRef, Optional} from 'angular2/angular2';
import {Gesture} from 'ionic/gestures/gesture'; import {List} from '../list/list';
import {DragGesture} from 'ionic/gestures/drag-gesture';
import {Hammer} from 'ionic/gestures/hammer';
import {List} from 'ionic/components/list/list';
import * as util from 'ionic/util';
import {CSS, raf} from 'ionic/util/dom';
@Directive({
selector: 'ion-item-options > button,ion-item-options > [button]',
host: {
'(click)': 'clicked($event)'
}
})
export class ItemSlidingOptionButton {
constructor(elementRef: ElementRef) {
}
clicked(event) {
// Don't allow the click to propagate
event.preventDefault();
event.stopPropagation();
}
}
/** /**
* @name ionItem * @name ionItem
@ -36,238 +12,35 @@ export class ItemSlidingOptionButton {
* @usage * @usage
* ```html * ```html
* <ion-list> * <ion-list>
* <ion-item-sliding *ng-for="#item of items" (click)="itemTapped($event, item)"> * <ion-item-sliding *ng-for="#item of items">
* {{item.title}} * <ion-item (click)="itemTapped(item)">
* <div class="item-note" item-right> * {{item.title}}
* {{item.note}} * </ion-item>
* </div> * <ion-item-options>
* <button (click)="favorite(item)">Favorite</button>
* <button (click)="share(item)">Share</button>
* </ion-item-options>
* </ion-item> * </ion-item>
* </ion-list> * </ion-list>
* ``` * ```
*/ */
@Component({ @Component({
selector: 'ion-item-sliding', selector: 'ion-item-sliding',
inputs: [
'sliding'
],
template: template:
'<ng-content select="ion-item-options"></ng-content>' + '<ng-content select="ion-item"></ng-content>' +
'<ion-item-sliding-content>' + '<ng-content select="ion-item-options"></ng-content>'
'<ng-content select="[item-left]"></ng-content>' +
'<ng-content select="[item-right]"></ng-content>' +
'<ion-item-content>' +
'<ng-content></ng-content>'+
'</ion-item-content>' +
'</ion-item-sliding-content>'
}) })
export class ItemSliding { export class ItemSliding {
/**
* TODO
* @param {ElementRef} elementRef A reference to the component's DOM element.
*/
constructor(elementRef: ElementRef, renderer: Renderer, @Optional() @Host() list: List, zone: NgZone) {
this._zone = zone;
renderer.setElementClass(elementRef, 'item', true);
renderer.setElementAttribute(elementRef, 'tappable', '');
this._isOpen = false; constructor(@Optional() private list: List, elementRef: ElementRef) {
this._isSlideActive = false; list.enableSlidingItems(true);
this._isTransitioning = false; elementRef.nativeElement.$ionSlide = ++slideIds;
this._transform = '';
this.list = list;
this.elementRef = elementRef;
this.swipeButtons = {};
this.optionButtons = {};
} }
onInit() { close() {
let ele = this.elementRef.nativeElement; this.list.closeSlidingItems();
this.itemSlidingContent = ele.querySelector('ion-item-sliding-content');
this.itemOptions = ele.querySelector('ion-item-options');
this.openAmount = 0;
this._zone.runOutsideAngular(() => {
this.gesture = new ItemSlideGesture(this, this.itemSlidingContent, this._zone);
});
} }
onDestroy() {
this.gesture && this.gesture.unlisten();
this.itemSlidingContent = this.itemOptionsContent = null;
}
close(andStopDrag) {
this.openAmount = 0;
// Enable it once, it'll get disabled on the next drag
raf(() => {
this.enableAnimation();
if (this.itemSlidingContent) {
this.itemSlidingContent.style[CSS.transform] = 'translateX(0)';
}
});
}
open(amt) {
let el = this.itemSlidingContent;
this.openAmount = amt || 0;
if (this.list) {
this.list.setOpenItem(this);
}
if (amt === '') {
el.style[CSS.transform] = '';
} else {
el.style[CSS.transform] = 'translateX(' + -amt + 'px)';
}
}
isOpen() {
return this.openAmount > 0;
}
getOpenAmt() {
return this.openAmount;
}
disableAnimation() {
this.itemSlidingContent.style[CSS.transition] = 'none';
}
enableAnimation() {
// Clear the explicit transition, allow for CSS one to take over
this.itemSlidingContent.style[CSS.transition] = '';
}
/**
* User did a touchstart
*/
didTouch() {
if (this.isOpen()) {
this.close();
this.didClose = true;
} else {
let openItem = this.list.getOpenItem();
if (openItem && openItem !== this) {
this.didClose = true;
}
if (this.list) {
this.list.closeOpenItem();
}
}
}
} }
class ItemSlideGesture extends DragGesture {
constructor(item: ItemSliding, el: Element, zone) {
super(el, {
direction: 'x',
threshold: el.offsetWidth
});
this.item = item; let slideIds = 0;
this.canDrag = true;
this.listen();
zone.runOutsideAngular(() => {
let touchStart = (e) => {
this.item.didTouch();
raf(() => {
this.item.itemOptionsWidth = this.item.itemOptions && this.item.itemOptions.offsetWidth || 0;
})
};
el.addEventListener('touchstart', touchStart);
el.addEventListener('mousedown', touchStart);
let touchEnd = (e) => {
// If we have a touch end and the item is closing,
// prevent default to stop a click from triggering
if(this.item.didClose) {
e.preventDefault();
}
this.item.didClose = false;
};
el.addEventListener('touchend', touchEnd);
el.addEventListener('mouseup', touchEnd);
el.addEventListener('mouseout', touchEnd);
el.addEventListener('mouseleave', touchEnd);
el.addEventListener('touchcancel', touchEnd);
});
}
onDragStart(ev) {
if (this.item.didClose) { return; }
if (!this.item.itemOptionsWidth) { return; }
this.slide = {};
this.slide.offsetX = this.item.getOpenAmt();
this.slide.startX = ev.center[this.direction];
this.slide.started = true;
this.item.disableAnimation();
}
onDrag(ev) {
if (!this.slide || !this.slide.started) return;
this.slide.x = ev.center[this.direction];
this.slide.delta = this.slide.x - this.slide.startX;
let newX = Math.max(0, this.slide.offsetX - this.slide.delta);
let buttonsWidth = this.item.itemOptionsWidth;
if (newX > this.item.itemOptionsWidth) {
// Calculate the new X position, capped at the top of the buttons
newX = -Math.min(-buttonsWidth, -buttonsWidth + (((this.slide.delta + buttonsWidth) * 0.4)));
}
this.item.open(newX);
}
onDragEnd(ev) {
if (!this.slide || !this.slide.started) return;
let buttonsWidth = this.item.itemOptionsWidth;
// If we are currently dragging, we want to snap back into place
// The final resting point X will be the width of the exposed buttons
var restingPoint = this.item.itemOptionsWidth;
// Check if the drag didn't clear the buttons mid-point
// and we aren't moving fast enough to swipe open
if (this.item.openAmount < (buttonsWidth / 2)) {
// If we are going left but too slow, or going right, go back to resting
if (ev.direction & Hammer.DIRECTION_RIGHT) {
// Left
restingPoint = 0;
} else if (Math.abs(ev.velocityX) < 0.3) {
// Right
restingPoint = 0;
}
}
raf(() => {
if (restingPoint === 0) {
// Reset to zero
this.item.open('');
var buttons = this.item.itemOptions;
clearTimeout(this.hideButtonsTimeout);
this.hideButtonsTimeout = setTimeout(() => {
buttons && buttons.classList.add('invisible');
}, 250);
} else {
this.item.open(restingPoint);
}
this.item.enableAnimation();
this.slide = null;
});
}
}

View File

@ -134,24 +134,6 @@ ion-input.item {
align-items: flex-start; align-items: flex-start;
} }
/**
* The hidden right-side buttons that can be exposed under a list item
* with dragging.
*/
ion-item-sliding-content {
display: block;
z-index: $z-index-item-options + 1;
flex: 1;
}
ion-item-options {
display: flex;
position: absolute;
top: 0;
right: 0;
z-index: $z-index-item-options;
height: 100%;
}
// TEMP hack for https://github.com/angular/angular/issues/4582 // TEMP hack for https://github.com/angular/angular/issues/4582
[item-right] { [item-right] {

View File

@ -163,26 +163,6 @@ ion-note {
} }
} }
ion-item-sliding.item {
padding-left: 0;
padding-right: 0;
}
ion-item-sliding-content {
background-color: $item-ios-sliding-content-bg;
padding-right: ($item-ios-padding-right / 2);
padding-left: ($item-ios-padding-left / 2);
display: flex;
min-height: 42px;
justify-content: center;
transition: $item-ios-sliding-transition;
// To allow the hairlines through
margin-top: 1px;
margin-bottom: 1px;
}
ion-item-options { ion-item-options {
button, [button] { button, [button] {
min-height: calc(100% - 2px); min-height: calc(100% - 2px);
@ -204,8 +184,7 @@ ion-note {
.item.activated, .item.activated,
a.item.activated, a.item.activated,
button.item.activated, button.item.activated {
.item.activated ion-item-sliding-content {
background-color: $item-ios-activated-background-color; background-color: $item-ios-activated-background-color;
transition-duration: 0ms; transition-duration: 0ms;
} }
@ -219,25 +198,13 @@ button.item {
.list, .list,
ion-card { ion-card {
button[ion-item]:not([detail-none]), button[ion-item]:not([detail-none]),
a[ion-item]:not([detail-none]), a[ion-item]:not([detail-none]) {
[detail-push]:not(ion-item-sliding) {
@include ios-detail-push-icon($item-ios-detail-push-color); @include ios-detail-push-icon($item-ios-detail-push-color);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right ($item-ios-padding-right - 2) center; background-position: right ($item-ios-padding-right - 2) center;
background-size: 14px 14px; background-size: 14px 14px;
margin-right: 32px; margin-right: 32px;
} }
ion-item-sliding[detail-push] {
ion-item-sliding-content {
@include ios-detail-push-icon($item-ios-detail-push-color);
background-repeat: no-repeat;
background-position: right ($item-ios-padding-right - 2) center;
background-size: 14px 14px;
margin-right: 32px;
}
}
} }
// Hairlines for iOS need to be set at 0.55px to show on iPhone 6 and 6 Plus // Hairlines for iOS need to be set at 0.55px to show on iPhone 6 and 6 Plus
@ -252,11 +219,6 @@ ion-card {
} }
} }
ion-item-sliding-content {
margin-top: 0.55px;
margin-bottom: 0.55px;
}
ion-header + .item { ion-header + .item {
border-top-width: 0.55px; border-top-width: 0.55px;

View File

@ -207,24 +207,6 @@ ion-note {
box-shadow: none; box-shadow: none;
} }
ion-item-sliding.item {
padding-left: 0;
padding-right: 0;
}
ion-item-sliding-content {
background-color: $item-md-sliding-content-bg;
padding-right: ($item-md-padding-right / 2);
padding-left: ($item-md-padding-left / 2);
display: flex;
min-height: 42px;
justify-content: center;
transition: $item-md-sliding-transition;
// To allow the hairlines through
margin-top: 1px;
margin-bottom: 1px;
}
ion-item-options { ion-item-options {
button, [button] { button, [button] {
height: calc(100% - 2px); height: calc(100% - 2px);
@ -246,15 +228,13 @@ ion-note {
.item, .item,
a.item, a.item,
button.item, button.item {
.item ion-item-sliding-content { transition: background-color $button-md-transition-duration $button-md-animation-curve, transform 300ms;
transition: background-color $button-md-transition-duration $button-md-animation-curve;
} }
.item.activated, .item.activated,
a.item.activated, a.item.activated,
button.item.activated, button.item.activated {
.item.activated ion-item-sliding-content {
background-color: $item-md-activated-background-color; background-color: $item-md-activated-background-color;
box-shadow: none; box-shadow: none;
} }

View File

@ -1,29 +1,35 @@
import {App} from 'ionic/ionic'; import {App, IonicApp} from 'ionic/ionic';
@App({ @App({
templateUrl: 'main.html' templateUrl: 'main.html'
}) })
class E2EApp { class E2EApp {
constructor() { constructor(private app: IonicApp) {
setTimeout(() => { setTimeout(() => {
this.shouldShow = true; this.shouldShow = true;
}, 10); }, 10);
} }
closeOpened() {
this.app.getComponent('myList').closeSlidingItems();
}
getItems() { getItems() {
console.log('getItems');
return [0,1]; return [0,1];
} }
didClick(e) { didClick(ev, item) {
console.log('CLICK', e.defaultPrevented, e) console.log('CLICK', ev.defaultPrevented, ev)
} }
archive(e) { archive(ev, item) {
console.log('Accept', e); console.log('Archive', ev, item);
item.close();
} }
del(e) {
console.log('Delete', e); del(ev, item) {
console.log('Delete', ev, item);
item.close();
} }
} }

View File

@ -1,77 +1,99 @@
<ion-toolbar><ion-title>Sliding Items</ion-title></ion-toolbar> <ion-toolbar><ion-title>Sliding Items</ion-title></ion-toolbar>
<ion-list> <ion-content>
<ion-item-sliding text-wrap detail-push (click)="didClick($event)"> <ion-list id="myList">
<h3>Max Lynch</h3>
<p>
Hey do you want to go to the game tonight?
</p>
<ion-item-options>
<button primary (click)="archive($event)">Archive</button>
<button danger (click)="del($event)">Delete</button>
</ion-item-options>
</ion-item-sliding>
<ion-item-sliding text-wrap detail-push class="activated"> <ion-item-sliding #item>
<h3>Adam Bradley</h3> <ion-item text-wrap detail-push (click)="didClick($event, item)">
<p> <h3>Max Lynch</h3>
I think I figured out how to get more Mountain Dew <p>
</p> Hey do you want to go to the game tonight?
<ion-item-options> </p>
<button primary>Archive</button> </ion-item>
<button danger><icon trash></icon></button> <ion-item-options>
</ion-item-options> <button primary (click)="archive($event, item)">Archive</button>
</ion-item-sliding> <button danger (click)="del($event, item)">Delete</button>
</ion-item-options>
</ion-item-sliding>
<ion-item-sliding text-wrap detail-push *ng-if="shouldShow"> <ion-item-sliding #item>
<h3>Ben Sperry</h3> <ion-item text-wrap detail-push class="activated">
<p> <h3>Adam Bradley</h3>
I like paper <p>
</p> I think I figured out how to get more Mountain Dew
<ion-item-options> </p>
<button primary>Archive</button> </ion-item>
<button danger>Delete</button> <ion-item-options>
</ion-item-options> <button primary (click)="archive($event, item)">Archive</button>
</ion-item-sliding> <button danger (click)="del($event, item)"><icon trash></icon></button>
</ion-item-options>
</ion-item-sliding>
<ion-item-sliding> <ion-item-sliding text-wrap detail-push *ng-if="shouldShow" #item>
<icon mail item-left></icon> <ion-item>
One Line w/ Icon, div only text <h3>Ben Sperry</h3>
<ion-item-options> <p>
<button primary>Archive</button> I like paper
</ion-item-options> </p>
</ion-item-sliding> </ion-item>
<ion-item-options>
<button primary (click)="archive($event, item)">Archive</button>
<button danger (click)="del($event, item)">Delete</button>
</ion-item-options>
</ion-item-sliding>
<ion-item-sliding> <ion-item-sliding #item>
<ion-avatar item-left> <ion-item>
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw=="> <icon mail item-left></icon>
</ion-avatar> One Line w/ Icon, div only text
One Line w/ Avatar, div only text </ion-item>
<ion-item-options> <ion-item-options>
<button primary>Archive</button> <button primary (click)="archive($event, item)">Archive</button>
</ion-item-options> </ion-item-options>
</ion-item-sliding> </ion-item-sliding>
<ion-item-sliding> <ion-item-sliding #item>
<ion-thumbnail item-left> <ion-item>
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw=="> <ion-avatar item-left>
</ion-thumbnail> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw==">
<h2>Two Lines w/ Thumbnail, H2 Header</h2> </ion-avatar>
<p>Paragraph text.</p> One Line w/ Avatar, div only text
<ion-item-options> </ion-item>
<button primary>Archive</button> <ion-item-options>
</ion-item-options> <button primary (click)="archive($event, item)">Archive</button>
</ion-item-sliding> </ion-item-options>
</ion-item-sliding>
<ion-item-sliding text-wrap detail-push *ng-for="#item of getItems()"> <ion-item-sliding #item>
<h3>ng-for {{item}}</h3> <ion-item>
<ion-item-options> <ion-thumbnail item-left>
<button primary>Archive</button> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw==">
</ion-item-options> </ion-thumbnail>
</ion-item-sliding> <h2>Two Lines w/ Thumbnail, H2 Header</h2>
<p>Paragraph text.</p>
</ion-item>
<ion-item-options>
<button primary (click)="archive($event, item)">Archive</button>
</ion-item-options>
</ion-item-sliding>
</ion-list> <ion-item-sliding *ng-for="#item of getItems()">
<ion-item text-wrap detail-push>
<h3>ng-for {{item}}</h3>
</ion-item>
<ion-item-options>
<button primary (click)="archive($event, item)">Archive</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
<p>
<button (click)="closeOpened()">Close opened items</button>
</p>
</ion-content>
<style> <style>
img { img {

View File

@ -1,8 +1,9 @@
import {Directive, ElementRef, Renderer} from 'angular2/angular2'; import {Directive, ElementRef, Renderer, NgZone} from 'angular2/angular2';
import {Ion} from '../ion'; import {Ion} from '../ion';
import {Config} from '../../config/config'; import {Config} from '../../config/config';
import {ListVirtualScroll} from './virtual'; import {ListVirtualScroll} from './virtual';
import {ItemSlidingGesture} from '../item/item-sliding-gesture';
import * as util from 'ionic/util'; import * as util from 'ionic/util';
/** /**
@ -30,7 +31,7 @@ export class List extends Ion {
* @param {ElementRef} elementRef TODO * @param {ElementRef} elementRef TODO
* @param {Config} config TODO * @param {Config} config TODO
*/ */
constructor(elementRef: ElementRef, config: Config, renderer: Renderer) { constructor(elementRef: ElementRef, config: Config, renderer: Renderer, private zone: NgZone) {
super(elementRef, config); super(elementRef, config);
renderer.setElementClass(elementRef, 'list', true); renderer.setElementClass(elementRef, 'list', true);
this.ele = elementRef.nativeElement; this.ele = elementRef.nativeElement;
@ -48,6 +49,12 @@ export class List extends Ion {
this._initVirtualScrolling(); this._initVirtualScrolling();
} }
} }
onDestroy() {
this.ele = null;
this.slidingGesture && this.slidingGesture.unlisten();
}
/** /**
* @private * @private
* TODO * TODO
@ -68,20 +75,32 @@ export class List extends Ion {
this.itemTemplate = item; this.itemTemplate = item;
} }
/** enableSlidingItems(shouldEnable) {
* Keeps track of any open item (a sliding item, for example), to close it later this._enableSliding = shouldEnable;
*/
setOpenItem(item) { if (this._init) {
this.openItem = item; if (shouldEnable) {
} this.zone.runOutsideAngular(() => {
closeOpenItem() { setTimeout(() => {
if(this.openItem) { this.slidingGesture = new ItemSlidingGesture(this, this.ele);
this.openItem.close(true); });
this.openItem = null; });
} else {
this.slidingGesture && this.slidingGesture.unlisten();
}
} }
} }
getOpenItem() {
return this.openItem; closeSlidingItems() {
this.slidingGesture && this.slidingGesture.closeOpened();
}
afterViewInit() {
this._init = true;
if (this._enableSliding) {
this.enableSlidingItems(true);
}
} }
} }
@ -97,6 +116,4 @@ export class List extends Ion {
'[attr.id]': 'id' '[attr.id]': 'id'
} }
}) })
export class ListHeader { export class ListHeader {}
}

View File

@ -8,14 +8,24 @@
<ion-navbar *navbar> <ion-navbar *navbar>
<ion-title>Heart</ion-title> <ion-title>Heart</ion-title>
</ion-navbar> </ion-navbar>
<ion-content padding> <ion-content>
<h2>Tab 1</h2> <ion-list>
<ion-header>
Tab 1
</ion-header>
<ion-item *ng-for="#i of items">Item {{i}} {{i}} {{i}} {{i}}</ion-item>
</ion-list>
</ion-content> </ion-content>
` `
}) })
class Tab1 { class Tab1 {
constructor(nav: NavController) { constructor(nav: NavController) {
this.nav = nav; this.nav = nav;
this.items = [];
for(var i = 1; i <= 250; i++) {
this.items.push(i);
}
} }
} }
@ -25,16 +35,35 @@ class Tab1 {
@Page({ @Page({
template: ` template: `
<ion-navbar *navbar> <ion-navbar *navbar>
<ion-title>Star</ion-title> <ion-title>Schedule</ion-title>
</ion-navbar> </ion-navbar>
<ion-content padding> <ion-content>
<h2>Tab 2</h2> <ion-list>
<ion-item-sliding *ng-for="#session of sessions" #sliding-item>
<ion-item>
<h3>{{session.name}} {{session.name}} {{session.name}}</h3>
<p>{{session.location}} {{session.location}} {{session.location}}</p>
</ion-item>
<ion-item-options>
<button primary>Speaker<br>Info</button>
<button secondary>Add to<br>Favorites</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
</ion-content> </ion-content>
` `
}) })
class Tab2 { class Tab2 {
constructor(nav: NavController) { constructor(nav: NavController) {
this.nav = nav; this.nav = nav;
this.sessions = [];
for(var i = 1; i <= 250; i++) {
this.sessions.push({
name: 'Name ' + i,
location: 'Location: ' + i
});
}
} }
} }
@ -76,8 +105,8 @@ class Tab3 {
</ion-menu> </ion-menu>
<ion-tabs #content> <ion-tabs #content>
<ion-tab tab-title="Heart" tab-icon="heart" [root]="root1"></ion-tab> <ion-tab tab-title="Plain List" tab-icon="star" [root]="root1"></ion-tab>
<ion-tab tab-title="Star" tab-icon="star" [root]="root2"></ion-tab> <ion-tab tab-title="Schedule" tab-icon="globe" [root]="root2"></ion-tab>
<ion-tab tab-title="Stopwatch" tab-icon="stopwatch" [root]="root3"></ion-tab> <ion-tab tab-title="Stopwatch" tab-icon="stopwatch" [root]="root3"></ion-tab>
</ion-tabs> </ion-tabs>
` `

View File

@ -73,6 +73,8 @@ export class Activator {
} }
disableActivated(ev) { disableActivated(ev) {
if (ev.defaultPrevented) return true;
let targetEle = ev.target; let targetEle = ev.target;
for (let x = 0; x < 4; x++) { for (let x = 0; x < 4; x++) {
if (!targetEle) break; if (!targetEle) break;

View File

@ -18,7 +18,3 @@ md-ripple {
transform: scale(0.001) translateZ(0); transform: scale(0.001) translateZ(0);
} }
ion-item-sliding md-ripple {
z-index: 2;
}

View File

@ -7,8 +7,8 @@ let startCoord = null;
let pointerTolerance = 4; let pointerTolerance = 4;
let lastTouch = 0; let lastTouch = 0;
let lastActivated = 0; let lastActivated = 0;
let disableNativeClickTime = 0; let disableNativeClickUntil = 0;
let disableNativeClickLimit = 1000; let disableNativeClickAmount = 3000;
let activator = null; let activator = null;
let isEnabled = false; let isEnabled = false;
let app = null; let app = null;
@ -45,13 +45,13 @@ function touchStart(ev) {
function touchEnd(ev) { function touchEnd(ev) {
touchAction(); touchAction();
if (isEnabled && startCoord && app.isEnabled()) { if (isEnabled && startCoord && app.isEnabled() && !ev.defaultPrevented) {
let endCoord = pointerCoord(ev); let endCoord = pointerCoord(ev);
if (!hasPointerMoved(pointerTolerance, startCoord, endCoord)) { if (!hasPointerMoved(pointerTolerance, startCoord, endCoord)) {
console.debug('create click from touch'); console.debug('create click from touch');
setDisableNativeClick();; disableNativeClickUntil = Date.now() + disableNativeClickAmount;
let clickEvent = doc.createEvent('MouseEvents'); let clickEvent = doc.createEvent('MouseEvents');
clickEvent.initMouseEvent('click', true, true, win, 1, 0, 0, endCoord.x, endCoord.y, false, false, false, false, 0, null); clickEvent.initMouseEvent('click', true, true, win, 1, 0, 0, endCoord.x, endCoord.y, false, false, false, false, 0, null);
@ -74,7 +74,7 @@ function mouseDown(ev) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
} else if (lastTouch + disableNativeClickLimit < Date.now()) { } else if (lastTouch + disableNativeClickAmount < Date.now()) {
pointerStart(ev); pointerStart(ev);
} }
} }
@ -86,7 +86,7 @@ function mouseUp(ev) {
ev.stopPropagation(); ev.stopPropagation();
} }
if (lastTouch + disableNativeClickLimit < Date.now()) { if (lastTouch + disableNativeClickAmount < Date.now()) {
pointerEnd(ev); pointerEnd(ev);
} }
} }
@ -127,7 +127,6 @@ function pointerCancel(ev) {
console.debug('pointerCancel from', ev.type); console.debug('pointerCancel from', ev.type);
activator.clearState(); activator.clearState();
moveListeners(false); moveListeners(false);
setDisableNativeClick();
} }
function moveListeners(shouldAdd) { function moveListeners(shouldAdd) {
@ -139,12 +138,8 @@ function moveListeners(shouldAdd) {
} }
} }
function setDisableNativeClick() {
disableNativeClickTime = Date.now() + disableNativeClickLimit;
}
function isDisabledNativeClick() { function isDisabledNativeClick() {
return disableNativeClickTime > Date.now(); return disableNativeClickUntil > Date.now();
} }
function click(ev) { function click(ev) {

View File

@ -15,7 +15,7 @@ import {Tab} from '../components/tabs/tab';
import {List, ListHeader} from '../components/list/list'; import {List, ListHeader} from '../components/list/list';
import {Item} from '../components/item/item'; import {Item} from '../components/item/item';
import {ItemGroup, ItemGroupTitle} from '../components/item/item-group'; import {ItemGroup, ItemGroupTitle} from '../components/item/item-group';
import {ItemSliding, ItemSlidingOptionButton} from '../components/item/item-sliding'; import {ItemSliding} from '../components/item/item-sliding';
import {Toolbar, ToolbarTitle, ToolbarItem} from '../components/toolbar/toolbar'; import {Toolbar, ToolbarTitle, ToolbarItem} from '../components/toolbar/toolbar';
import {Icon} from '../components/icon/icon'; import {Icon} from '../components/icon/icon';
import {Checkbox} from '../components/checkbox/checkbox'; import {Checkbox} from '../components/checkbox/checkbox';
@ -60,7 +60,6 @@ export const IONIC_DIRECTIVES = [
ItemGroup, ItemGroup,
ItemGroupTitle, ItemGroupTitle,
ItemSliding, ItemSliding,
ItemSlidingOptionButton,
// Slides // Slides
Slides, Slides,

View File

@ -10,24 +10,24 @@ export class DragGesture extends Gesture {
listen() { listen() {
super.listen(); super.listen();
this.on('panstart', ev => { this.on('panstart', ev => {
if (this.onDragStart(ev) !== false) { if (this.onDragStart(ev) !== false) {
this.dragging = true; this.dragging = true;
} }
// ev.stopPropagation(); });
})
this.on('panmove', ev => { this.on('panmove', ev => {
if (!this.dragging) return; if (!this.dragging) return;
if (this.onDrag(ev) === false) { if (this.onDrag(ev) === false) {
this.dragging = false; this.dragging = false;
} }
// ev.stopPropagation()
}); });
this.on('panend', ev => { this.on('panend', ev => {
if (!this.dragging) return; if (!this.dragging) return;
this.onDragEnd(ev); this.onDragEnd(ev);
this.dragging = false; this.dragging = false;
// ev.stopPropagation()
}); });
} }

View File

@ -27,6 +27,7 @@
"components/icon/icon", "components/icon/icon",
"components/item/item", "components/item/item",
"components/item/item-media", "components/item/item-media",
"components/item/item-sliding",
"components/grid/grid", "components/grid/grid",
"components/text-input/text-input", "components/text-input/text-input",
"components/text-input/label", "components/text-input/label",

View File

@ -199,25 +199,25 @@ export function hasFocusedTextInput() {
return false; return false;
} }
export function closest(ele, selector) { let matchesFn;
var matchesFn; ['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector'].some(fn => {
if (typeof document.documentElement[fn] == 'function') {
matchesFn = fn;
}
});
// find vendor prefix export function closest(ele, selector, checkSelf) {
['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn) { if (ele && matchesFn) {
if (typeof document.body[fn] == 'function') {
matchesFn = fn;
return true;
}
return false;
})
// traverse parents // traverse parents
while (ele !== null) { ele = (checkSelf ? ele : ele.parentElement);
parent = ele.parentElement;
if (parent!==null && parent[matchesFn](selector)) { while (ele !== null) {
return parent; if (ele[matchesFn](selector)) {
return ele;
}
ele = ele.parentElement;
} }
ele = parent;
} }
return null; return null;
@ -233,10 +233,10 @@ export function removeElement(ele) {
* to reduce DOM reads. Cache is cleared on a window resize. * to reduce DOM reads. Cache is cleared on a window resize.
* @param {TODO} ele TODO * @param {TODO} ele TODO
*/ */
export function getDimensions(ion) { export function getDimensions(ion, ele) {
if (!ion._dimId) { if (!ion._dimId) {
ion._dimId = ++dimensionIds; ion._dimId = ++dimensionIds;
if (ion._dimId % 100 === 0) { if (ion._dimId % 1000 === 0) {
// periodically flush dimensions // periodically flush dimensions
flushDimensionCache(); flushDimensionCache();
} }
@ -244,7 +244,7 @@ export function getDimensions(ion) {
let dimensions = dimensionCache[ion._dimId]; let dimensions = dimensionCache[ion._dimId];
if (!dimensions) { if (!dimensions) {
let ele = ion.getNativeElement(); ele = ele || ion.getNativeElement();
dimensions = dimensionCache[ion._dimId] = { dimensions = dimensionCache[ion._dimId] = {
width: ele.offsetWidth, width: ele.offsetWidth,
height: ele.offsetHeight, height: ele.offsetHeight,