tweb/src/helpers/dom/sortable.ts

203 lines
6.4 KiB
TypeScript

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {ScrollableBase} from '../../components/scrollable';
import SwipeHandler from '../../components/swipeHandler';
import IS_TOUCH_SUPPORTED from '../../environment/touchSupport';
import rootScope from '../../lib/rootScope';
import liteMode from '../liteMode';
import {Middleware} from '../middleware';
import clamp from '../number/clamp';
import safeAssign from '../object/safeAssign';
import pause from '../schedulers/pause';
import cancelEvent from './cancelEvent';
import {attachClickEvent} from './clickEvent';
import findUpAsChild from './findUpAsChild';
import positionElementByIndex from './positionElementByIndex';
import whichChild from './whichChild';
export default class Sortable {
private element: HTMLElement;
private elementRect: DOMRect;
private containerRect: DOMRect;
private scrollableRect: DOMRect;
private minY: number;
private maxY: number;
private siblings: HTMLElement[];
private swipeHandler: SwipeHandler;
private startScrollPos: number;
private addScrollPos: number;
private list: HTMLElement;
private middleware: Middleware;
private onSort: (prevIdx: number, newIdx: number) => void;
private scrollable: ScrollableBase;
constructor(options: {
list: HTMLElement,
middleware: Middleware,
onSort: Sortable['onSort'],
scrollable?: Sortable['scrollable']
}) {
safeAssign(this, options);
this.swipeHandler = new SwipeHandler({
element: this.list,
onSwipe: this.onSwipe,
verifyTouchTarget: this.verifyTouchTarget,
onStart: this.onStart,
onReset: this.onReset,
setCursorTo: document.body,
middleware: this.middleware,
withDelay: true
});
}
private onSwipe = (xDiff: number, yDiff: number) => {
yDiff = clamp(yDiff, this.minY, this.maxY);
this.element.style.transform = `translateY(${yDiff}px)`;
const count = Math.round(Math.abs(yDiff) / this.elementRect.height);
const lastSiblings = this.siblings;
this.siblings = [];
const property = yDiff < 0 ? 'previousElementSibling' : 'nextElementSibling';
let sibling = this.element[property] as HTMLElement;
for(let i = 0; i < count; ++i) {
if(this.getSortableTarget(sibling)) {
this.siblings.push(sibling);
sibling = sibling[property] as HTMLElement;
} else {
break;
}
}
(lastSiblings || []).forEach((sibling) => {
if(!this.siblings.includes(sibling)) {
sibling.style.transform = '';
}
});
this.siblings.forEach((sibling) => {
const y = this.elementRect.height * (yDiff < 0 ? 1 : -1);
sibling.style.transform = `translateY(${y}px)`;
});
if(this.scrollableRect) {
const diff = yDiff;
const toEnd = diff > 0;
const elementEndPos = toEnd ? this.elementRect.bottom : this.elementRect.top;
const clientY = elementEndPos + diff - this.addScrollPos;
// console.log(clientY, this.scrollableRect.top, elementEndPos, diff, this.addScrollPos, toEnd);
let change = 2;
if((clientY + (toEnd ? 0 : this.elementRect.height)) >= this.scrollableRect.bottom/* && diff < this.maxY */) {
} else if((clientY - (toEnd ? this.elementRect.height : 0)) <= this.scrollableRect.top/* && diff > this.minY */) {
change *= -1;
} else {
change = undefined;
}
if(change !== undefined) {
this.scrollable.container[this.scrollable.scrollProperty] += change;
}
}
};
private verifyTouchTarget = (e: {target: EventTarget}) => {
if(this.list.classList.contains('is-reordering')) {
return false;
}
this.element = this.getSortableTarget(e.target as HTMLElement);
return !!this.element/* && pause(150).then(() => true) */;
};
private onScroll = () => {
const scrollPos = this.scrollable.container[this.scrollable.scrollProperty];
const diff = this.addScrollPos = scrollPos - this.startScrollPos;
const isVertical = this.scrollable.scrollProperty === 'scrollTop';
this.swipeHandler.add(isVertical ? 0 : diff, isVertical ? diff : 0);
};
private onStart = () => {
this.list.classList.add('is-reordering');
this.element.classList.add('is-dragging', 'no-transition');
this.swipeHandler.setCursor('grabbing');
this.elementRect = this.element.getBoundingClientRect();
this.containerRect = this.list.getBoundingClientRect();
this.minY = this.containerRect.top - this.elementRect.top;
this.maxY = this.containerRect.bottom - this.elementRect.bottom;
this.addScrollPos = 0;
if(this.scrollable) {
this.startScrollPos = this.scrollable.container[this.scrollable.scrollProperty];
this.scrollableRect = this.scrollable.container.getBoundingClientRect();
this.scrollable.container.addEventListener('scroll', this.onScroll);
}
};
private onReset = async() => {
const length = this.siblings.length;
const move = length && length * (this.siblings[0].previousElementSibling === this.element ? 1 : -1);
const idx = whichChild(this.element);
const newIdx = idx + move;
this.element.classList.remove('no-transition');
this.element.style.transform = move ? `translateY(${move * this.elementRect.height}px)` : '';
this.swipeHandler.setCursor('');
if(this.scrollable) {
this.scrollable.container.removeEventListener('scroll', this.onScroll);
}
if(!IS_TOUCH_SUPPORTED) {
attachClickEvent(document.body, cancelEvent, {capture: true, once: true});
}
if(liteMode.isAvailable('animations')) {
await pause(250);
}
this.list.classList.remove('is-reordering');
this.element.classList.remove('is-dragging');
positionElementByIndex(this.element, this.list, newIdx, idx);
[this.element, ...this.siblings].forEach((element) => {
element.style.transform = '';
});
this.element =
this.siblings =
this.elementRect =
this.containerRect =
this.minY =
this.maxY =
this.startScrollPos =
this.addScrollPos =
undefined;
// cancelClick = true;
if(!move) {
return;
}
this.onSort(idx, newIdx);
};
private getSortableTarget(target: HTMLElement) {
if(!target) {
return;
}
let child = findUpAsChild(target as HTMLElement, this.list);
if(child && child.classList.contains('cant-sort')) {
child = undefined;
}
return child;
}
}