[WIKI-740] refactor: editor table performance (#8411)

This commit is contained in:
Aaryan Khandelwal
2025-12-23 14:24:05 +05:30
committed by GitHub
parent 21df1028f8
commit 373e640a25
4 changed files with 134 additions and 4 deletions

View File

@@ -12,7 +12,7 @@ import {
} from "@floating-ui/react";
import type { Editor } from "@tiptap/core";
import { Ellipsis } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
// plane imports
import { cn } from "@plane/utils";
// constants
@@ -49,6 +49,25 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
const { col, editor } = props;
// states
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// Track active event listeners for cleanup
const activeListenersRef = useRef<{
mouseup?: (e: MouseEvent) => void;
mousemove?: (e: MouseEvent) => void;
}>({});
// Cleanup window event listeners on unmount
useEffect(() => {
const listenersRef = activeListenersRef.current;
return () => {
// Remove any lingering window event listeners when component unmounts
if (listenersRef.mouseup) {
window.removeEventListener("mouseup", listenersRef.mouseup);
}
if (listenersRef.mousemove) {
window.removeEventListener("mousemove", listenersRef.mousemove);
}
};
}, []);
// floating ui
const { refs, floatingStyles, context } = useFloating({
placement: "bottom-start",
@@ -94,6 +113,17 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
e.stopPropagation();
e.preventDefault();
// Prevent multiple simultaneous drag operations
// If there are already listeners attached, remove them first
if (activeListenersRef.current.mouseup) {
window.removeEventListener("mouseup", activeListenersRef.current.mouseup);
}
if (activeListenersRef.current.mousemove) {
window.removeEventListener("mousemove", activeListenersRef.current.mousemove);
}
activeListenersRef.current.mouseup = undefined;
activeListenersRef.current.mousemove = undefined;
const table = findTable(editor.state.selection);
if (!table) return;
@@ -133,6 +163,9 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
}
window.removeEventListener("mouseup", handleFinish);
window.removeEventListener("mousemove", handleMove);
// Clear the ref
activeListenersRef.current.mouseup = undefined;
activeListenersRef.current.mousemove = undefined;
};
let pseudoColumn: HTMLElement | undefined;
@@ -169,6 +202,9 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
};
try {
// Store references for cleanup
activeListenersRef.current.mouseup = handleFinish;
activeListenersRef.current.mousemove = handleMove;
window.addEventListener("mouseup", handleFinish);
window.addEventListener("mousemove", handleMove);
} catch (error) {

View File

@@ -18,6 +18,8 @@ type TableColumnDragHandlePluginState = {
// track table structure to detect changes
tableWidth?: number;
tableNodePos?: number;
// track renderers for cleanup
renderers?: ReactRenderer[];
};
const TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY = new PluginKey("tableColumnHandlerDecorationPlugin");
@@ -58,11 +60,22 @@ export const TableColumnDragHandlePlugin = (editor: Editor): Plugin<TableColumnD
decorations: mapped,
tableWidth: tableMap.width,
tableNodePos: table.pos,
renderers: prev.renderers,
};
}
// Clean up old renderers before creating new ones
prev.renderers?.forEach((renderer) => {
try {
renderer.destroy();
} catch (error) {
console.error("Error destroying renderer:", error);
}
});
// recreate all decorations
const decorations: Decoration[] = [];
const renderers: ReactRenderer[] = [];
for (let col = 0; col < tableMap.width; col++) {
const pos = getTableCellWidgetDecorationPos(table, tableMap, col);
@@ -75,6 +88,7 @@ export const TableColumnDragHandlePlugin = (editor: Editor): Plugin<TableColumnD
editor,
});
renderers.push(dragHandleComponent);
decorations.push(Decoration.widget(pos, () => dragHandleComponent.element));
}
@@ -82,12 +96,27 @@ export const TableColumnDragHandlePlugin = (editor: Editor): Plugin<TableColumnD
decorations: DecorationSet.create(newState.doc, decorations),
tableWidth: tableMap.width,
tableNodePos: table.pos,
renderers,
};
},
},
props: {
decorations(state) {
return TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY.getState(state).decorations;
return (TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY.getState(state) as TableColumnDragHandlePluginState | undefined)
?.decorations;
},
},
destroy() {
// Clean up all renderers when plugin is destroyed
const state =
editor.state &&
(TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY.getState(editor.state) as TableColumnDragHandlePluginState | undefined);
state?.renderers?.forEach((renderer: ReactRenderer) => {
try {
renderer.destroy();
} catch (error) {
console.error("Error destroying renderer:", error);
}
});
},
});

View File

@@ -12,7 +12,7 @@ import {
} from "@floating-ui/react";
import type { Editor } from "@tiptap/core";
import { Ellipsis } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
// plane imports
import { cn } from "@plane/utils";
// constants
@@ -49,6 +49,25 @@ export function RowDragHandle(props: RowDragHandleProps) {
const { editor, row } = props;
// states
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// Track active event listeners for cleanup
const activeListenersRef = useRef<{
mouseup?: (e: MouseEvent) => void;
mousemove?: (e: MouseEvent) => void;
}>({});
// Cleanup window event listeners on unmount
useEffect(() => {
const listenersRef = activeListenersRef.current;
return () => {
// Remove any lingering window event listeners when component unmounts
if (listenersRef.mouseup) {
window.removeEventListener("mouseup", listenersRef.mouseup);
}
if (listenersRef.mousemove) {
window.removeEventListener("mousemove", listenersRef.mousemove);
}
};
}, []);
// floating ui
const { refs, floatingStyles, context } = useFloating({
placement: "bottom-start",
@@ -94,6 +113,17 @@ export function RowDragHandle(props: RowDragHandleProps) {
e.stopPropagation();
e.preventDefault();
// Prevent multiple simultaneous drag operations
// If there are already listeners attached, remove them first
if (activeListenersRef.current.mouseup) {
window.removeEventListener("mouseup", activeListenersRef.current.mouseup);
}
if (activeListenersRef.current.mousemove) {
window.removeEventListener("mousemove", activeListenersRef.current.mousemove);
}
activeListenersRef.current.mouseup = undefined;
activeListenersRef.current.mousemove = undefined;
const table = findTable(editor.state.selection);
if (!table) return;
@@ -133,6 +163,9 @@ export function RowDragHandle(props: RowDragHandleProps) {
}
window.removeEventListener("mouseup", handleFinish);
window.removeEventListener("mousemove", handleMove);
// Clear the ref
activeListenersRef.current.mouseup = undefined;
activeListenersRef.current.mousemove = undefined;
};
let pseudoRow: HTMLElement | undefined;
@@ -168,6 +201,9 @@ export function RowDragHandle(props: RowDragHandleProps) {
};
try {
// Store references for cleanup
activeListenersRef.current.mouseup = handleFinish;
activeListenersRef.current.mousemove = handleMove;
window.addEventListener("mouseup", handleFinish);
window.addEventListener("mousemove", handleMove);
} catch (error) {

View File

@@ -18,6 +18,8 @@ type TableRowDragHandlePluginState = {
// track table structure to detect changes
tableHeight?: number;
tableNodePos?: number;
// track renderers for cleanup
renderers?: ReactRenderer[];
};
const TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY = new PluginKey("tableRowDragHandlePlugin");
@@ -58,11 +60,22 @@ export const TableRowDragHandlePlugin = (editor: Editor): Plugin<TableRowDragHan
decorations: mapped,
tableHeight: tableMap.height,
tableNodePos: table.pos,
renderers: prev.renderers,
};
}
// Clean up old renderers before creating new ones
prev.renderers?.forEach((renderer) => {
try {
renderer.destroy();
} catch (error) {
console.error("Error destroying renderer:", error);
}
});
// recreate all decorations
const decorations: Decoration[] = [];
const renderers: ReactRenderer[] = [];
for (let row = 0; row < tableMap.height; row++) {
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width);
@@ -75,6 +88,7 @@ export const TableRowDragHandlePlugin = (editor: Editor): Plugin<TableRowDragHan
editor,
});
renderers.push(dragHandleComponent);
decorations.push(Decoration.widget(pos, () => dragHandleComponent.element));
}
@@ -82,12 +96,27 @@ export const TableRowDragHandlePlugin = (editor: Editor): Plugin<TableRowDragHan
decorations: DecorationSet.create(newState.doc, decorations),
tableHeight: tableMap.height,
tableNodePos: table.pos,
renderers,
};
},
},
props: {
decorations(state) {
return TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY.getState(state).decorations;
return (TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY.getState(state) as TableRowDragHandlePluginState | undefined)
?.decorations;
},
},
destroy() {
// Clean up all renderers when plugin is destroyed
const state =
editor.state &&
(TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY.getState(editor.state) as TableRowDragHandlePluginState | undefined);
state?.renderers?.forEach((renderer: ReactRenderer) => {
try {
renderer.destroy();
} catch (error) {
console.error("Error destroying renderer:", error);
}
});
},
});