mirror of
https://github.com/docmost/docmost.git
synced 2025-11-18 17:01:10 +10:00
278 lines
9.9 KiB
TypeScript
278 lines
9.9 KiB
TypeScript
import { Editor, Extension } from "@tiptap/core";
|
|
import { PluginKey, Plugin, PluginSpec } from "@tiptap/pm/state";
|
|
import { EditorProps, EditorView } from "@tiptap/pm/view";
|
|
import { DraggingDOMs, getDndRelatedDOMs, getHoveringCell, HoveringCellInfo } from "./utils";
|
|
import { getDragOverColumn, getDragOverRow } from "./calc-drag-over";
|
|
import { moveColumn, moveRow } from "../utils";
|
|
import { PreviewController } from "./preview/preview-controller";
|
|
import { DropIndicatorController } from "./preview/drop-indicator-controller";
|
|
import { DragHandleController } from "./handle/drag-handle-controller";
|
|
import { EmptyImageController } from "./handle/empty-image-controller";
|
|
import { AutoScrollController } from "./auto-scroll-controller";
|
|
|
|
export const TableDndKey = new PluginKey('table-drag-and-drop')
|
|
|
|
class TableDragHandlePluginSpec implements PluginSpec<void> {
|
|
key = TableDndKey
|
|
props: EditorProps<Plugin<void>>
|
|
|
|
private _colDragHandle: HTMLElement;
|
|
private _rowDragHandle: HTMLElement;
|
|
private _hoveringCell?: HoveringCellInfo;
|
|
private _disposables: (() => void)[] = [];
|
|
private _draggingCoords: { x: number; y: number } = { x: 0, y: 0 };
|
|
private _dragging = false;
|
|
private _draggingDirection: 'col' | 'row' = 'col';
|
|
private _draggingIndex = -1;
|
|
private _droppingIndex = -1;
|
|
private _draggingDOMs?: DraggingDOMs | undefined
|
|
private _startCoords: { x: number; y: number } = { x: 0, y: 0 };
|
|
private _previewController: PreviewController;
|
|
private _dropIndicatorController: DropIndicatorController;
|
|
private _dragHandleController: DragHandleController;
|
|
private _emptyImageController: EmptyImageController;
|
|
private _autoScrollController: AutoScrollController;
|
|
|
|
constructor(public editor: Editor) {
|
|
this.props = {
|
|
handleDOMEvents: {
|
|
pointerover: this._pointerOver,
|
|
}
|
|
}
|
|
|
|
this._dragHandleController = new DragHandleController();
|
|
this._colDragHandle = this._dragHandleController.colDragHandle;
|
|
this._rowDragHandle = this._dragHandleController.rowDragHandle;
|
|
|
|
this._previewController = new PreviewController();
|
|
this._dropIndicatorController = new DropIndicatorController();
|
|
this._emptyImageController = new EmptyImageController();
|
|
|
|
this._autoScrollController = new AutoScrollController();
|
|
|
|
this._bindDragEvents();
|
|
}
|
|
|
|
view = () => {
|
|
const wrapper = this.editor.options.element;
|
|
wrapper.appendChild(this._colDragHandle)
|
|
wrapper.appendChild(this._rowDragHandle)
|
|
wrapper.appendChild(this._previewController.previewRoot)
|
|
wrapper.appendChild(this._dropIndicatorController.dropIndicatorRoot)
|
|
|
|
return {
|
|
update: this.update,
|
|
destroy: this.destroy,
|
|
}
|
|
}
|
|
|
|
update = () => {}
|
|
|
|
destroy = () => {
|
|
if (!this.editor.isDestroyed) return;
|
|
this._dragHandleController.destroy();
|
|
this._emptyImageController.destroy();
|
|
this._previewController.destroy();
|
|
this._dropIndicatorController.destroy();
|
|
this._autoScrollController.stop();
|
|
|
|
this._disposables.forEach(disposable => disposable());
|
|
}
|
|
|
|
private _pointerOver = (view: EditorView, event: PointerEvent) => {
|
|
if (this._dragging) return;
|
|
|
|
// Don't show drag handles in readonly mode
|
|
if (!this.editor.isEditable) {
|
|
this._dragHandleController.hide();
|
|
return;
|
|
}
|
|
|
|
const hoveringCell = getHoveringCell(view, event)
|
|
this._hoveringCell = hoveringCell;
|
|
if (!hoveringCell) {
|
|
this._dragHandleController.hide();
|
|
} else {
|
|
this._dragHandleController.show(this.editor, hoveringCell);
|
|
}
|
|
}
|
|
|
|
private _onDragColStart = (event: DragEvent) => {
|
|
this._onDragStart(event, 'col');
|
|
}
|
|
|
|
private _onDraggingCol = (event: DragEvent) => {
|
|
const draggingDOMs = this._draggingDOMs;
|
|
if (!draggingDOMs) return;
|
|
|
|
this._draggingCoords = { x: event.clientX, y: event.clientY };
|
|
this._previewController.onDragging(draggingDOMs, this._draggingCoords.x, this._draggingCoords.y, 'col');
|
|
|
|
this._autoScrollController.checkXAutoScroll(event.clientX, draggingDOMs);
|
|
|
|
const direction = this._startCoords.x > this._draggingCoords.x ? 'left' : 'right';
|
|
const dragOverColumn = getDragOverColumn(draggingDOMs.table, this._draggingCoords.x);
|
|
if (!dragOverColumn) return;
|
|
|
|
const [col, index] = dragOverColumn;
|
|
this._droppingIndex = index;
|
|
this._dropIndicatorController.onDragging(col, direction, 'col');
|
|
}
|
|
|
|
private _onDragRowStart = (event: DragEvent) => {
|
|
this._onDragStart(event, 'row');
|
|
}
|
|
|
|
private _onDraggingRow = (event: DragEvent) => {
|
|
const draggingDOMs = this._draggingDOMs;
|
|
if (!draggingDOMs) return;
|
|
|
|
this._draggingCoords = { x: event.clientX, y: event.clientY };
|
|
this._previewController.onDragging(draggingDOMs, this._draggingCoords.x, this._draggingCoords.y, 'row');
|
|
|
|
this._autoScrollController.checkYAutoScroll(event.clientY);
|
|
|
|
const direction = this._startCoords.y > this._draggingCoords.y ? 'up' : 'down';
|
|
const dragOverRow = getDragOverRow(draggingDOMs.table, this._draggingCoords.y);
|
|
if (!dragOverRow) return;
|
|
|
|
const [row, index] = dragOverRow;
|
|
this._droppingIndex = index;
|
|
this._dropIndicatorController.onDragging(row, direction, 'row');
|
|
}
|
|
|
|
private _onDragEnd = () => {
|
|
this._dragging = false;
|
|
this._draggingIndex = -1;
|
|
this._droppingIndex = -1;
|
|
this._startCoords = { x: 0, y: 0 };
|
|
this._autoScrollController.stop();
|
|
this._dropIndicatorController.onDragEnd();
|
|
this._previewController.onDragEnd();
|
|
}
|
|
|
|
private _bindDragEvents = () => {
|
|
this._colDragHandle.addEventListener('dragstart', this._onDragColStart);
|
|
this._disposables.push(() => {
|
|
this._colDragHandle.removeEventListener('dragstart', this._onDragColStart);
|
|
})
|
|
|
|
this._colDragHandle.addEventListener('dragend', this._onDragEnd);
|
|
this._disposables.push(() => {
|
|
this._colDragHandle.removeEventListener('dragend', this._onDragEnd);
|
|
})
|
|
|
|
this._rowDragHandle.addEventListener('dragstart', this._onDragRowStart);
|
|
this._disposables.push(() => {
|
|
this._rowDragHandle.removeEventListener('dragstart', this._onDragRowStart);
|
|
})
|
|
|
|
this._rowDragHandle.addEventListener('dragend', this._onDragEnd);
|
|
this._disposables.push(() => {
|
|
this._rowDragHandle.removeEventListener('dragend', this._onDragEnd);
|
|
})
|
|
|
|
const ownerDocument = this.editor.view.dom?.ownerDocument
|
|
if (ownerDocument) {
|
|
// To make `drop` event work, we need to prevent the default behavior of the
|
|
// `dragover` event for drop zone. Here we set the whole document as the
|
|
// drop zone so that even the mouse moves outside the editor, the `drop`
|
|
// event will still be triggered.
|
|
ownerDocument.addEventListener('drop', this._onDrop);
|
|
ownerDocument.addEventListener('dragover', this._onDrag);
|
|
this._disposables.push(() => {
|
|
ownerDocument.removeEventListener('drop', this._onDrop);
|
|
ownerDocument.removeEventListener('dragover', this._onDrag);
|
|
});
|
|
}
|
|
}
|
|
|
|
private _onDragStart = (event: DragEvent, type: 'col' | 'row') => {
|
|
const dataTransfer = event.dataTransfer;
|
|
if (dataTransfer) {
|
|
dataTransfer.effectAllowed = 'move';
|
|
this._emptyImageController.hideDragImage(dataTransfer);
|
|
}
|
|
this._dragging = true;
|
|
this._draggingDirection = type;
|
|
this._startCoords = { x: event.clientX, y: event.clientY };
|
|
const draggingIndex = (type === 'col' ? this._hoveringCell?.colIndex : this._hoveringCell?.rowIndex) ?? 0;
|
|
|
|
this._draggingIndex = draggingIndex;
|
|
|
|
const relatedDoms = getDndRelatedDOMs(
|
|
this.editor.view,
|
|
this._hoveringCell?.cellPos,
|
|
draggingIndex,
|
|
type
|
|
)
|
|
this._draggingDOMs = relatedDoms;
|
|
|
|
const index = type === 'col' ? this._hoveringCell?.colIndex : this._hoveringCell?.rowIndex;
|
|
|
|
this._previewController.onDragStart(relatedDoms, index, type);
|
|
this._dropIndicatorController.onDragStart(relatedDoms, type);
|
|
}
|
|
|
|
private _onDrag = (event: DragEvent) => {
|
|
event.preventDefault()
|
|
if (!this._dragging) return;
|
|
if (this._draggingDirection === 'col') {
|
|
this._onDraggingCol(event);
|
|
} else {
|
|
this._onDraggingRow(event);
|
|
}
|
|
}
|
|
|
|
private _onDrop = () => {
|
|
if (!this._dragging) return;
|
|
const direction = this._draggingDirection;
|
|
const from = this._draggingIndex;
|
|
const to = this._droppingIndex;
|
|
const tr = this.editor.state.tr;
|
|
const pos = this.editor.state.selection.from;
|
|
|
|
if (direction === 'col') {
|
|
const canMove = moveColumn({
|
|
tr,
|
|
originIndex: from,
|
|
targetIndex: to,
|
|
select: true,
|
|
pos,
|
|
})
|
|
if (canMove) {
|
|
this.editor.view.dispatch(tr);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (direction === 'row') {
|
|
const canMove = moveRow({
|
|
tr,
|
|
originIndex: from,
|
|
targetIndex: to,
|
|
select: true,
|
|
pos,
|
|
})
|
|
if (canMove) {
|
|
this.editor.view.dispatch(tr);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const TableDndExtension = Extension.create({
|
|
name: 'table-drag-and-drop',
|
|
addProseMirrorPlugins() {
|
|
const editor = this.editor
|
|
|
|
const dragHandlePluginSpec = new TableDragHandlePluginSpec(editor)
|
|
const dragHandlePlugin = new Plugin(dragHandlePluginSpec)
|
|
|
|
return [dragHandlePlugin]
|
|
}
|
|
})
|