mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(item): sliding items work with list reorder
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import {Item} from './item';
|
||||
import {List} from '../list/list';
|
||||
import {Reorder} from '../item/item-reorder';
|
||||
import {UIEventManager} from '../../util/ui-event-manager';
|
||||
import {closest, Coordinates, pointerCoord, CSS, nativeRaf} from '../../util/dom';
|
||||
|
||||
@@ -12,7 +12,6 @@ const ITEM_REORDER_ACTIVE = 'reorder-active';
|
||||
* @private
|
||||
*/
|
||||
export class ItemReorderGesture {
|
||||
private selectedItem: Item = null;
|
||||
private selectedItemEle: HTMLElement = null;
|
||||
private selectedItemHeight: number;
|
||||
|
||||
@@ -26,7 +25,7 @@ export class ItemReorderGesture {
|
||||
|
||||
private events: UIEventManager = new UIEventManager(false);
|
||||
|
||||
constructor(public list: List) {
|
||||
constructor(public list: Reorder) {
|
||||
let element = this.list.getNativeElement();
|
||||
this.events.pointerEvents(element,
|
||||
this.onDragStart.bind(this),
|
||||
@@ -35,24 +34,22 @@ export class ItemReorderGesture {
|
||||
}
|
||||
|
||||
private onDragStart(ev: any): boolean {
|
||||
let itemEle = ev.target;
|
||||
if (itemEle.nodeName !== 'ION-REORDER') {
|
||||
let reorderElement = ev.target;
|
||||
if (reorderElement.nodeName !== 'ION-REORDER') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let item = <Item> itemEle['$ionComponent'];
|
||||
let item = reorderElement['$ionReorderNode'];
|
||||
if (!item) {
|
||||
console.error('item does not contain ion component');
|
||||
console.error('item does not contain ion ionReorderNode');
|
||||
return false;
|
||||
}
|
||||
ev.preventDefault();
|
||||
|
||||
// Preparing state
|
||||
this.selectedItem = item;
|
||||
this.selectedItemEle = item.getNativeElement();
|
||||
this.selectedItemHeight = item.height();
|
||||
this.lastToIndex = item.index;
|
||||
this.lastYcoord = -100;
|
||||
this.selectedItemEle = item;
|
||||
this.selectedItemHeight = item.offsetHeight;
|
||||
this.lastYcoord = this.lastToIndex = -100;
|
||||
|
||||
this.windowHeight = window.innerHeight - AUTO_SCROLL_MARGIN;
|
||||
this.lastScrollPosition = this.list.scrollContent(0);
|
||||
@@ -60,7 +57,7 @@ export class ItemReorderGesture {
|
||||
this.offset = pointerCoord(ev);
|
||||
this.offset.y += this.lastScrollPosition;
|
||||
|
||||
item.setCssClass(ITEM_REORDER_ACTIVE, true);
|
||||
item.classList.add(ITEM_REORDER_ACTIVE);
|
||||
this.list.reorderStart();
|
||||
return true;
|
||||
}
|
||||
@@ -83,9 +80,9 @@ export class ItemReorderGesture {
|
||||
if (Math.abs(posY - this.lastYcoord) > 30) {
|
||||
let overItem = this.itemForCoord(coord);
|
||||
if (overItem) {
|
||||
let toIndex = overItem.index;
|
||||
if (toIndex !== this.lastToIndex || this.emptyZone) {
|
||||
let fromIndex = this.selectedItem.index;
|
||||
let toIndex = indexForItem(overItem);
|
||||
if (toIndex && (toIndex !== this.lastToIndex || this.emptyZone)) {
|
||||
let fromIndex = indexForItem(this.selectedItemEle);
|
||||
this.lastToIndex = toIndex;
|
||||
this.lastYcoord = posY;
|
||||
this.emptyZone = false;
|
||||
@@ -96,40 +93,25 @@ export class ItemReorderGesture {
|
||||
}
|
||||
}
|
||||
|
||||
// Update selected item position
|
||||
// Update selected item position
|
||||
let ydiff = Math.round(posY - this.offset.y + scrollPosition);
|
||||
selectedItem.style[CSS.transform] = `translateY(${ydiff}px)`;
|
||||
}
|
||||
|
||||
|
||||
private onDragEnd() {
|
||||
if (!this.selectedItemEle) {
|
||||
return;
|
||||
}
|
||||
|
||||
nativeRaf(() => {
|
||||
let toIndex = this.lastToIndex;
|
||||
let fromIndex = this.selectedItem.index;
|
||||
this.selectedItem.setCssClass(ITEM_REORDER_ACTIVE, false);
|
||||
this.selectedItem = null;
|
||||
this.selectedItemEle = null;
|
||||
this.list.reorderEmit(fromIndex, toIndex);
|
||||
});
|
||||
let toIndex = this.lastToIndex;
|
||||
let fromIndex = indexForItem(this.selectedItemEle);
|
||||
this.selectedItemEle.classList.remove(ITEM_REORDER_ACTIVE);
|
||||
this.selectedItemEle = null;
|
||||
this.list.reorderEmit(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
private itemForCoord(coord: Coordinates): Item {
|
||||
let element = <HTMLElement>document.elementFromPoint(this.offset.x - 100, coord.y);
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
if (element.nodeName !== 'ION-ITEM') {
|
||||
return null;
|
||||
}
|
||||
let item = <Item>(<any>element)['$ionComponent'];
|
||||
if (!item) {
|
||||
console.error('item does not have $ionComponent');
|
||||
return null;
|
||||
}
|
||||
return item;
|
||||
private itemForCoord(coord: Coordinates): HTMLElement {
|
||||
return itemForPosition(this.offset.x - 100, coord.y);
|
||||
}
|
||||
|
||||
private scroll(posY: number): number {
|
||||
@@ -141,8 +123,6 @@ export class ItemReorderGesture {
|
||||
return this.lastScrollPosition;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -153,3 +133,25 @@ export class ItemReorderGesture {
|
||||
this.list = null;
|
||||
}
|
||||
}
|
||||
|
||||
function itemForPosition(x: number, y: number): HTMLElement {
|
||||
let element = <HTMLElement>document.elementFromPoint(x, y);
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
if (element.nodeName !== 'ION-ITEM' && !element.hasAttribute('ion-item')) {
|
||||
return null;
|
||||
}
|
||||
if (indexForItem(element)) {
|
||||
return element;
|
||||
}
|
||||
let parent = element.parentNode;
|
||||
if (indexForItem(parent)) {
|
||||
return <HTMLElement>parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function indexForItem(element: any): number {
|
||||
return element['$ionIndex'];
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ ion-reorder {
|
||||
|
||||
.reorder-enabled {
|
||||
|
||||
.item {
|
||||
.item, .item-wrapper {
|
||||
transition: transform 300ms;
|
||||
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@@ -40,6 +42,7 @@ ion-reorder {
|
||||
}
|
||||
|
||||
|
||||
.item-wrapper.reorder-active,
|
||||
.item.reorder-active {
|
||||
z-index: 4;
|
||||
|
||||
|
||||
@@ -1,6 +1,171 @@
|
||||
import { Component, ElementRef, forwardRef, Inject } from '@angular/core';
|
||||
import { Component, Directive, ElementRef, EventEmitter, forwardRef, Input, NgZone, Renderer, Inject, Optional, Output } from '@angular/core';
|
||||
|
||||
import { Content } from '../content/content';
|
||||
import { CSS } from '../../util/dom';
|
||||
import { Item } from './item';
|
||||
import { ItemReorderGesture } from '../item/item-reorder-gesture';
|
||||
import { isTrueProperty } from '../../util/util';
|
||||
|
||||
export interface ReorderIndexes {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[reorder]',
|
||||
host: {
|
||||
'[class.reorder-enabled]': '_enableReorder',
|
||||
}
|
||||
})
|
||||
export class Reorder {
|
||||
private _enableReorder: boolean = false;
|
||||
private _reorderGesture: ItemReorderGesture;
|
||||
private _lastToIndex: number = -1;
|
||||
private _element: HTMLElement;
|
||||
|
||||
@Output() ionItemReorder: EventEmitter<ReorderIndexes> = new EventEmitter<ReorderIndexes>();
|
||||
|
||||
constructor(
|
||||
elementRef: ElementRef,
|
||||
private _rendered: Renderer,
|
||||
private _zone: NgZone,
|
||||
@Optional() private _content: Content) {
|
||||
this._element = elementRef.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._element = null;
|
||||
this._reorderGesture && this._reorderGesture.destroy();
|
||||
}
|
||||
|
||||
|
||||
@Input()
|
||||
get reorder(): boolean {
|
||||
return this._enableReorder;
|
||||
}
|
||||
set reorder(val: boolean) {
|
||||
this._enableReorder = isTrueProperty(val);
|
||||
|
||||
if (!this._enableReorder) {
|
||||
this._reorderGesture && this._reorderGesture.destroy();
|
||||
this._reorderGesture = null;
|
||||
|
||||
} else if (!this._reorderGesture) {
|
||||
console.debug('enableReorderItems');
|
||||
this._reorderGesture = new ItemReorderGesture(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
reorderStart() {
|
||||
let children = this._element.children;
|
||||
let len = children.length;
|
||||
this.setCssClass('reorder-active', true);
|
||||
for (let i = 0; i < len; i++) {
|
||||
children[i]['$ionIndex'] = i;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
reorderEmit(fromIndex: number, toIndex: number) {
|
||||
this.reorderReset();
|
||||
if (fromIndex !== toIndex) {
|
||||
this._zone.run(() => {
|
||||
this.ionItemReorder.emit({
|
||||
from: fromIndex,
|
||||
to: toIndex,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
scrollContent(scroll: number) {
|
||||
let scrollTop = this._content.getScrollTop() + scroll;
|
||||
if (scroll !== 0) {
|
||||
this._content.scrollTo(0, scrollTop, 0);
|
||||
}
|
||||
return scrollTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
reorderReset() {
|
||||
let children = this._element.children;
|
||||
let len = children.length;
|
||||
|
||||
this.setCssClass('reorder-active', false);
|
||||
let transform = CSS.transform;
|
||||
for (let i = 0; i < len; i++) {
|
||||
children[i].style[transform] = '';
|
||||
}
|
||||
this._lastToIndex = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
reorderMove(fromIndex: number, toIndex: number, itemHeight: number) {
|
||||
if (this._lastToIndex === -1) {
|
||||
this._lastToIndex = fromIndex;
|
||||
}
|
||||
let lastToIndex = this._lastToIndex;
|
||||
this._lastToIndex = toIndex;
|
||||
|
||||
// TODO: I think both loops can be merged into a single one
|
||||
// but I had no luck last time I tried
|
||||
|
||||
/********* DOM READ ********** */
|
||||
let children = this._element.children;
|
||||
|
||||
/********* DOM WRITE ********* */
|
||||
let transform = CSS.transform;
|
||||
if (toIndex >= lastToIndex) {
|
||||
for (var i = lastToIndex; i <= toIndex; i++) {
|
||||
if (i !== fromIndex) {
|
||||
children[i].style[transform] = (i > fromIndex)
|
||||
? `translateY(${-itemHeight}px)` : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toIndex <= lastToIndex) {
|
||||
for (var i = toIndex; i <= lastToIndex; i++) {
|
||||
if (i !== fromIndex) {
|
||||
children[i].style[transform] = (i < fromIndex)
|
||||
? `translateY(${itemHeight}px)` : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setCssClass(classname: string, add: boolean) {
|
||||
this._rendered.setElementClass(this._element, classname, add);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getNativeElement(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -11,8 +176,18 @@ import { Item } from './item';
|
||||
})
|
||||
export class ItemReorder {
|
||||
constructor(
|
||||
@Inject(forwardRef(() => Item)) item: Item,
|
||||
elementRef: ElementRef) {
|
||||
elementRef.nativeElement['$ionComponent'] = item;
|
||||
@Inject(forwardRef(() => Item)) private item: Item,
|
||||
private elementRef: ElementRef) {
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
let item = this.item.getNativeElement();
|
||||
if (item.parentNode.nodeName === 'ION-ITEM-SLIDING') {
|
||||
this.elementRef.nativeElement['$ionReorderNode'] = item.parentNode;
|
||||
} else {
|
||||
this.elementRef.nativeElement['$ionReorderNode'] = item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -70,38 +70,40 @@ export class ItemSlidingGesture extends DragGesture {
|
||||
}
|
||||
|
||||
onDragEnd(ev: any) {
|
||||
if (this.selectedContainer) {
|
||||
ev.preventDefault();
|
||||
if (!this.selectedContainer) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
|
||||
let openAmount = this.selectedContainer.endSliding(ev.velocityX);
|
||||
this.selectedContainer = null;
|
||||
let openAmount = this.selectedContainer.endSliding(ev.velocityX);
|
||||
this.selectedContainer = null;
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
closeOpened(): boolean {
|
||||
if (this.openContainer) {
|
||||
this.openContainer.close();
|
||||
this.openContainer = null;
|
||||
this.selectedContainer = null;
|
||||
this.off('tap', this.onTap);
|
||||
this.onTap = null;
|
||||
return true;
|
||||
if (!this.openContainer) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
this.openContainer.close();
|
||||
this.openContainer = null;
|
||||
this.selectedContainer = null;
|
||||
this.off('tap', this.onTap);
|
||||
this.onTap = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
unlisten() {
|
||||
this.closeOpened();
|
||||
super.unlisten();
|
||||
this.list = null;
|
||||
}
|
||||
|
||||
@@ -71,9 +71,11 @@ ion-item-sliding.active-slide {
|
||||
z-index: $z-index-item-options + 1;
|
||||
|
||||
opacity: 1;
|
||||
transition: all 300ms cubic-bezier(.36, .66, .04, 1);
|
||||
transition: transform 500ms cubic-bezier(.36, .66, .04, 1);
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
ion-item-options {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, ContentChildren, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, Input, Optional, Output, QueryList, Renderer, ViewEncapsulation } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, ContentChildren, ContentChild, Directive, ElementRef, EventEmitter, Input, Optional, Output, QueryList, Renderer, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
import { CSS, nativeRaf, nativeTimeout } from '../../util/dom';
|
||||
import { CSS, nativeRaf, nativeTimeout, clearNativeTimeout } from '../../util/dom';
|
||||
import { Item } from './item';
|
||||
import { isPresent } from '../../util/util';
|
||||
import { List } from '../list/list';
|
||||
|
||||
const SWIPE_FACTOR = 1.1;
|
||||
const SWIPE_MARGIN = 20;
|
||||
const ELASTIC_FACTOR = 0.55;
|
||||
|
||||
export const enum ItemSideFlags {
|
||||
@@ -52,8 +52,7 @@ export class ItemOptions {
|
||||
*/
|
||||
@Output() ionSwipe: EventEmitter<ItemSliding> = new EventEmitter<ItemSliding>();
|
||||
|
||||
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {
|
||||
}
|
||||
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -83,10 +82,13 @@ export class ItemOptions {
|
||||
}
|
||||
|
||||
const enum SlidingState {
|
||||
Disabled = 0,
|
||||
Enabled = 1,
|
||||
Right = 2,
|
||||
Left = 3
|
||||
Disabled = 1 << 1,
|
||||
Enabled = 1 << 2,
|
||||
Right = 1 << 3,
|
||||
Left = 1 << 4,
|
||||
|
||||
SwipeRight = 1 << 5,
|
||||
SwipeLeft = 1 << 6,
|
||||
}
|
||||
|
||||
|
||||
@@ -194,23 +196,15 @@ export class ItemSliding {
|
||||
private _optsDirty: boolean = true;
|
||||
private _state: SlidingState = SlidingState.Disabled;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* */
|
||||
slidingPercent: number = 0;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* */
|
||||
@ContentChild(Item) private item: Item;
|
||||
|
||||
|
||||
/**
|
||||
* @output {event} Expression to evaluate when the sliding position changes.
|
||||
* It reports the relative position.
|
||||
*
|
||||
* ```ts
|
||||
* ondrag(percent) {
|
||||
* ondrag(item) {
|
||||
* let percent = item.getSlidingPercent();
|
||||
* if (percent > 0) {
|
||||
* // positive
|
||||
* console.log('right side');
|
||||
@@ -225,20 +219,16 @@ export class ItemSliding {
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
@Output() ionDrag: EventEmitter<number> = new EventEmitter<number>();
|
||||
@Output() ionDrag: EventEmitter<ItemSliding> = new EventEmitter<ItemSliding>();
|
||||
|
||||
constructor(@Optional() private _list: List, private _renderer: Renderer, private _elementRef: ElementRef) {
|
||||
_list.enableSlidingItems(true);
|
||||
constructor( @Optional() list: List, private _renderer: Renderer, private _elementRef: ElementRef) {
|
||||
list && list.containsSlidingItem(true);
|
||||
_elementRef.nativeElement.$ionComponent = this;
|
||||
_renderer.setElementClass(_elementRef.nativeElement, 'item-wrapper', true);
|
||||
this.setCssClass('item-wrapper', true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@ContentChildren(ItemOptions)
|
||||
set _itemOptions(itemOptions: QueryList<ItemOptions>) {
|
||||
private set _itemOptions(itemOptions: QueryList<ItemOptions>) {
|
||||
let sides = 0;
|
||||
for (var item of itemOptions.toArray()) {
|
||||
var side = item.getSides();
|
||||
@@ -253,12 +243,33 @@ export class ItemSliding {
|
||||
this._sides = sides;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getOpenAmount(): number {
|
||||
return this._openAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getSlidingPercent(): number {
|
||||
let openAmount = this._openAmount;
|
||||
if (openAmount > 0) {
|
||||
return openAmount / this._optsWidthRightSide;
|
||||
} else if (openAmount < 0) {
|
||||
return openAmount / this._optsWidthLeftSide;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
startSliding(startX: number) {
|
||||
if (this._timer) {
|
||||
clearTimeout(this._timer);
|
||||
clearNativeTimeout(this._timer);
|
||||
this._timer = null;
|
||||
}
|
||||
if (this._openAmount === 0) {
|
||||
@@ -278,7 +289,7 @@ export class ItemSliding {
|
||||
return;
|
||||
}
|
||||
|
||||
let openAmount = this._startX - x;
|
||||
let openAmount = (this._startX - x);
|
||||
switch (this._sides) {
|
||||
case ItemSideFlags.Right: openAmount = Math.max(0, openAmount); break;
|
||||
case ItemSideFlags.Left: openAmount = Math.min(0, openAmount); break;
|
||||
@@ -316,105 +327,88 @@ export class ItemSliding {
|
||||
restingPoint = 0;
|
||||
}
|
||||
|
||||
this.fireSwipeEvent();
|
||||
this._setOpenAmount(restingPoint, true);
|
||||
this.fireSwipeEvent();
|
||||
return restingPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* */
|
||||
fireSwipeEvent() {
|
||||
if (this.slidingPercent > SWIPE_FACTOR) {
|
||||
private fireSwipeEvent() {
|
||||
if (this._state & SlidingState.SwipeRight) {
|
||||
this._rightOptions.ionSwipe.emit(this);
|
||||
} else if (this.slidingPercent < -SWIPE_FACTOR) {
|
||||
} else if (this._state & SlidingState.SwipeLeft) {
|
||||
this._leftOptions.ionSwipe.emit(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* */
|
||||
calculateOptsWidth() {
|
||||
private calculateOptsWidth() {
|
||||
nativeRaf(() => {
|
||||
if (this._optsDirty) {
|
||||
this._optsWidthRightSide = 0;
|
||||
if (this._rightOptions) {
|
||||
this._optsWidthRightSide = this._rightOptions.width();
|
||||
}
|
||||
|
||||
this._optsWidthLeftSide = 0;
|
||||
if (this._leftOptions) {
|
||||
this._optsWidthLeftSide = this._leftOptions.width();
|
||||
}
|
||||
this._optsDirty = false;
|
||||
if (!this._optsDirty) {
|
||||
return;
|
||||
}
|
||||
this._optsWidthRightSide = 0;
|
||||
if (this._rightOptions) {
|
||||
this._optsWidthRightSide = this._rightOptions.width();
|
||||
}
|
||||
|
||||
this._optsWidthLeftSide = 0;
|
||||
if (this._leftOptions) {
|
||||
this._optsWidthLeftSide = this._leftOptions.width();
|
||||
}
|
||||
this._optsDirty = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private _setOpenAmount(openAmount: number, isFinal: boolean) {
|
||||
if (this._timer) {
|
||||
clearTimeout(this._timer);
|
||||
clearNativeTimeout(this._timer);
|
||||
this._timer = null;
|
||||
}
|
||||
this._openAmount = openAmount;
|
||||
|
||||
if (isFinal) {
|
||||
this.item.setCssStyle(CSS.transition, '');
|
||||
|
||||
let didEnd = openAmount === 0;
|
||||
if (didEnd) {
|
||||
// TODO: refactor. there must exist a better way
|
||||
// if sliding ended, we wait 400ms until animation finishes
|
||||
} else {
|
||||
if (openAmount > 0) {
|
||||
let state = (openAmount >= (this._optsWidthRightSide + SWIPE_MARGIN))
|
||||
? SlidingState.Right | SlidingState.SwipeRight
|
||||
: SlidingState.Right;
|
||||
|
||||
this._setState(state);
|
||||
|
||||
} else if (openAmount < 0) {
|
||||
let state = (openAmount <= (-this._optsWidthLeftSide - SWIPE_MARGIN))
|
||||
? SlidingState.Left | SlidingState.SwipeLeft
|
||||
: SlidingState.Left;
|
||||
|
||||
this._setState(state);
|
||||
}
|
||||
}
|
||||
if (openAmount === 0) {
|
||||
this._timer = nativeTimeout(() => {
|
||||
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;
|
||||
}, 600);
|
||||
this.item.setCssStyle(CSS.transform, '');
|
||||
return;
|
||||
}
|
||||
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)'));
|
||||
|
||||
this.item.setCssStyle(CSS.transform, `translate3d(${-openAmount}px,0,0)`);
|
||||
this.ionDrag.emit(this);
|
||||
}
|
||||
|
||||
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);
|
||||
if (state === SlidingState.Disabled || state === SlidingState.Enabled) {
|
||||
this.setClass('active-swipe-right', false);
|
||||
this.setClass('active-swipe-left', false);
|
||||
}
|
||||
if (state === this._state) {
|
||||
return;
|
||||
}
|
||||
this.setCssClass('active-slide', (state !== SlidingState.Disabled));
|
||||
this.setCssClass('active-options-right', !!(state & SlidingState.Right));
|
||||
this.setCssClass('active-options-left', !!(state & SlidingState.Left));
|
||||
this.setCssClass('active-swipe-right', !!(state & SlidingState.SwipeRight));
|
||||
this.setCssClass('active-swipe-left', !!(state & SlidingState.SwipeLeft));
|
||||
this._state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setClass(className: string, add: boolean) {
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, className, add);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getOpenAmount(): number {
|
||||
return this._openAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the sliding item. Items can also be closed from the [List](../../list/List).
|
||||
@@ -454,24 +448,37 @@ export class ItemSliding {
|
||||
this._setOpenAmount(0, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setCssClass(cssClass: string, shouldAdd: boolean) {
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, cssClass, shouldAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setCssStyle(property: string, value: string) {
|
||||
this._renderer.setElementStyle(this._elementRef.nativeElement, property, value);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldClose(isCloseDirection: boolean, isMovingFast: boolean, isOnCloseZone: boolean): boolean {
|
||||
// The logic required to know when the sliding item should close (openAmount=0)
|
||||
// depends on three booleans (isCloseDirection, isMovingFast, isOnCloseZone)
|
||||
// and it ended up being too complicated to be written manually without errors
|
||||
// so the truth table is attached below: (0=false, 1=true)
|
||||
// isCloseDirection | isMovingFast | isOnCloseZone || shouldClose
|
||||
// 0 | 0 | 0 || 0
|
||||
// 0 | 0 | 1 || 1
|
||||
// 0 | 1 | 0 || 0
|
||||
// 0 | 1 | 1 || 0
|
||||
// 1 | 0 | 0 || 0
|
||||
// 1 | 0 | 1 || 1
|
||||
// 1 | 1 | 0 || 1
|
||||
// 1 | 1 | 1 || 1
|
||||
// The resulting expression was generated by resolving the K-map (Karnaugh map):
|
||||
let shouldClose = (!isMovingFast && isOnCloseZone) || (isCloseDirection && isMovingFast);
|
||||
return shouldClose;
|
||||
// The logic required to know when the sliding item should close (openAmount=0)
|
||||
// depends on three booleans (isCloseDirection, isMovingFast, isOnCloseZone)
|
||||
// and it ended up being too complicated to be written manually without errors
|
||||
// so the truth table is attached below: (0=false, 1=true)
|
||||
// isCloseDirection | isMovingFast | isOnCloseZone || shouldClose
|
||||
// 0 | 0 | 0 || 0
|
||||
// 0 | 0 | 1 || 1
|
||||
// 0 | 1 | 0 || 0
|
||||
// 0 | 1 | 1 || 0
|
||||
// 1 | 0 | 0 || 0
|
||||
// 1 | 0 | 1 || 1
|
||||
// 1 | 1 | 0 || 1
|
||||
// 1 | 1 | 1 || 1
|
||||
// The resulting expression was generated by resolving the K-map (Karnaugh map):
|
||||
let shouldClose = (!isMovingFast && isOnCloseZone) || (isCloseDirection && isMovingFast);
|
||||
return shouldClose;
|
||||
}
|
||||
|
||||
|
||||
@@ -291,11 +291,6 @@ export class Item {
|
||||
private _label: Label;
|
||||
private _viewLabel: boolean = true;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@Input() index: number;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -308,7 +303,6 @@ export class Item {
|
||||
|
||||
constructor(form: Form, private _renderer: Renderer, private _elementRef: ElementRef) {
|
||||
this.id = form.nextId().toString();
|
||||
_elementRef.nativeElement['$ionComponent'] = this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,20 +327,6 @@ export class Item {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setCssClass(cssClass: string, shouldAdd: boolean) {
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, cssClass, shouldAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setCssStyle(property: string, value: string) {
|
||||
this._renderer.setElementStyle(this._elementRef.nativeElement, property, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -406,15 +386,22 @@ export class Item {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getNativeElement() {
|
||||
return this._elementRef.nativeElement;
|
||||
setCssClass(cssClass: string, shouldAdd: boolean) {
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, cssClass, shouldAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
height(): number {
|
||||
return this._elementRef.nativeElement.offsetHeight;
|
||||
setCssStyle(property: string, value: string) {
|
||||
this._renderer.setElementStyle(this._elementRef.nativeElement, property, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getNativeElement(): HTMLElement {
|
||||
return this._elementRef.nativeElement;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Component, ChangeDetectorRef} from '@angular/core';
|
||||
import {ionicBootstrap} from '../../../../../src';
|
||||
import {ionicBootstrap, reorderArray} from '../../../../../src';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -21,9 +21,7 @@ class E2EPage {
|
||||
}
|
||||
|
||||
reorder(indexes: any) {
|
||||
let element = this.items[indexes.from];
|
||||
this.items.splice(indexes.from, 1);
|
||||
this.items.splice(indexes.to, 0, element);
|
||||
this.items = reorderArray(this.items, indexes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
<ion-content>
|
||||
|
||||
<ion-list [reorder]="isReordering" (ionItemReorder)="reorder($event)">
|
||||
<ion-item *ngFor="let item of items; let index=index"
|
||||
[index]="index"
|
||||
<ion-item *ngFor="let item of items"
|
||||
[style.background]="'rgb('+(255-item*4)+','+(255-item*4)+','+(255-item*4)+')'"
|
||||
[style.height]="item*2+35+'px'">
|
||||
{{item}}
|
||||
|
||||
@@ -4,8 +4,6 @@ import { Content } from '../content/content';
|
||||
import { Ion } from '../ion';
|
||||
import { isTrueProperty } from '../../util/util';
|
||||
import { ItemSlidingGesture } from '../item/item-sliding-gesture';
|
||||
import { ItemReorderGesture } from '../item/item-reorder-gesture';
|
||||
import { nativeTimeout } from '../../util/dom';
|
||||
|
||||
/**
|
||||
* The List is a widely used interface element in almost any mobile app,
|
||||
@@ -25,20 +23,13 @@ import { nativeTimeout } from '../../util/dom';
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'ion-list',
|
||||
host: {
|
||||
'[class.reorder-enabled]': '_enableReorder',
|
||||
}
|
||||
})
|
||||
export class List extends Ion {
|
||||
private _enableReorder: boolean = false;
|
||||
private _enableSliding: boolean = false;
|
||||
private _enableSliding: boolean = true;
|
||||
private _containsSlidingItems: boolean = false;
|
||||
private _slidingGesture: ItemSlidingGesture;
|
||||
private _reorderGesture: ItemReorderGesture;
|
||||
private _lastToIndex: number = -1;
|
||||
|
||||
@Output() ionItemReorder: EventEmitter<{ from: number, to: number }> = new EventEmitter<{ from: number, to: number }>();
|
||||
|
||||
constructor(elementRef: ElementRef, private _rendered: Renderer, private _zone: NgZone, @Optional() private _content: Content) {
|
||||
constructor(elementRef: ElementRef, private _rendered: Renderer) {
|
||||
super(elementRef);
|
||||
}
|
||||
|
||||
@@ -47,7 +38,6 @@ export class List extends Ion {
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._slidingGesture && this._slidingGesture.destroy();
|
||||
this._reorderGesture && this._reorderGesture.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,20 +60,38 @@ export class List extends Ion {
|
||||
* ```
|
||||
* @param {boolean} shouldEnable whether the item-sliding should be enabled or not
|
||||
*/
|
||||
enableSlidingItems(shouldEnable: boolean) {
|
||||
if (this._enableSliding === shouldEnable) {
|
||||
return;
|
||||
}
|
||||
@Input()
|
||||
get sliding(): boolean {
|
||||
return this._enableSliding;
|
||||
}
|
||||
set sliding(val: boolean) {
|
||||
this._enableSliding = isTrueProperty(val);
|
||||
this._updateSlidingState();
|
||||
}
|
||||
|
||||
this._enableSliding = shouldEnable;
|
||||
if (shouldEnable) {
|
||||
console.debug('enableSlidingItems');
|
||||
nativeTimeout(() => this._slidingGesture = new ItemSlidingGesture(this));
|
||||
} else {
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
containsSlidingItem(contains: boolean) {
|
||||
this._containsSlidingItems = contains;
|
||||
this._updateSlidingState();
|
||||
}
|
||||
|
||||
|
||||
private _updateSlidingState() {
|
||||
let shouldSlide = this._enableSliding && this._containsSlidingItems;
|
||||
if (!shouldSlide) {
|
||||
this._slidingGesture && this._slidingGesture.unlisten();
|
||||
this._slidingGesture = null;
|
||||
|
||||
} else if (!this._slidingGesture) {
|
||||
console.debug('enableSlidingItems');
|
||||
this._slidingGesture = new ItemSlidingGesture(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close the open sliding item.
|
||||
*
|
||||
@@ -106,111 +114,6 @@ export class List extends Ion {
|
||||
closeSlidingItems() {
|
||||
this._slidingGesture && this._slidingGesture.closeOpened();
|
||||
}
|
||||
|
||||
setCssClass(classname: string, add: boolean) {
|
||||
this._rendered.setElementClass(this.getNativeElement(), classname, add);
|
||||
}
|
||||
|
||||
reorderStart() {
|
||||
this.setCssClass('reorder-active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
reorderEmit(fromIndex: number, toIndex: number) {
|
||||
this.reorderReset();
|
||||
if (fromIndex !== toIndex) {
|
||||
this._zone.run(() => {
|
||||
this.ionItemReorder.emit({
|
||||
from: fromIndex,
|
||||
to: toIndex,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
scrollContent(scroll: number) {
|
||||
let scrollTop = this._content.getScrollTop() + scroll;
|
||||
if (scroll !== 0) {
|
||||
this._content.scrollTo(0, scrollTop, 0);
|
||||
}
|
||||
return scrollTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
reorderReset() {
|
||||
let children = this.elementRef.nativeElement.children;
|
||||
let len = children.length;
|
||||
this.setCssClass('reorder-active', false);
|
||||
for (let i = 0; i < len; i++) {
|
||||
children[i].style.transform = '';
|
||||
}
|
||||
this._lastToIndex = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
reorderMove(fromIndex: number, toIndex: number, itemHeight: number) {
|
||||
if (this._lastToIndex === -1) {
|
||||
this._lastToIndex = fromIndex;
|
||||
}
|
||||
let lastToIndex = this._lastToIndex;
|
||||
this._lastToIndex = toIndex;
|
||||
|
||||
// TODO: I think both loops can be merged into a single one
|
||||
// but I had no luck last time I tried
|
||||
|
||||
/********* DOM READ ********** */
|
||||
let children = this.elementRef.nativeElement.children;
|
||||
|
||||
/********* DOM WRITE ********* */
|
||||
if (toIndex >= lastToIndex) {
|
||||
for (var i = lastToIndex; i <= toIndex; i++) {
|
||||
if (i !== fromIndex) {
|
||||
children[i].style.transform = (i > fromIndex)
|
||||
? `translateY(${-itemHeight}px)` : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toIndex <= lastToIndex) {
|
||||
for (var i = toIndex; i <= lastToIndex; i++) {
|
||||
if (i !== fromIndex) {
|
||||
children[i].style.transform = (i < fromIndex)
|
||||
? `translateY(${itemHeight}px)` : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
get reorder(): boolean {
|
||||
return this._enableReorder;
|
||||
}
|
||||
|
||||
set reorder(val: boolean) {
|
||||
let enabled = isTrueProperty(val);
|
||||
if (this._enableReorder === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._enableReorder = enabled;
|
||||
if (enabled) {
|
||||
console.debug('enableReorderItems');
|
||||
nativeTimeout(() => this._reorderGesture = new ItemReorderGesture(this));
|
||||
|
||||
} else {
|
||||
this._reorderGesture && this._reorderGesture.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Tabs } from '../components/tabs/tabs';
|
||||
import { Tab } from '../components/tabs/tab';
|
||||
import { List, ListHeader } from '../components/list/list';
|
||||
import { Item, ItemContent } from '../components/item/item';
|
||||
import { Reorder } from '../components/item/item-reorder';
|
||||
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';
|
||||
@@ -145,6 +146,7 @@ export const IONIC_DIRECTIVES: any[] = [
|
||||
ItemContent,
|
||||
ItemSliding,
|
||||
ItemOptions,
|
||||
Reorder,
|
||||
VirtualScroll,
|
||||
VirtualItem,
|
||||
VirtualHeader,
|
||||
|
||||
@@ -19,6 +19,7 @@ export * from './util/click-block';
|
||||
export * from './util/events';
|
||||
export * from './util/keyboard';
|
||||
export * from './util/form';
|
||||
export { reorderArray } from './util/util';
|
||||
|
||||
export * from './animations/animation';
|
||||
export * from './transitions/page-transition';
|
||||
|
||||
@@ -175,3 +175,11 @@ export function getQuerystring(url: string): any {
|
||||
}
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
|
||||
export function reorderArray(array: any[], indexes: {from: number, to: number}): any[] {
|
||||
let element = array[indexes.from];
|
||||
array.splice(indexes.from, 1);
|
||||
array.splice(indexes.to, 0, element);
|
||||
return array;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user