rc-dock
Version:
dock layout for react component
351 lines (296 loc) • 9.66 kB
text/typescript
import classNames from "classnames";
import {groupClassNames} from "../Utils";
export type DragType = 'left' | 'right' | 'touch';
interface DragDropComponent {
element: HTMLElement;
ownerDocument: Document;
dragType: DragType;
baseX: number;
baseY: number;
scaleX: number;
scaleY: number;
}
export class DragState {
_init: boolean;
event: MouseEvent | TouchEvent;
component: DragDropComponent;
pageX = 0;
pageY = 0;
clientX = 0;
clientY = 0;
dx = 0;
dy = 0;
dropped: any = false;
constructor(event: MouseEvent | TouchEvent, component: DragDropComponent, init = false) {
this.event = event;
this.component = component;
this._init = init;
if (event) {
if (event.type.startsWith('touch')) {
let touch: Touch;
if (event.type === 'touchend') {
touch = (event as TouchEvent).changedTouches[0];
} else {
touch = (event as TouchEvent).touches[0];
}
this.pageX = touch.pageX;
this.pageY = touch.pageY;
this.clientX = touch.clientX;
this.clientY = touch.clientY;
} else if ('pageX' in event) {
this.pageX = event.pageX;
this.pageY = event.pageY;
this.clientX = event.clientX;
this.clientY = event.clientY;
}
this.dx = (this.pageX - component.baseX) * component.scaleX;
this.dy = (this.pageY - component.baseY) * component.scaleY;
}
}
moved(): boolean {
return Math.abs(this.dx) >= 1 || Math.abs(this.dy) >= 1;
}
/**
* @param refElement, the element being moved
* @param draggingHtml, the element show in the dragging layer
*/
startDrag(refElement?: HTMLElement, draggingHtml?: HTMLElement | string) {
if (!this._init) {
throw new Error('startDrag can only be used in onDragStart callback');
}
if (refElement === undefined) {
refElement = this.component.element;
}
createDraggingElement(this, refElement, draggingHtml);
this.component.ownerDocument.body.classList.add('dock-dragging');
}
setData(data?: { [key: string]: any }, scope?: any) {
if (!this._init) {
throw new Error('setData can only be used in onDragStart callback');
}
_dataScope = scope;
_data = data;
}
getData(field: string, scope?: any) {
if (!_data) {
// todo: find drag string from event and convert it to _data if possible
_data = {};
}
if (scope === _dataScope && _data) {
return _data[field];
}
return null;
}
static getData(field: string, scope?: any) {
if (scope === _dataScope && _data) {
return _data[field];
}
return null;
}
get dragType(): DragType {
return this.component.dragType;
}
acceptMessage: string;
rejected: boolean;
accept(message: string = '') {
this.acceptMessage = message;
this.rejected = false;
}
reject() {
this.rejected = true;
}
_onMove() {
if (_data) {
let ownerDocument = this.component.ownerDocument;
let searchElement = ownerDocument.elementFromPoint(this.clientX, this.clientY) as HTMLElement;
let droppingHost: HandlerHost;
while (searchElement && searchElement !== ownerDocument.body) {
if (_dragListeners.has(searchElement)) {
let host = _dragListeners.get(searchElement);
let handlers = host.getHandlers();
if (handlers.onDragOverT) {
handlers.onDragOverT(this);
if (this.acceptMessage != null) {
droppingHost = host;
break;
}
}
}
searchElement = searchElement.parentElement;
}
setDroppingHandler(droppingHost, this);
}
moveDraggingElement(this);
}
_onDragEnd(canceled: boolean = false) {
if (_droppingHandlers && _droppingHandlers.onDropT && !canceled) {
this.dropped = _droppingHandlers.onDropT(this);
if (this.component.dragType === 'right') {
// prevent the next menu event if drop handler is called on right mouse button
this.component.ownerDocument.addEventListener('contextmenu', preventDefault, true);
setTimeout(() => {
this.component.ownerDocument.removeEventListener('contextmenu', preventDefault, true);
}, 0);
}
}
destroyDraggingElement(this);
this.component.ownerDocument.body.classList.remove('dock-dragging');
}
getRect() {
let x = this.clientX;
let y = this.clientY;
let w = this.dx;
let h = this.dy;
if (w < 0) {
w = -w;
} else {
x -= w;
}
if (h < 0) {
h = -h;
} else {
y -= h;
}
return new DOMRect(x, y, w, h);
}
}
function preventDefault(e: Event) {
e.preventDefault();
e.stopPropagation();
}
export type DragHandler = (state: DragState) => void;
export type DropHandler = (state: DragState) => any;
let _dataScope: any;
let _data: { [key: string]: any };
let _draggingState: DragState;
// applying dragging style
let _refElement: HTMLElement;
let _droppingHost: HandlerHost;
let _droppingHandlers: DragHandlers;
function setDroppingHandler(host: HandlerHost, state: DragState) {
if (_droppingHost === host) {
return;
}
if (_droppingHandlers && _droppingHandlers.onDragLeaveT) {
_droppingHandlers.onDragLeaveT(state);
}
_droppingHost = host;
_droppingHandlers = _droppingHost?.getHandlers();
}
export interface DragHandlers {
onDragOverT?: DragHandler;
onDragLeaveT?: DragHandler;
onDropT?: DropHandler;
}
export interface HandlerHost {
getHandlers(): DragHandlers;
}
let _dragListeners: WeakMap<HTMLElement, HandlerHost> = new WeakMap<HTMLElement, HandlerHost>();
export function isDragging() {
return _draggingState != null;
}
export function addHandlers(element: HTMLElement, handler: HandlerHost) {
_dragListeners.set(element, handler);
}
export function removeHandlers(element: HTMLElement) {
let host = _dragListeners.get(element);
if (host === _droppingHost) {
_droppingHost = null;
_droppingHandlers = null;
}
_dragListeners.delete(element);
}
let _draggingDiv: HTMLDivElement;
let _draggingIcon: HTMLDivElement;
function _createDraggingDiv(doc: Document) {
_draggingDiv = doc.createElement('div');
_draggingIcon = doc.createElement('div');
const tabGroup = (_data && 'tabGroup' in _data ? _data['tabGroup'] : undefined) as string | undefined;
_draggingDiv.className = classNames(groupClassNames(tabGroup), 'dragging-layer');
_draggingDiv.appendChild(document.createElement('div')); // place holder for dragging element
_draggingDiv.appendChild(_draggingIcon);
}
function createDraggingElement(state: DragState, refElement: HTMLElement, draggingHtml?: HTMLElement | string) {
_draggingState = state;
if (refElement) {
refElement.classList.add('dragging');
_refElement = refElement;
}
_createDraggingDiv(state.component.ownerDocument);
state.component.ownerDocument.body.appendChild(_draggingDiv);
let draggingWidth = 0;
let draggingHeight = 0;
if (draggingHtml === undefined) {
draggingHtml = state.component.element;
}
if (draggingHtml && 'outerHTML' in (draggingHtml as any)) {
draggingWidth = (draggingHtml as HTMLElement).offsetWidth;
draggingHeight = (draggingHtml as HTMLElement).offsetHeight;
draggingHtml = (draggingHtml as HTMLElement).outerHTML;
}
if (draggingHtml) {
_draggingDiv.firstElementChild.outerHTML = draggingHtml as string;
if (window.getComputedStyle(_draggingDiv.firstElementChild).backgroundColor === 'rgba(0, 0, 0, 0)') {
(_draggingDiv.firstElementChild as HTMLElement).style.backgroundColor
= window.getComputedStyle(_draggingDiv).getPropertyValue('--default-background-color');
}
if (draggingWidth) {
if (draggingWidth > 400) draggingWidth = 400;
(_draggingDiv.firstElementChild as HTMLElement).style.width = `${draggingWidth}px`;
}
if (draggingHeight) {
if (draggingHeight > 300) draggingHeight = 300;
(_draggingDiv.firstElementChild as HTMLElement).style.height = `${draggingHeight}px`;
}
}
for (let callback of _dragStateListener) {
if (_dataScope) {
callback(_dataScope);
} else {
callback(true);
}
}
}
function moveDraggingElement(state: DragState) {
_draggingDiv.style.left = `${state.pageX}px`;
_draggingDiv.style.top = `${state.pageY}px`;
if (state.rejected) {
_draggingIcon.className = 'drag-accept-reject';
} else if (state.acceptMessage) {
_draggingIcon.className = state.acceptMessage;
} else {
_draggingIcon.className = '';
}
}
export function destroyDraggingElement(e: DragState) {
if (_refElement) {
_refElement.classList.remove('dragging');
_refElement = null;
}
if (_draggingDiv) {
_draggingDiv.remove();
_draggingDiv = null;
}
_draggingState = null;
setDroppingHandler(null, e);
_dataScope = null;
_data = null;
for (let callback of _dragStateListener) {
callback(null);
}
}
let _dragStateListener: Set<(scope: any) => void> = new Set();
export function addDragStateListener(callback: (scope: any) => void) {
_dragStateListener.add(callback);
}
export function removeDragStateListener(callback: (scope: any) => void) {
_dragStateListener.delete(callback);
}
// work around for drag scroll issue on IOS
if (typeof window !== 'undefined' && window.navigator && window.navigator.platform && /iP(ad|hone|od)/.test(window.navigator.platform)) {
document.addEventListener('touchmove', (e: TouchEvent) => {
if (e.touches.length === 1 && document.body.classList.contains('dock-dragging')) {
e.preventDefault();
}
}, {passive: false});
}