Skip to content

Commit 373e640

Browse files
authored
[WIKI-740] refactor: editor table performance (#8411)
1 parent 21df102 commit 373e640

File tree

4 files changed

+134
-4
lines changed

4 files changed

+134
-4
lines changed

packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from "@floating-ui/react";
1313
import type { Editor } from "@tiptap/core";
1414
import { Ellipsis } from "lucide-react";
15-
import { useCallback, useEffect, useState } from "react";
15+
import { useCallback, useEffect, useRef, useState } from "react";
1616
// plane imports
1717
import { cn } from "@plane/utils";
1818
// constants
@@ -49,6 +49,25 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
4949
const { col, editor } = props;
5050
// states
5151
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
52+
// Track active event listeners for cleanup
53+
const activeListenersRef = useRef<{
54+
mouseup?: (e: MouseEvent) => void;
55+
mousemove?: (e: MouseEvent) => void;
56+
}>({});
57+
58+
// Cleanup window event listeners on unmount
59+
useEffect(() => {
60+
const listenersRef = activeListenersRef.current;
61+
return () => {
62+
// Remove any lingering window event listeners when component unmounts
63+
if (listenersRef.mouseup) {
64+
window.removeEventListener("mouseup", listenersRef.mouseup);
65+
}
66+
if (listenersRef.mousemove) {
67+
window.removeEventListener("mousemove", listenersRef.mousemove);
68+
}
69+
};
70+
}, []);
5271
// floating ui
5372
const { refs, floatingStyles, context } = useFloating({
5473
placement: "bottom-start",
@@ -94,6 +113,17 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
94113
e.stopPropagation();
95114
e.preventDefault();
96115

116+
// Prevent multiple simultaneous drag operations
117+
// If there are already listeners attached, remove them first
118+
if (activeListenersRef.current.mouseup) {
119+
window.removeEventListener("mouseup", activeListenersRef.current.mouseup);
120+
}
121+
if (activeListenersRef.current.mousemove) {
122+
window.removeEventListener("mousemove", activeListenersRef.current.mousemove);
123+
}
124+
activeListenersRef.current.mouseup = undefined;
125+
activeListenersRef.current.mousemove = undefined;
126+
97127
const table = findTable(editor.state.selection);
98128
if (!table) return;
99129

@@ -133,6 +163,9 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
133163
}
134164
window.removeEventListener("mouseup", handleFinish);
135165
window.removeEventListener("mousemove", handleMove);
166+
// Clear the ref
167+
activeListenersRef.current.mouseup = undefined;
168+
activeListenersRef.current.mousemove = undefined;
136169
};
137170

138171
let pseudoColumn: HTMLElement | undefined;
@@ -169,6 +202,9 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
169202
};
170203

171204
try {
205+
// Store references for cleanup
206+
activeListenersRef.current.mouseup = handleFinish;
207+
activeListenersRef.current.mousemove = handleMove;
172208
window.addEventListener("mouseup", handleFinish);
173209
window.addEventListener("mousemove", handleMove);
174210
} catch (error) {

packages/editor/src/core/extensions/table/plugins/drag-handles/column/plugin.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ type TableColumnDragHandlePluginState = {
1818
// track table structure to detect changes
1919
tableWidth?: number;
2020
tableNodePos?: number;
21+
// track renderers for cleanup
22+
renderers?: ReactRenderer[];
2123
};
2224

2325
const TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY = new PluginKey("tableColumnHandlerDecorationPlugin");
@@ -58,11 +60,22 @@ export const TableColumnDragHandlePlugin = (editor: Editor): Plugin<TableColumnD
5860
decorations: mapped,
5961
tableWidth: tableMap.width,
6062
tableNodePos: table.pos,
63+
renderers: prev.renderers,
6164
};
6265
}
6366

67+
// Clean up old renderers before creating new ones
68+
prev.renderers?.forEach((renderer) => {
69+
try {
70+
renderer.destroy();
71+
} catch (error) {
72+
console.error("Error destroying renderer:", error);
73+
}
74+
});
75+
6476
// recreate all decorations
6577
const decorations: Decoration[] = [];
78+
const renderers: ReactRenderer[] = [];
6679

6780
for (let col = 0; col < tableMap.width; col++) {
6881
const pos = getTableCellWidgetDecorationPos(table, tableMap, col);
@@ -75,19 +88,35 @@ export const TableColumnDragHandlePlugin = (editor: Editor): Plugin<TableColumnD
7588
editor,
7689
});
7790

91+
renderers.push(dragHandleComponent);
7892
decorations.push(Decoration.widget(pos, () => dragHandleComponent.element));
7993
}
8094

8195
return {
8296
decorations: DecorationSet.create(newState.doc, decorations),
8397
tableWidth: tableMap.width,
8498
tableNodePos: table.pos,
99+
renderers,
85100
};
86101
},
87102
},
88103
props: {
89104
decorations(state) {
90-
return TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY.getState(state).decorations;
105+
return (TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY.getState(state) as TableColumnDragHandlePluginState | undefined)
106+
?.decorations;
91107
},
92108
},
109+
destroy() {
110+
// Clean up all renderers when plugin is destroyed
111+
const state =
112+
editor.state &&
113+
(TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY.getState(editor.state) as TableColumnDragHandlePluginState | undefined);
114+
state?.renderers?.forEach((renderer: ReactRenderer) => {
115+
try {
116+
renderer.destroy();
117+
} catch (error) {
118+
console.error("Error destroying renderer:", error);
119+
}
120+
});
121+
},
93122
});

packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from "@floating-ui/react";
1313
import type { Editor } from "@tiptap/core";
1414
import { Ellipsis } from "lucide-react";
15-
import { useCallback, useEffect, useState } from "react";
15+
import { useCallback, useEffect, useRef, useState } from "react";
1616
// plane imports
1717
import { cn } from "@plane/utils";
1818
// constants
@@ -49,6 +49,25 @@ export function RowDragHandle(props: RowDragHandleProps) {
4949
const { editor, row } = props;
5050
// states
5151
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
52+
// Track active event listeners for cleanup
53+
const activeListenersRef = useRef<{
54+
mouseup?: (e: MouseEvent) => void;
55+
mousemove?: (e: MouseEvent) => void;
56+
}>({});
57+
58+
// Cleanup window event listeners on unmount
59+
useEffect(() => {
60+
const listenersRef = activeListenersRef.current;
61+
return () => {
62+
// Remove any lingering window event listeners when component unmounts
63+
if (listenersRef.mouseup) {
64+
window.removeEventListener("mouseup", listenersRef.mouseup);
65+
}
66+
if (listenersRef.mousemove) {
67+
window.removeEventListener("mousemove", listenersRef.mousemove);
68+
}
69+
};
70+
}, []);
5271
// floating ui
5372
const { refs, floatingStyles, context } = useFloating({
5473
placement: "bottom-start",
@@ -94,6 +113,17 @@ export function RowDragHandle(props: RowDragHandleProps) {
94113
e.stopPropagation();
95114
e.preventDefault();
96115

116+
// Prevent multiple simultaneous drag operations
117+
// If there are already listeners attached, remove them first
118+
if (activeListenersRef.current.mouseup) {
119+
window.removeEventListener("mouseup", activeListenersRef.current.mouseup);
120+
}
121+
if (activeListenersRef.current.mousemove) {
122+
window.removeEventListener("mousemove", activeListenersRef.current.mousemove);
123+
}
124+
activeListenersRef.current.mouseup = undefined;
125+
activeListenersRef.current.mousemove = undefined;
126+
97127
const table = findTable(editor.state.selection);
98128
if (!table) return;
99129

@@ -133,6 +163,9 @@ export function RowDragHandle(props: RowDragHandleProps) {
133163
}
134164
window.removeEventListener("mouseup", handleFinish);
135165
window.removeEventListener("mousemove", handleMove);
166+
// Clear the ref
167+
activeListenersRef.current.mouseup = undefined;
168+
activeListenersRef.current.mousemove = undefined;
136169
};
137170

138171
let pseudoRow: HTMLElement | undefined;
@@ -168,6 +201,9 @@ export function RowDragHandle(props: RowDragHandleProps) {
168201
};
169202

170203
try {
204+
// Store references for cleanup
205+
activeListenersRef.current.mouseup = handleFinish;
206+
activeListenersRef.current.mousemove = handleMove;
171207
window.addEventListener("mouseup", handleFinish);
172208
window.addEventListener("mousemove", handleMove);
173209
} catch (error) {

packages/editor/src/core/extensions/table/plugins/drag-handles/row/plugin.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ type TableRowDragHandlePluginState = {
1818
// track table structure to detect changes
1919
tableHeight?: number;
2020
tableNodePos?: number;
21+
// track renderers for cleanup
22+
renderers?: ReactRenderer[];
2123
};
2224

2325
const TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY = new PluginKey("tableRowDragHandlePlugin");
@@ -58,11 +60,22 @@ export const TableRowDragHandlePlugin = (editor: Editor): Plugin<TableRowDragHan
5860
decorations: mapped,
5961
tableHeight: tableMap.height,
6062
tableNodePos: table.pos,
63+
renderers: prev.renderers,
6164
};
6265
}
6366

67+
// Clean up old renderers before creating new ones
68+
prev.renderers?.forEach((renderer) => {
69+
try {
70+
renderer.destroy();
71+
} catch (error) {
72+
console.error("Error destroying renderer:", error);
73+
}
74+
});
75+
6476
// recreate all decorations
6577
const decorations: Decoration[] = [];
78+
const renderers: ReactRenderer[] = [];
6679

6780
for (let row = 0; row < tableMap.height; row++) {
6881
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width);
@@ -75,19 +88,35 @@ export const TableRowDragHandlePlugin = (editor: Editor): Plugin<TableRowDragHan
7588
editor,
7689
});
7790

91+
renderers.push(dragHandleComponent);
7892
decorations.push(Decoration.widget(pos, () => dragHandleComponent.element));
7993
}
8094

8195
return {
8296
decorations: DecorationSet.create(newState.doc, decorations),
8397
tableHeight: tableMap.height,
8498
tableNodePos: table.pos,
99+
renderers,
85100
};
86101
},
87102
},
88103
props: {
89104
decorations(state) {
90-
return TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY.getState(state).decorations;
105+
return (TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY.getState(state) as TableRowDragHandlePluginState | undefined)
106+
?.decorations;
91107
},
92108
},
109+
destroy() {
110+
// Clean up all renderers when plugin is destroyed
111+
const state =
112+
editor.state &&
113+
(TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY.getState(editor.state) as TableRowDragHandlePluginState | undefined);
114+
state?.renderers?.forEach((renderer: ReactRenderer) => {
115+
try {
116+
renderer.destroy();
117+
} catch (error) {
118+
console.error("Error destroying renderer:", error);
119+
}
120+
});
121+
},
93122
});

0 commit comments

Comments
 (0)