mirror of
https://github.com/docmost/docmost.git
synced 2025-11-10 01:52:05 +10:00
feat: table row/column drag and drop (#1467)
* chore: add dev container * feat: add drag handle when hovering cell * feat: add column drag and drop * feat: add support for row drag and drop * refactor: extract preview controllers * fix: hover issue * refactor: add handle controller * chore: f * chore: remove log * chore: remove dev files * feat: hide other drop indicators when table dnd working * feat: add auto scroll and bug fix * chore: f * fix: firefox
This commit is contained in:
@ -38,6 +38,7 @@ import {
|
||||
Embed,
|
||||
SearchAndReplace,
|
||||
Mention,
|
||||
TableDndExtension,
|
||||
} from "@docmost/editor-ext";
|
||||
import {
|
||||
randomElement,
|
||||
@ -168,6 +169,7 @@ export const mainExtensions = [
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableDndExtension,
|
||||
MathInline.configure({
|
||||
view: MathInlineView,
|
||||
}),
|
||||
|
||||
@ -375,7 +375,7 @@ export default function PageEditor({
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<div className="editor-container" style={{ position: "relative" }}>
|
||||
<div ref={menuContainerRef}>
|
||||
<EditorContent editor={editor} />
|
||||
|
||||
|
||||
@ -45,6 +45,10 @@
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&[data-direction='horizontal'] {
|
||||
rotate: 90deg;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .drag-handle {
|
||||
|
||||
@ -8,6 +8,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-dnd-preview {
|
||||
padding: 0;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.table-dnd-drop-indicator {
|
||||
background-color: #adf;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
@ -118,3 +128,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor-container:has(.table-dnd-drop-indicator[data-dragging="true"]) {
|
||||
.prosemirror-dropcursor-block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.prosemirror-dropcursor-inline {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.1.0",
|
||||
"@docmost/editor-ext": "workspace:*",
|
||||
"@floating-ui/dom": "^1.7.3",
|
||||
"@hocuspocus/extension-redis": "^2.15.2",
|
||||
"@hocuspocus/provider": "^2.15.2",
|
||||
"@hocuspocus/server": "^2.15.2",
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
import { DraggingDOMs } from "./utils";
|
||||
|
||||
const EDGE_THRESHOLD = 100;
|
||||
const SCROLL_SPEED = 10;
|
||||
|
||||
export class AutoScrollController {
|
||||
private _autoScrollInterval?: number;
|
||||
|
||||
checkYAutoScroll = (clientY: number) => {
|
||||
const scrollContainer = document.documentElement;
|
||||
|
||||
if (clientY < 0 + EDGE_THRESHOLD) {
|
||||
this._startYAutoScroll(scrollContainer!, -1 * SCROLL_SPEED);
|
||||
} else if (clientY > window.innerHeight - EDGE_THRESHOLD) {
|
||||
this._startYAutoScroll(scrollContainer!, SCROLL_SPEED);
|
||||
} else {
|
||||
this._stopYAutoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
checkXAutoScroll = (clientX: number, draggingDOMs: DraggingDOMs) => {
|
||||
const table = draggingDOMs?.table;
|
||||
if (!table) return;
|
||||
|
||||
const scrollContainer = table.closest<HTMLElement>('.tableWrapper');
|
||||
const editorRect = scrollContainer.getBoundingClientRect();
|
||||
if (!scrollContainer) return;
|
||||
|
||||
if (clientX < editorRect.left + EDGE_THRESHOLD) {
|
||||
this._startXAutoScroll(scrollContainer!, -1 * SCROLL_SPEED);
|
||||
} else if (clientX > editorRect.right - EDGE_THRESHOLD) {
|
||||
this._startXAutoScroll(scrollContainer!, SCROLL_SPEED);
|
||||
} else {
|
||||
this._stopXAutoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
stop = () => {
|
||||
this._stopXAutoScroll();
|
||||
this._stopYAutoScroll();
|
||||
}
|
||||
|
||||
private _startXAutoScroll = (scrollContainer: HTMLElement, speed: number) => {
|
||||
if (this._autoScrollInterval) {
|
||||
clearInterval(this._autoScrollInterval);
|
||||
}
|
||||
|
||||
this._autoScrollInterval = window.setInterval(() => {
|
||||
scrollContainer.scrollLeft += speed;
|
||||
}, 16);
|
||||
}
|
||||
|
||||
private _stopXAutoScroll = () => {
|
||||
if (this._autoScrollInterval) {
|
||||
clearInterval(this._autoScrollInterval);
|
||||
this._autoScrollInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _startYAutoScroll = (scrollContainer: HTMLElement, speed: number) => {
|
||||
if (this._autoScrollInterval) {
|
||||
clearInterval(this._autoScrollInterval);
|
||||
}
|
||||
|
||||
this._autoScrollInterval = window.setInterval(() => {
|
||||
scrollContainer.scrollTop += speed;
|
||||
}, 16);
|
||||
}
|
||||
|
||||
private _stopYAutoScroll = () => {
|
||||
if (this._autoScrollInterval) {
|
||||
clearInterval(this._autoScrollInterval);
|
||||
this._autoScrollInterval = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
packages/editor-ext/src/lib/table/dnd/calc-drag-over.ts
Normal file
44
packages/editor-ext/src/lib/table/dnd/calc-drag-over.ts
Normal file
@ -0,0 +1,44 @@
|
||||
function findDragOverElement(
|
||||
elements: Element[],
|
||||
pointer: number,
|
||||
axis: 'x' | 'y',
|
||||
): [Element, number] | undefined {
|
||||
const startProp = axis === 'x' ? 'left' : 'top'
|
||||
const endProp = axis === 'x' ? 'right' : 'bottom'
|
||||
const lastIndex = elements.length - 1
|
||||
|
||||
const index = elements.findIndex((el, index) => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const boundaryStart = rect[startProp]
|
||||
const boundaryEnd = rect[endProp]
|
||||
|
||||
// The pointer is within the boundary of the current element.
|
||||
if (boundaryStart <= pointer && pointer <= boundaryEnd) return true
|
||||
// The pointer is beyond the last element.
|
||||
if (index === lastIndex && pointer > boundaryEnd) return true
|
||||
// The pointer is before the first element.
|
||||
if (index === 0 && pointer < boundaryStart) return true
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return index >= 0 ? [elements[index], index] : undefined
|
||||
}
|
||||
|
||||
export function getDragOverColumn(
|
||||
table: HTMLTableElement,
|
||||
pointerX: number,
|
||||
): [element: Element, index: number] | undefined {
|
||||
const firstRow = table.querySelector('tr')
|
||||
if (!firstRow) return
|
||||
const cells = Array.from(firstRow.children)
|
||||
return findDragOverElement(cells, pointerX, 'x')
|
||||
}
|
||||
|
||||
export function getDragOverRow(
|
||||
table: HTMLTableElement,
|
||||
pointerY: number,
|
||||
): [element: Element, index: number] | undefined {
|
||||
const rows = Array.from(table.querySelectorAll('tr'))
|
||||
return findDragOverElement(rows, pointerY, 'y')
|
||||
}
|
||||
271
packages/editor-ext/src/lib/table/dnd/dnd-extension.ts
Normal file
271
packages/editor-ext/src/lib/table/dnd/dnd-extension.ts
Normal file
@ -0,0 +1,271 @@
|
||||
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;
|
||||
|
||||
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]
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,105 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { HoveringCellInfo } from "../utils";
|
||||
import { computePosition, offset } from "@floating-ui/dom";
|
||||
|
||||
export class DragHandleController {
|
||||
private _colDragHandle: HTMLElement;
|
||||
private _rowDragHandle: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
this._colDragHandle = this._createDragHandleDom('col');
|
||||
this._rowDragHandle = this._createDragHandleDom('row');
|
||||
}
|
||||
|
||||
get colDragHandle() {
|
||||
return this._colDragHandle;
|
||||
}
|
||||
|
||||
get rowDragHandle() {
|
||||
return this._rowDragHandle;
|
||||
}
|
||||
|
||||
show = (editor: Editor, hoveringCell: HoveringCellInfo) => {
|
||||
this._showColDragHandle(editor, hoveringCell);
|
||||
this._showRowDragHandle(editor, hoveringCell);
|
||||
}
|
||||
|
||||
hide = () => {
|
||||
Object.assign(this._colDragHandle.style, {
|
||||
display: 'none',
|
||||
left: '-999px',
|
||||
top: '-999px',
|
||||
});
|
||||
Object.assign(this._rowDragHandle.style, {
|
||||
display: 'none',
|
||||
left: '-999px',
|
||||
top: '-999px',
|
||||
});
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this._colDragHandle.remove()
|
||||
this._rowDragHandle.remove()
|
||||
}
|
||||
|
||||
private _createDragHandleDom = (type: 'col' | 'row') => {
|
||||
const dragHandle = document.createElement('div')
|
||||
dragHandle.classList.add('drag-handle')
|
||||
dragHandle.setAttribute('draggable', 'true')
|
||||
dragHandle.setAttribute('data-direction', type === 'col' ? 'horizontal' : 'vertical')
|
||||
dragHandle.setAttribute('data-drag-handle', '')
|
||||
Object.assign(dragHandle.style, {
|
||||
position: 'absolute',
|
||||
top: '-999px',
|
||||
left: '-999px',
|
||||
display: 'none',
|
||||
})
|
||||
return dragHandle;
|
||||
}
|
||||
|
||||
private _showColDragHandle(editor: Editor, hoveringCell: HoveringCellInfo) {
|
||||
const referenceCell = editor.view.nodeDOM(hoveringCell.colFirstCellPos);
|
||||
if (!referenceCell) return;
|
||||
|
||||
const yOffset = -1 * parseInt(getComputedStyle(this._colDragHandle).height) / 2;
|
||||
|
||||
computePosition(
|
||||
referenceCell as HTMLElement,
|
||||
this._colDragHandle,
|
||||
{
|
||||
placement: 'top',
|
||||
middleware: [offset(yOffset)]
|
||||
}
|
||||
)
|
||||
.then(({ x, y }) => {
|
||||
Object.assign(this._colDragHandle.style, {
|
||||
display: 'block',
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
private _showRowDragHandle(editor: Editor, hoveringCell: HoveringCellInfo) {
|
||||
const referenceCell = editor.view.nodeDOM(hoveringCell.rowFirstCellPos);
|
||||
if (!referenceCell) return;
|
||||
|
||||
const xOffset = -1 * parseInt(getComputedStyle(this._rowDragHandle).width) / 2;
|
||||
|
||||
computePosition(
|
||||
referenceCell as HTMLElement,
|
||||
this._rowDragHandle,
|
||||
{
|
||||
middleware: [offset(xOffset)],
|
||||
placement: 'left'
|
||||
}
|
||||
)
|
||||
.then(({ x, y}) => {
|
||||
Object.assign(this._rowDragHandle.style, {
|
||||
display: 'block',
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
export class EmptyImageController {
|
||||
private _emptyImage: HTMLImageElement;
|
||||
|
||||
constructor() {
|
||||
this._emptyImage = new Image(1, 1);
|
||||
this._emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
}
|
||||
|
||||
get emptyImage() {
|
||||
return this._emptyImage;
|
||||
}
|
||||
|
||||
hideDragImage = (dataTransfer: DataTransfer) => {
|
||||
dataTransfer.effectAllowed = 'move';
|
||||
dataTransfer.setDragImage(this._emptyImage, 0, 0);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this._emptyImage.remove();
|
||||
}
|
||||
}
|
||||
1
packages/editor-ext/src/lib/table/dnd/index.ts
Normal file
1
packages/editor-ext/src/lib/table/dnd/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './dnd-extension'
|
||||
@ -0,0 +1,102 @@
|
||||
import { computePosition, offset } from "@floating-ui/dom";
|
||||
import { DraggingDOMs } from "../utils";
|
||||
|
||||
const DROP_INDICATOR_WIDTH = 2;
|
||||
|
||||
export class DropIndicatorController {
|
||||
private _dropIndicator: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
this._dropIndicator = document.createElement('div');
|
||||
this._dropIndicator.classList.add('table-dnd-drop-indicator');
|
||||
Object.assign(this._dropIndicator.style, {
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
get dropIndicatorRoot() {
|
||||
return this._dropIndicator;
|
||||
}
|
||||
|
||||
onDragStart = (relatedDoms: DraggingDOMs, type: 'col' | 'row') => {
|
||||
this._initDropIndicatorStyle(relatedDoms.table, type);
|
||||
this._initDropIndicatorPosition(relatedDoms.cell, type);
|
||||
this._dropIndicator.dataset.dragging = 'true';
|
||||
}
|
||||
|
||||
onDragEnd = () => {
|
||||
Object.assign(this._dropIndicator.style, { display: 'none' });
|
||||
this._dropIndicator.dataset.dragging = 'false';
|
||||
}
|
||||
|
||||
onDragging = (target: Element, direction: 'left' | 'right' | 'up' | 'down', type: 'col' | 'row') => {
|
||||
if (type === 'col') {
|
||||
void computePosition(target, this._dropIndicator, {
|
||||
placement: direction === 'left' ? 'left' : 'right',
|
||||
middleware: [offset((direction === 'left' ? -1 * DROP_INDICATOR_WIDTH : 0))],
|
||||
}).then(({ x }) => {
|
||||
Object.assign(this._dropIndicator.style, { left: `${x}px` });
|
||||
})
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'row') {
|
||||
void computePosition(target, this._dropIndicator, {
|
||||
placement: direction === 'up' ? 'top' : 'bottom',
|
||||
middleware: [offset((direction === 'up' ? -1 * DROP_INDICATOR_WIDTH : 0))],
|
||||
}).then(({ y }) => {
|
||||
Object.assign(this._dropIndicator.style, { top: `${y}px` });
|
||||
})
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this._dropIndicator.remove();
|
||||
}
|
||||
|
||||
private _initDropIndicatorStyle = (table: HTMLElement, type: 'col' | 'row') => {
|
||||
const tableRect = table.getBoundingClientRect();
|
||||
|
||||
if (type === 'col') {
|
||||
Object.assign(this._dropIndicator.style, {
|
||||
display: 'block',
|
||||
width: `${DROP_INDICATOR_WIDTH}px`,
|
||||
height: `${tableRect.height}px`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'row') {
|
||||
Object.assign(this._dropIndicator.style, {
|
||||
display: 'block',
|
||||
width: `${tableRect.width}px`,
|
||||
height: `${DROP_INDICATOR_WIDTH}px`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _initDropIndicatorPosition = (cell: HTMLElement, type: 'col' | 'row') => {
|
||||
void computePosition(cell, this._dropIndicator, {
|
||||
placement: type === 'row' ? 'right' : 'bottom',
|
||||
middleware: [
|
||||
offset(({ rects }) => {
|
||||
if (type === 'col') {
|
||||
return -rects.reference.height
|
||||
}
|
||||
return -rects.reference.width
|
||||
}),
|
||||
],
|
||||
}).then(({ x, y }) => {
|
||||
Object.assign(this._dropIndicator.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
import { computePosition, offset, ReferenceElement } from "@floating-ui/dom";
|
||||
import { DraggingDOMs } from "../utils";
|
||||
import { clearPreviewDOM, createPreviewDOM } from "./render-preview";
|
||||
|
||||
export class PreviewController {
|
||||
private _preview: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
this._preview = document.createElement('div');
|
||||
this._preview.classList.add('table-dnd-preview');
|
||||
this._preview.classList.add('ProseMirror');
|
||||
Object.assign(this._preview.style, {
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
display: 'none',
|
||||
});
|
||||
}
|
||||
|
||||
get previewRoot(): HTMLElement {
|
||||
return this._preview;
|
||||
}
|
||||
|
||||
onDragStart = (relatedDoms: DraggingDOMs, index: number | undefined, type: 'col' | 'row') => {
|
||||
this._initPreviewStyle(relatedDoms.table, relatedDoms.cell, type);
|
||||
createPreviewDOM(relatedDoms.table, this._preview, index, type)
|
||||
this._initPreviewPosition(relatedDoms.cell, type);
|
||||
}
|
||||
|
||||
onDragEnd = () => {
|
||||
clearPreviewDOM(this._preview);
|
||||
Object.assign(this._preview.style, { display: 'none' });
|
||||
}
|
||||
|
||||
onDragging = (relatedDoms: DraggingDOMs, x: number, y: number, type: 'col' | 'row') => {
|
||||
this._updatePreviewPosition(x, y, relatedDoms.cell, type);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this._preview.remove();
|
||||
}
|
||||
|
||||
private _initPreviewStyle(table: HTMLTableElement, cell: HTMLTableCellElement, type: 'col' | 'row') {
|
||||
const tableRect = table.getBoundingClientRect();
|
||||
const cellRect = cell.getBoundingClientRect();
|
||||
|
||||
if (type === 'col') {
|
||||
Object.assign(this._preview.style, {
|
||||
display: 'block',
|
||||
width: `${cellRect.width}px`,
|
||||
height: `${tableRect.height}px`,
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'row') {
|
||||
Object.assign(this._preview.style, {
|
||||
display: 'block',
|
||||
width: `${tableRect.width}px`,
|
||||
height: `${cellRect.height}px`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private _initPreviewPosition(cell: HTMLElement, type: 'col' | 'row') {
|
||||
void computePosition(cell, this._preview, {
|
||||
placement: type === 'row' ? 'right' : 'bottom',
|
||||
middleware: [
|
||||
offset(({ rects }) => {
|
||||
if (type === 'col') {
|
||||
return -rects.reference.height
|
||||
}
|
||||
return -rects.reference.width
|
||||
}),
|
||||
],
|
||||
}).then(({ x, y }) => {
|
||||
Object.assign(this._preview.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private _updatePreviewPosition(x: number, y: number, cell: HTMLElement, type: 'col' | 'row') {
|
||||
computePosition(
|
||||
getVirtualElement(cell, x, y),
|
||||
this._preview,
|
||||
{ placement: type === 'row' ? 'right' : 'bottom' },
|
||||
).then(({ x, y }) => {
|
||||
if (type === 'row') {
|
||||
Object.assign(this._preview.style, {
|
||||
top: `${y}px`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'col') {
|
||||
Object.assign(this._preview.style, {
|
||||
left: `${x}px`,
|
||||
})
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getVirtualElement(cell: HTMLElement, x: number, y: number): ReferenceElement {
|
||||
return {
|
||||
contextElement: cell,
|
||||
getBoundingClientRect: () => {
|
||||
const rect = cell.getBoundingClientRect()
|
||||
return {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
right: x + rect.width / 2,
|
||||
bottom: y + rect.height / 2,
|
||||
top: y - rect.height / 2,
|
||||
left: x - rect.width / 2,
|
||||
x: x - rect.width / 2,
|
||||
y: y - rect.height / 2,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
export function clearPreviewDOM(previewRoot: HTMLElement): void {
|
||||
while (previewRoot.firstChild) {
|
||||
previewRoot.removeChild(previewRoot.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
export function createPreviewDOM(
|
||||
table: HTMLTableElement,
|
||||
previewRoot: HTMLElement,
|
||||
index: number,
|
||||
direction: 'row' | 'col',
|
||||
): void {
|
||||
clearPreviewDOM(previewRoot)
|
||||
|
||||
const previewTable = document.createElement('table')
|
||||
const previewTableBody = document.createElement('tbody')
|
||||
previewTable.appendChild(previewTableBody)
|
||||
previewRoot.appendChild(previewTable)
|
||||
|
||||
const rows = table.querySelectorAll('tr')
|
||||
|
||||
if (direction === 'row') {
|
||||
const row = rows[index]
|
||||
const rowDOM = row.cloneNode(true)
|
||||
previewTableBody.appendChild(rowDOM)
|
||||
} else {
|
||||
rows.forEach((row) => {
|
||||
const rowDOM = row.cloneNode(false)
|
||||
const cells = row.querySelectorAll('th,td')
|
||||
if (cells[index]) {
|
||||
const cellDOM = cells[index].cloneNode(true)
|
||||
rowDOM.appendChild(cellDOM)
|
||||
previewTableBody.appendChild(rowDOM)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
107
packages/editor-ext/src/lib/table/dnd/utils.ts
Normal file
107
packages/editor-ext/src/lib/table/dnd/utils.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { cellAround, TableMap } from "@tiptap/pm/tables"
|
||||
import { EditorView } from "@tiptap/pm/view"
|
||||
|
||||
export function getHoveringCell(
|
||||
view: EditorView,
|
||||
event: MouseEvent,
|
||||
): HoveringCellInfo | undefined {
|
||||
const domCell = domCellAround(event.target as HTMLElement | null)
|
||||
if (!domCell) return
|
||||
|
||||
const { left, top, width, height } = domCell.getBoundingClientRect()
|
||||
const eventPos = view.posAtCoords({
|
||||
// Use the center coordinates of the cell to ensure we're within the
|
||||
// selected cell. This prevents potential issues when the mouse is on the
|
||||
// border of two cells.
|
||||
left: left + width / 2,
|
||||
top: top + height / 2,
|
||||
})
|
||||
if (!eventPos) return
|
||||
|
||||
const $cellPos = cellAround(view.state.doc.resolve(eventPos.pos))
|
||||
if (!$cellPos) return
|
||||
|
||||
const map = TableMap.get($cellPos.node(-1))
|
||||
const tableStart = $cellPos.start(-1)
|
||||
const cellRect = map.findCell($cellPos.pos - tableStart)
|
||||
const rowIndex = cellRect.top
|
||||
const colIndex = cellRect.left
|
||||
|
||||
return {
|
||||
rowIndex,
|
||||
colIndex,
|
||||
cellPos: $cellPos.pos,
|
||||
rowFirstCellPos: getCellPos(map, tableStart, rowIndex, 0),
|
||||
colFirstCellPos: getCellPos(map, tableStart, 0, colIndex),
|
||||
}
|
||||
}
|
||||
|
||||
function domCellAround(target: HTMLElement | null): HTMLElement | null {
|
||||
while (target && target.nodeName != 'TD' && target.nodeName != 'TH') {
|
||||
target = target.classList?.contains('ProseMirror')
|
||||
? null
|
||||
: (target.parentNode as HTMLElement | null)
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
export interface HoveringCellInfo {
|
||||
rowIndex: number
|
||||
colIndex: number
|
||||
cellPos: number
|
||||
rowFirstCellPos: number
|
||||
colFirstCellPos: number
|
||||
}
|
||||
|
||||
function getCellPos(
|
||||
map: TableMap,
|
||||
tableStart: number,
|
||||
rowIndex: number,
|
||||
colIndex: number,
|
||||
) {
|
||||
const cellIndex = getCellIndex(map, rowIndex, colIndex)
|
||||
const posInTable = map.map[cellIndex]
|
||||
return tableStart + posInTable
|
||||
}
|
||||
|
||||
function getCellIndex(
|
||||
map: TableMap,
|
||||
rowIndex: number,
|
||||
colIndex: number,
|
||||
): number {
|
||||
return map.width * rowIndex + colIndex
|
||||
}
|
||||
|
||||
function getTableDOMByPos(view: EditorView, pos: number): HTMLTableElement | undefined {
|
||||
const dom = view.domAtPos(pos).node
|
||||
if (!dom) return
|
||||
const element = dom instanceof HTMLElement ? dom : dom.parentElement
|
||||
const table = element?.closest('table')
|
||||
return table ?? undefined
|
||||
}
|
||||
|
||||
function getTargetFirstCellDOM(table: HTMLTableElement, index: number, direction: 'row' | 'col'): HTMLTableCellElement | undefined {
|
||||
if (direction === 'row') {
|
||||
const row = table.querySelectorAll('tr')[index]
|
||||
const cell = row?.querySelector<HTMLTableCellElement>('th,td')
|
||||
return cell ?? undefined
|
||||
} else {
|
||||
const row = table.querySelector('tr')
|
||||
const cell = row?.querySelectorAll<HTMLTableCellElement>('th,td')[index]
|
||||
return cell ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
export type DraggingDOMs = {
|
||||
table: HTMLTableElement
|
||||
cell: HTMLTableCellElement
|
||||
}
|
||||
|
||||
export function getDndRelatedDOMs(view: EditorView, cellPos: number | undefined, draggingIndex: number, direction: 'row' | 'col'): DraggingDOMs | undefined {
|
||||
if (cellPos == null) return
|
||||
const table = getTableDOMByPos(view, cellPos)
|
||||
if (!table) return
|
||||
const cell = getTargetFirstCellDOM(table, draggingIndex, direction)
|
||||
if (!cell) return
|
||||
return { table, cell }
|
||||
}
|
||||
@ -2,3 +2,4 @@ export * from "./row";
|
||||
export * from "./cell";
|
||||
export * from "./header";
|
||||
export * from "./table";
|
||||
export * from "./dnd";
|
||||
@ -0,0 +1,44 @@
|
||||
import type { Node } from '@tiptap/pm/model'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
|
||||
/**
|
||||
* Convert an array of rows to a table node.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function convertArrayOfRowsToTableNode(
|
||||
tableNode: Node,
|
||||
arrayOfNodes: (Node | null)[][],
|
||||
): Node {
|
||||
const rowsPM = []
|
||||
const map = TableMap.get(tableNode)
|
||||
for (let rowIndex = 0; rowIndex < map.height; rowIndex++) {
|
||||
const row = tableNode.child(rowIndex)
|
||||
const rowCells = []
|
||||
|
||||
for (let colIndex = 0; colIndex < map.width; colIndex++) {
|
||||
if (!arrayOfNodes[rowIndex][colIndex]) continue
|
||||
|
||||
const cellPos = map.map[rowIndex * map.width + colIndex]
|
||||
|
||||
const cell = arrayOfNodes[rowIndex][colIndex]!
|
||||
const oldCell = tableNode.nodeAt(cellPos)!
|
||||
const newCell = oldCell.type.createChecked(
|
||||
Object.assign({}, cell.attrs),
|
||||
cell.content,
|
||||
cell.marks,
|
||||
)
|
||||
rowCells.push(newCell)
|
||||
}
|
||||
|
||||
rowsPM.push(row.type.createChecked(row.attrs, rowCells, row.marks))
|
||||
}
|
||||
|
||||
const newTable = tableNode.type.createChecked(
|
||||
tableNode.attrs,
|
||||
rowsPM,
|
||||
tableNode.marks,
|
||||
)
|
||||
|
||||
return newTable
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
import type { Node } from '@tiptap/pm/model'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
|
||||
/**
|
||||
* This function will transform the table node into a matrix of rows and columns
|
||||
* respecting merged cells, for example this table:
|
||||
*
|
||||
* ```
|
||||
* ┌──────┬──────┬─────────────┐
|
||||
* │ A1 │ B1 │ C1 │
|
||||
* ├──────┼──────┴──────┬──────┤
|
||||
* │ A2 │ B2 │ │
|
||||
* ├──────┼─────────────┤ D1 │
|
||||
* │ A3 │ B3 │ C3 │ │
|
||||
* └──────┴──────┴──────┴──────┘
|
||||
* ```
|
||||
*
|
||||
* will be converted to the below:
|
||||
*
|
||||
* ```javascript
|
||||
* [
|
||||
* [A1, B1, C1, null],
|
||||
* [A2, B2, null, D1],
|
||||
* [A3, B3, C3, null],
|
||||
* ]
|
||||
* ```
|
||||
* @internal
|
||||
*/
|
||||
export function convertTableNodeToArrayOfRows(tableNode: Node): (Node | null)[][] {
|
||||
const map = TableMap.get(tableNode)
|
||||
const rows: (Node | null)[][] = []
|
||||
const rowCount = map.height
|
||||
const colCount = map.width
|
||||
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
||||
const row: (Node | null)[] = []
|
||||
for (let colIndex = 0; colIndex < colCount; colIndex++) {
|
||||
let cellIndex = rowIndex * colCount + colIndex
|
||||
let cellPos = map.map[cellIndex]
|
||||
if (rowIndex > 0) {
|
||||
const topCellIndex = cellIndex - colCount
|
||||
const topCellPos = map.map[topCellIndex]
|
||||
if (cellPos === topCellPos) {
|
||||
row.push(null)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (colIndex > 0) {
|
||||
const leftCellIndex = cellIndex - 1
|
||||
const leftCellPos = map.map[leftCellIndex]
|
||||
if (cellPos === leftCellPos) {
|
||||
row.push(null)
|
||||
continue
|
||||
}
|
||||
}
|
||||
row.push(tableNode.nodeAt(cellPos))
|
||||
}
|
||||
rows.push(row)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import type { Selection } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
|
||||
import { findTable } from './query'
|
||||
import type { CellPos } from './types'
|
||||
|
||||
/**
|
||||
* Returns an array of cells in a column(s), where `columnIndex` could be a column index or an array of column indexes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function getCellsInColumn(columnIndexes: number | number[], selection: Selection): CellPos[] | undefined {
|
||||
const table = findTable(selection.$from)
|
||||
if (!table) {
|
||||
return
|
||||
}
|
||||
|
||||
const map = TableMap.get(table.node)
|
||||
const indexes = Array.isArray(columnIndexes) ? columnIndexes : [columnIndexes]
|
||||
|
||||
return indexes
|
||||
.filter((index) => index >= 0 && index <= map.width - 1)
|
||||
.flatMap((index) => {
|
||||
const cells = map.cellsInRect({
|
||||
left: index,
|
||||
right: index + 1,
|
||||
top: 0,
|
||||
bottom: map.height,
|
||||
})
|
||||
return cells.map((nodePos) => {
|
||||
const node = table.node.nodeAt(nodePos)!
|
||||
const pos = nodePos + table.start
|
||||
return { pos, start: pos + 1, node, depth: table.depth + 2 }
|
||||
})
|
||||
})
|
||||
}
|
||||
36
packages/editor-ext/src/lib/table/utils/get-cells-in-row.ts
Normal file
36
packages/editor-ext/src/lib/table/utils/get-cells-in-row.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { Selection } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
|
||||
import { findTable } from './query'
|
||||
import type { CellPos } from './types'
|
||||
|
||||
/**
|
||||
* Returns an array of cells in a row(s), where `rowIndex` could be a row index or an array of row indexes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function getCellsInRow(rowIndex: number | number[], selection: Selection): CellPos[] | undefined {
|
||||
const table = findTable(selection.$from)
|
||||
if (!table) {
|
||||
return
|
||||
}
|
||||
|
||||
const map = TableMap.get(table.node)
|
||||
const indexes = Array.isArray(rowIndex) ? rowIndex : [rowIndex]
|
||||
|
||||
return indexes
|
||||
.filter((index) => index >= 0 && index <= map.height - 1)
|
||||
.flatMap((index) => {
|
||||
const cells = map.cellsInRect({
|
||||
left: 0,
|
||||
right: map.width,
|
||||
top: index,
|
||||
bottom: index + 1,
|
||||
})
|
||||
return cells.map((nodePos) => {
|
||||
const node = table.node.nodeAt(nodePos)!
|
||||
const pos = nodePos + table.start
|
||||
return { pos, start: pos + 1, node, depth: table.depth + 2 }
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
|
||||
import { getCellsInColumn } from './get-cells-in-column'
|
||||
import { getCellsInRow } from './get-cells-in-row'
|
||||
import type { CellSelectionRange } from './types'
|
||||
|
||||
/**
|
||||
* Returns a range of rectangular selection spanning all merged cells around a
|
||||
* column at index `columnIndex`.
|
||||
*
|
||||
* Original implementation from Atlassian (Apache License 2.0)
|
||||
*
|
||||
* https://bitbucket.org/atlassian/atlassian-frontend-mirror/src/5f91cb871e8248bc3bae5ddc30bb9fd9200fadbb/editor/editor-tables/src/utils/get-selection-range-in-column.ts#editor/editor-tables/src/utils/get-selection-range-in-column.ts
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function getSelectionRangeInColumn(tr: Transaction, startColIndex: number, endColIndex: number = startColIndex): CellSelectionRange | undefined {
|
||||
let startIndex = startColIndex
|
||||
let endIndex = endColIndex
|
||||
|
||||
// looking for selection start column (startIndex)
|
||||
for (let i = startColIndex; i >= 0; i--) {
|
||||
const cells = getCellsInColumn(i, tr.selection)
|
||||
if (cells) {
|
||||
cells.forEach((cell) => {
|
||||
const maybeEndIndex = cell.node.attrs.colspan + i - 1
|
||||
if (maybeEndIndex >= startIndex) {
|
||||
startIndex = i
|
||||
}
|
||||
if (maybeEndIndex > endIndex) {
|
||||
endIndex = maybeEndIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// looking for selection end column (endIndex)
|
||||
for (let i = startColIndex; i <= endIndex; i++) {
|
||||
const cells = getCellsInColumn(i, tr.selection)
|
||||
if (cells) {
|
||||
cells.forEach((cell) => {
|
||||
const maybeEndIndex = cell.node.attrs.colspan + i - 1
|
||||
if (cell.node.attrs.colspan > 1 && maybeEndIndex > endIndex) {
|
||||
endIndex = maybeEndIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// filter out columns without cells (where all rows have colspan > 1 in the same column)
|
||||
const indexes = []
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const maybeCells = getCellsInColumn(i, tr.selection)
|
||||
if (maybeCells && maybeCells.length > 0) {
|
||||
indexes.push(i)
|
||||
}
|
||||
}
|
||||
startIndex = indexes[0]
|
||||
endIndex = indexes[indexes.length - 1]
|
||||
|
||||
const firstSelectedColumnCells = getCellsInColumn(startIndex, tr.selection)
|
||||
const firstRowCells = getCellsInRow(0, tr.selection)
|
||||
if (!firstSelectedColumnCells || !firstRowCells) {
|
||||
return
|
||||
}
|
||||
|
||||
const $anchor = tr.doc.resolve(
|
||||
firstSelectedColumnCells[firstSelectedColumnCells.length - 1].pos,
|
||||
)
|
||||
|
||||
let headCell
|
||||
for (let i = endIndex; i >= startIndex; i--) {
|
||||
const columnCells = getCellsInColumn(i, tr.selection)
|
||||
if (columnCells && columnCells.length > 0) {
|
||||
for (let j = firstRowCells.length - 1; j >= 0; j--) {
|
||||
if (firstRowCells[j].pos === columnCells[0].pos) {
|
||||
headCell = columnCells[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (headCell) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!headCell) {
|
||||
return
|
||||
}
|
||||
|
||||
const $head = tr.doc.resolve(headCell.pos)
|
||||
return { $anchor, $head, indexes }
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
|
||||
import { getCellsInColumn } from './get-cells-in-column'
|
||||
import { getCellsInRow } from './get-cells-in-row'
|
||||
import type { CellSelectionRange } from './types'
|
||||
|
||||
/**
|
||||
* Returns a range of rectangular selection spanning all merged cells around a
|
||||
* row at index `rowIndex`.
|
||||
*
|
||||
* Original implementation from Atlassian (Apache License 2.0)
|
||||
*
|
||||
* https://bitbucket.org/atlassian/atlassian-frontend-mirror/src/5f91cb871e8248bc3bae5ddc30bb9fd9200fadbb/editor/editor-tables/src/utils/get-selection-range-in-row.ts#editor/editor-tables/src/utils/get-selection-range-in-row.ts
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function getSelectionRangeInRow(tr: Transaction, startRowIndex: number, endRowIndex: number = startRowIndex): CellSelectionRange | undefined {
|
||||
let startIndex = startRowIndex
|
||||
let endIndex = endRowIndex
|
||||
|
||||
// looking for selection start row (startIndex)
|
||||
for (let i = startRowIndex; i >= 0; i--) {
|
||||
const cells = getCellsInRow(i, tr.selection)
|
||||
if (cells) {
|
||||
cells.forEach((cell) => {
|
||||
const maybeEndIndex = cell.node.attrs.rowspan + i - 1
|
||||
if (maybeEndIndex >= startIndex) {
|
||||
startIndex = i
|
||||
}
|
||||
if (maybeEndIndex > endIndex) {
|
||||
endIndex = maybeEndIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// looking for selection end row (endIndex)
|
||||
for (let i = startRowIndex; i <= endIndex; i++) {
|
||||
const cells = getCellsInRow(i, tr.selection)
|
||||
if (cells) {
|
||||
cells.forEach((cell) => {
|
||||
const maybeEndIndex = cell.node.attrs.rowspan + i - 1
|
||||
if (cell.node.attrs.rowspan > 1 && maybeEndIndex > endIndex) {
|
||||
endIndex = maybeEndIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// filter out rows without cells (where all columns have rowspan > 1 in the same row)
|
||||
const indexes = []
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const maybeCells = getCellsInRow(i, tr.selection)
|
||||
if (maybeCells && maybeCells.length > 0) {
|
||||
indexes.push(i)
|
||||
}
|
||||
}
|
||||
startIndex = indexes[0]
|
||||
endIndex = indexes[indexes.length - 1]
|
||||
|
||||
const firstSelectedRowCells = getCellsInRow(startIndex, tr.selection)
|
||||
const firstColumnCells = getCellsInColumn(0, tr.selection)
|
||||
if (!firstSelectedRowCells || !firstColumnCells) {
|
||||
return
|
||||
}
|
||||
|
||||
const $anchor = tr.doc.resolve(firstSelectedRowCells[firstSelectedRowCells.length - 1].pos)
|
||||
|
||||
let headCell
|
||||
for (let i = endIndex; i >= startIndex; i--) {
|
||||
const rowCells = getCellsInRow(i, tr.selection)
|
||||
if (rowCells && rowCells.length > 0) {
|
||||
for (let j = firstColumnCells.length - 1; j >= 0; j--) {
|
||||
if (firstColumnCells[j].pos === rowCells[0].pos) {
|
||||
headCell = rowCells[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (headCell) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!headCell) {
|
||||
return
|
||||
}
|
||||
|
||||
const $head = tr.doc.resolve(headCell.pos)
|
||||
return { $anchor, $head, indexes }
|
||||
}
|
||||
3
packages/editor-ext/src/lib/table/utils/index.ts
Normal file
3
packages/editor-ext/src/lib/table/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './move-column'
|
||||
export * from './move-row'
|
||||
export * from './query'
|
||||
81
packages/editor-ext/src/lib/table/utils/move-column.ts
Normal file
81
packages/editor-ext/src/lib/table/utils/move-column.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { Node } from '@tiptap/pm/model'
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
import {
|
||||
CellSelection,
|
||||
TableMap,
|
||||
} from '@tiptap/pm/tables'
|
||||
|
||||
import { convertArrayOfRowsToTableNode } from './convert-array-of-rows-to-table-node'
|
||||
import { convertTableNodeToArrayOfRows } from './convert-table-node-to-array-of-rows'
|
||||
import { getSelectionRangeInColumn } from './get-selection-range-in-column'
|
||||
import { moveRowInArrayOfRows } from './move-row-in-array-of-rows'
|
||||
import { findTable } from './query'
|
||||
import { transpose } from './transpose'
|
||||
|
||||
export interface MoveColumnParams {
|
||||
tr: Transaction
|
||||
originIndex: number
|
||||
targetIndex: number
|
||||
select: boolean
|
||||
pos: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a column from index `origin` to index `target`.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function moveColumn(moveColParams: MoveColumnParams): boolean {
|
||||
const { tr, originIndex, targetIndex, select, pos } = moveColParams
|
||||
const $pos = tr.doc.resolve(pos)
|
||||
const table = findTable($pos)
|
||||
if (!table) return false
|
||||
|
||||
const indexesOriginColumn = getSelectionRangeInColumn(tr, originIndex)?.indexes
|
||||
const indexesTargetColumn = getSelectionRangeInColumn(tr, targetIndex)?.indexes
|
||||
|
||||
if (!indexesOriginColumn || !indexesTargetColumn) return false
|
||||
|
||||
if (indexesOriginColumn.includes(targetIndex)) return false
|
||||
|
||||
const newTable = moveTableColumn(
|
||||
table.node,
|
||||
indexesOriginColumn,
|
||||
indexesTargetColumn,
|
||||
0,
|
||||
)
|
||||
|
||||
tr.replaceWith(
|
||||
table.pos,
|
||||
table.pos + table.node.nodeSize,
|
||||
newTable,
|
||||
)
|
||||
|
||||
if (!select) return true
|
||||
|
||||
const map = TableMap.get(newTable)
|
||||
const start = table.start
|
||||
const index = targetIndex
|
||||
const lastCell = map.positionAt(map.height - 1, index, newTable)
|
||||
const $lastCell = tr.doc.resolve(start + lastCell)
|
||||
|
||||
const firstCell = map.positionAt(0, index, newTable)
|
||||
const $firstCell = tr.doc.resolve(start + firstCell)
|
||||
|
||||
tr.setSelection(CellSelection.colSelection($lastCell, $firstCell))
|
||||
return true
|
||||
}
|
||||
|
||||
function moveTableColumn(
|
||||
table: Node,
|
||||
indexesOrigin: number[],
|
||||
indexesTarget: number[],
|
||||
direction: -1 | 1 | 0,
|
||||
) {
|
||||
let rows = transpose(convertTableNodeToArrayOfRows(table))
|
||||
|
||||
rows = moveRowInArrayOfRows(rows, indexesOrigin, indexesTarget, direction)
|
||||
rows = transpose(rows)
|
||||
|
||||
return convertArrayOfRowsToTableNode(table, rows)
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Move a row in an array of rows.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function moveRowInArrayOfRows<T>(
|
||||
rows: T[],
|
||||
indexesOrigin: number[],
|
||||
indexesTarget: number[],
|
||||
directionOverride: -1 | 1 | 0,
|
||||
): T[] {
|
||||
const direction = indexesOrigin[0] > indexesTarget[0] ? -1 : 1
|
||||
|
||||
const rowsExtracted = rows.splice(indexesOrigin[0], indexesOrigin.length)
|
||||
const positionOffset = rowsExtracted.length % 2 === 0 ? 1 : 0
|
||||
let target: number
|
||||
|
||||
if (directionOverride === -1 && direction === 1) {
|
||||
target = indexesTarget[0] - 1
|
||||
} else if (directionOverride === 1 && direction === -1) {
|
||||
target = indexesTarget[indexesTarget.length - 1] - positionOffset + 1
|
||||
} else {
|
||||
target = direction === -1
|
||||
? indexesTarget[0]
|
||||
: indexesTarget[indexesTarget.length - 1] - positionOffset
|
||||
}
|
||||
|
||||
rows.splice(target, 0, ...rowsExtracted)
|
||||
return rows
|
||||
}
|
||||
74
packages/editor-ext/src/lib/table/utils/move-row.ts
Normal file
74
packages/editor-ext/src/lib/table/utils/move-row.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import type { Node } from '@tiptap/pm/model'
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
import {
|
||||
CellSelection,
|
||||
TableMap,
|
||||
} from '@tiptap/pm/tables'
|
||||
|
||||
import { convertArrayOfRowsToTableNode } from './convert-array-of-rows-to-table-node'
|
||||
import { convertTableNodeToArrayOfRows } from './convert-table-node-to-array-of-rows'
|
||||
import { getSelectionRangeInRow } from './get-selection-range-in-row'
|
||||
import { moveRowInArrayOfRows } from './move-row-in-array-of-rows'
|
||||
import { findTable } from './query'
|
||||
|
||||
export interface MoveRowParams {
|
||||
tr: Transaction
|
||||
originIndex: number
|
||||
targetIndex: number
|
||||
select: boolean
|
||||
pos: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a row from index `origin` to index `target`.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function moveRow(moveRowParams: MoveRowParams): boolean {
|
||||
const { tr, originIndex, targetIndex, select, pos } = moveRowParams
|
||||
const $pos = tr.doc.resolve(pos)
|
||||
const table = findTable($pos)
|
||||
if (!table) return false
|
||||
|
||||
const indexesOriginRow = getSelectionRangeInRow(tr, originIndex)?.indexes
|
||||
const indexesTargetRow = getSelectionRangeInRow(tr, targetIndex)?.indexes
|
||||
|
||||
if (!indexesOriginRow || !indexesTargetRow) return false
|
||||
|
||||
if (indexesOriginRow.includes(targetIndex)) return false
|
||||
|
||||
const newTable = moveTableRow(table.node, indexesOriginRow, indexesTargetRow, 0)
|
||||
|
||||
tr.replaceWith(
|
||||
table.pos,
|
||||
table.pos + table.node.nodeSize,
|
||||
newTable,
|
||||
)
|
||||
|
||||
if (!select) return true
|
||||
|
||||
const map = TableMap.get(newTable)
|
||||
const start = table.start
|
||||
const index = targetIndex
|
||||
const lastCell = map.positionAt(index, map.width - 1, newTable)
|
||||
const $lastCell = tr.doc.resolve(start + lastCell)
|
||||
|
||||
const firstCell = map.positionAt(index, 0, newTable)
|
||||
const $firstCell = tr.doc.resolve(start + firstCell)
|
||||
|
||||
tr.setSelection(CellSelection.rowSelection($lastCell, $firstCell))
|
||||
return true
|
||||
}
|
||||
|
||||
function moveTableRow(
|
||||
table: Node,
|
||||
indexesOrigin: number[],
|
||||
indexesTarget: number[],
|
||||
direction: -1 | 1 | 0,
|
||||
) {
|
||||
let rows = convertTableNodeToArrayOfRows(table)
|
||||
|
||||
rows = moveRowInArrayOfRows(rows, indexesOrigin, indexesTarget, direction)
|
||||
|
||||
return convertArrayOfRowsToTableNode(table, rows)
|
||||
}
|
||||
122
packages/editor-ext/src/lib/table/utils/query.ts
Normal file
122
packages/editor-ext/src/lib/table/utils/query.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import type {
|
||||
Node,
|
||||
ResolvedPos,
|
||||
} from '@tiptap/pm/model'
|
||||
import type { Selection } from '@tiptap/pm/state'
|
||||
import {
|
||||
cellAround,
|
||||
cellNear,
|
||||
CellSelection,
|
||||
inSameTable,
|
||||
} from '@tiptap/pm/tables'
|
||||
|
||||
/**
|
||||
* Checks if the given object is a `CellSelection` instance.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function isCellSelection(value: unknown): value is CellSelection {
|
||||
return value instanceof CellSelection
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest table node.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findTable($pos: ResolvedPos): FindParentNodeResult | undefined {
|
||||
return findParentNode((node) => node.type.spec.tableRole === 'table', $pos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find the anchor and head cell in the same table by using the given
|
||||
* anchor and head as hit points, or fallback to the selection's anchor and
|
||||
* head.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findCellRange(
|
||||
selection: Selection,
|
||||
anchorHit?: number,
|
||||
headHit?: number,
|
||||
): [ResolvedPos, ResolvedPos] | undefined {
|
||||
if (anchorHit == null && headHit == null && isCellSelection(selection)) {
|
||||
return [selection.$anchorCell, selection.$headCell]
|
||||
}
|
||||
|
||||
const anchor: number = anchorHit ?? headHit ?? selection.anchor
|
||||
const head: number = headHit ?? anchorHit ?? selection.head
|
||||
|
||||
const doc = selection.$head.doc
|
||||
|
||||
const $anchorCell = findCellPos(doc, anchor)
|
||||
const $headCell = findCellPos(doc, head)
|
||||
|
||||
if ($anchorCell && $headCell && inSameTable($anchorCell, $headCell)) {
|
||||
return [$anchorCell, $headCell]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find a resolved pos of a cell by using the given pos as a hit point.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findCellPos(
|
||||
doc: Node,
|
||||
pos: number,
|
||||
): ResolvedPos | undefined {
|
||||
const $pos = doc.resolve(pos)
|
||||
return cellAround($pos) || cellNear($pos)
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface FindParentNodeResult {
|
||||
/**
|
||||
* The closest parent node that satisfies the predicate.
|
||||
*/
|
||||
node: Node
|
||||
|
||||
/**
|
||||
* The position directly before the node.
|
||||
*/
|
||||
pos: number
|
||||
|
||||
/**
|
||||
* The position at the start of the node.
|
||||
*/
|
||||
start: number
|
||||
|
||||
/**
|
||||
* The depth of the node.
|
||||
*/
|
||||
depth: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest parent node that satisfies the predicate.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findParentNode(
|
||||
/**
|
||||
* The predicate to test the parent node.
|
||||
*/
|
||||
predicate: (node: Node) => boolean,
|
||||
/**
|
||||
* The position to start searching from.
|
||||
*/
|
||||
$pos: ResolvedPos,
|
||||
): FindParentNodeResult | undefined {
|
||||
for (let depth = $pos.depth; depth >= 0; depth -= 1) {
|
||||
const node = $pos.node(depth)
|
||||
|
||||
if (predicate(node)) {
|
||||
const pos = depth === 0 ? 0 : $pos.before(depth)
|
||||
const start = $pos.start(depth)
|
||||
return { node, pos, start, depth }
|
||||
}
|
||||
}
|
||||
}
|
||||
29
packages/editor-ext/src/lib/table/utils/transpose.ts
Normal file
29
packages/editor-ext/src/lib/table/utils/transpose.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Transposes a 2D array by flipping columns to rows.
|
||||
*
|
||||
* Transposition is a familiar algebra concept where the matrix is flipped
|
||||
* along its diagonal. For more details, see:
|
||||
* https://en.wikipedia.org/wiki/Transpose
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* const arr = [
|
||||
* ['a1', 'a2', 'a3'],
|
||||
* ['b1', 'b2', 'b3'],
|
||||
* ['c1', 'c2', 'c3'],
|
||||
* ['d1', 'd2', 'd3'],
|
||||
* ];
|
||||
*
|
||||
* const result = transpose(arr);
|
||||
* result === [
|
||||
* ['a1', 'b1', 'c1', 'd1'],
|
||||
* ['a2', 'b2', 'c2', 'd2'],
|
||||
* ['a3', 'b3', 'c3', 'd3'],
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export function transpose<T>(array: T[][]): T[][] {
|
||||
return array[0].map((_, i) => {
|
||||
return array.map((column) => column[i])
|
||||
})
|
||||
}
|
||||
18
packages/editor-ext/src/lib/table/utils/types.ts
Normal file
18
packages/editor-ext/src/lib/table/utils/types.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type {
|
||||
Node,
|
||||
ResolvedPos,
|
||||
} from '@tiptap/pm/model'
|
||||
|
||||
export type CellPos = {
|
||||
pos: number
|
||||
start: number
|
||||
depth: number
|
||||
node: Node
|
||||
}
|
||||
|
||||
export type CellSelectionRange = {
|
||||
$anchor: ResolvedPos
|
||||
$head: ResolvedPos
|
||||
// an array of column/row indexes
|
||||
indexes: number[]
|
||||
}
|
||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@ -22,6 +22,9 @@ importers:
|
||||
'@docmost/editor-ext':
|
||||
specifier: workspace:*
|
||||
version: link:packages/editor-ext
|
||||
'@floating-ui/dom':
|
||||
specifier: ^1.7.3
|
||||
version: 1.7.3
|
||||
'@hocuspocus/extension-redis':
|
||||
specifier: ^2.15.2
|
||||
version: 2.15.2(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
|
||||
@ -2189,9 +2192,15 @@ packages:
|
||||
'@floating-ui/core@1.5.3':
|
||||
resolution: {integrity: sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==}
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
||||
|
||||
'@floating-ui/dom@1.6.3':
|
||||
resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==}
|
||||
|
||||
'@floating-ui/dom@1.7.3':
|
||||
resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==}
|
||||
|
||||
'@floating-ui/react-dom@2.1.2':
|
||||
resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
|
||||
peerDependencies:
|
||||
@ -2204,6 +2213,9 @@ packages:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@floating-ui/utils@0.2.8':
|
||||
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
|
||||
|
||||
@ -11848,11 +11860,20 @@ snapshots:
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.8
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.6.3':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.5.3
|
||||
'@floating-ui/utils': 0.2.8
|
||||
|
||||
'@floating-ui/dom@1.7.3':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.3
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.3
|
||||
@ -11867,6 +11888,8 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
tabbable: 6.2.0
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@floating-ui/utils@0.2.8': {}
|
||||
|
||||
'@hocuspocus/common@2.15.2':
|
||||
|
||||
Reference in New Issue
Block a user