/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import findUpClassName from "../helpers/dom/findUpClassName"; import EventListenerBase from "../helpers/eventListenerBase"; import mediaSizes from "../helpers/mediaSizes"; import clamp from "../helpers/number/clamp"; import safeAssign from "../helpers/object/safeAssign"; import windowSize from "../helpers/windowSize"; import SwipeHandler from "./swipeHandler"; type ResizeSide = 'n' | 'e' | 's' | 'w' | 'ne' | 'se' | 'sw' | 'nw'; export type MovableState = { top?: number; left?: number; width: number; height: number; }; const className = 'movable-element'; const resizeHandlerClassName = className + '-resize-handler'; export type MovableElementOptions = { minWidth: MovableElement['minWidth'], minHeight: MovableElement['minHeight'], element: MovableElement['element'], verifyTouchTarget?: MovableElement['verifyTouchTarget'] }; export default class MovableElement extends EventListenerBase<{ resize: () => void }> { private minWidth: number; private minHeight: number; private element: HTMLElement; private verifyTouchTarget: (e: TouchEvent | MouseEvent) => boolean; private top: number; private left: number; private _width: number; private _height: number; private swipeHandler: SwipeHandler; private handlers: HTMLElement[]; constructor(options: MovableElementOptions) { super(true); safeAssign(this, options); this.top = this.left = this.width = this.height = 0; this.element.classList.add(className); this.addResizeHandlers(); this.setSwipeHandler(); mediaSizes.addEventListener('resize', this.onResize); } private onResize = () => { this.fixDimensions(); this.fixPosition(); this.setPosition(); }; public destroyElements() { this.element.classList.remove(className); if(this.handlers) { this.handlers.forEach(handler => { handler.remove(); }); } } public destroy() { mediaSizes.removeEventListener('resize', this.onResize); this.swipeHandler.removeListeners(); } private addResizeHandlers() { const sides: ResizeSide[] = ['n', 'e', 's', 'w', 'ne', 'se', 'sw', 'nw']; this.handlers = sides.map(side => { const div = document.createElement('div'); div.dataset.side = side; div.classList.add(resizeHandlerClassName, resizeHandlerClassName + '-side-' + side); this.element.append(div); return div; }); } private setSwipeHandler() { let startTop: number, startLeft: number, startWidth: number, startHeight: number, resizingSide: ResizeSide; const swipeHandler = this.swipeHandler = new SwipeHandler({ element: this.element, onSwipe: (xDiff, yDiff, e) => { xDiff *= -1; // to right will be positive yDiff *= -1; // to bottom will be positive // console.log(xDiff, yDiff, e); if(resizingSide) { if(resizingSide.includes('e') || resizingSide.includes('w')) { const isEnlarging = resizingSide.includes('e') && xDiff > 0 || resizingSide.includes('w') && xDiff < 0; const resizeDiff = Math.abs(xDiff) * (isEnlarging ? 1 : -1); const maxPossible = resizingSide.includes('e') ? windowSize.width - startLeft : startWidth + startLeft; this.width = Math.min(maxPossible, startWidth + resizeDiff); } if(resizingSide.includes('n') || resizingSide.includes('s')) { const isEnlarging = resizingSide.includes('s') && yDiff > 0 || resizingSide.includes('n') && yDiff < 0; const resizeDiff = Math.abs(yDiff) * (isEnlarging ? 1 : -1); const maxPossible = resizingSide.includes('s') ? windowSize.height - startTop : startHeight + startTop; this.height = Math.min(maxPossible, startHeight + resizeDiff); } this.fixDimensions(); if(resizingSide.includes('w')) { this.left = Math.min(startLeft + startWidth - this.minWidth, startLeft + xDiff); } if(resizingSide.includes('n')) { this.top = Math.min(startTop + startHeight - this.minHeight, startTop + yDiff); } } else { this.top = startTop + yDiff; this.left = startLeft + xDiff; } this.fixPosition(); this.setPosition(); }, verifyTouchTarget: (e) => { const target = e.target; if(this.verifyTouchTarget && !this.verifyTouchTarget(e)) { return false; } const resizeHandler = findUpClassName(target, resizeHandlerClassName); if(resizeHandler) { resizingSide = resizeHandler.dataset.side as ResizeSide; swipeHandler.setCursor(''); } else { resizingSide = undefined; swipeHandler.setCursor('grabbing'); } return true; }, onFirstSwipe: () => { startTop = this.top; startLeft = this.left; startWidth = this.width; startHeight = this.height; } }); } public setPositionToCenter() { this.top = (windowSize.height / 2) - (this.height / 2); this.left = (windowSize.width / 2) - (this.width / 2); this.setPosition(); } private fixDimensions() { this.width = clamp(this.width, this.minWidth, windowSize.width); this.height = clamp(this.height, this.minHeight, windowSize.height); } private fixPosition() { this.top = clamp(this.top, 0, windowSize.height - this.height); this.left = clamp(this.left, 0, windowSize.width - this.width); } private setPosition() { this.element.style.top = this.top + 'px'; this.element.style.left = this.left + 'px'; this.element.style.right = 'auto'; this.element.style.bottom = 'auto'; this.element.style.width = this.width + 'px'; this.element.style.height = this.height + 'px'; this.dispatchEvent('resize'); } public get width() { return this._width; } public get height() { return this._height; } private set width(value: number) { this._width = value; } private set height(value: number) { this._height = value; } public get state(): MovableState { const {top, left, width, height} = this; return { top, left, width, height }; } public set state(state: MovableState) { const {top, left, width, height} = state; this.top = top; this.left = left; this.width = width; this.height = height; this.onResize(); } }