smartrte-react 0.3.5 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +106 -14
- package/dist/adapters/domChecklistCommandBridge.js +1 -1
- package/dist/adapters/domCommandBridge.d.ts +2 -2
- package/dist/adapters/domCommandBridge.js +2 -2
- package/dist/adapters/domInlineAtomCommandBridge.d.ts +10 -9
- package/dist/adapters/domInlineAtomCommandBridge.js +154 -50
- package/dist/adapters/domInlineImageCommandBridge.d.ts +7 -7
- package/dist/adapters/domInlineImageCommandBridge.js +6 -72
- package/dist/adapters/domMoveCommandBridge.js +1 -1
- package/dist/adapters/domSelectionBridge.d.ts +3 -3
- package/dist/adapters/domSmartDocument.d.ts +4 -4
- package/dist/adapters/domSmartDocument.js +2 -2
- package/dist/adapters/domTableCommandBridge.d.ts +42 -16
- package/dist/adapters/domTableCommandBridge.js +203 -173
- package/dist/adapters/internalFlags.d.ts +0 -8
- package/dist/adapters/internalFlags.js +0 -24
- package/dist/adapters/legacyListShadowComparator.d.ts +1 -1
- package/dist/adapters/legacyListShadowComparator.js +1 -1
- package/dist/adapters/pdfPrint.d.ts +8 -0
- package/dist/adapters/pdfPrint.js +22 -0
- package/dist/adapters/shadowMode.d.ts +3 -3
- package/dist/adapters/shadowMode.js +1 -1
- package/dist/canonicalEditorRuntime.d.ts +53 -1
- package/dist/canonicalEditorRuntime.js +61 -4
- package/dist/capabilityPresets.d.ts +27 -0
- package/dist/capabilityPresets.js +21 -0
- package/dist/collabTransport.d.ts +66 -0
- package/dist/collabTransport.js +1 -0
- package/dist/commentProvider.d.ts +24 -0
- package/dist/commentProvider.js +1 -0
- package/dist/components/CanonicalAuthorityEditor.d.ts +35 -1
- package/dist/components/CanonicalAuthorityEditor.js +1086 -112
- package/dist/components/ClassicEditorAuthority.d.ts +93 -22
- package/dist/components/ClassicEditorAuthority.js +11 -43
- package/dist/components/ColorPickerPopover.d.ts +39 -0
- package/dist/components/ColorPickerPopover.js +285 -0
- package/dist/components/CommentMarkers.d.ts +25 -0
- package/dist/components/CommentMarkers.js +105 -0
- package/dist/components/CommentThreadPanel.d.ts +26 -0
- package/dist/components/CommentThreadPanel.js +47 -0
- package/dist/components/ContextMenu.d.ts +23 -0
- package/dist/components/ContextMenu.js +141 -0
- package/dist/components/FormulaLibraryPopover.d.ts +16 -0
- package/dist/components/FormulaLibraryPopover.js +107 -0
- package/dist/components/LinkEditorPopover.d.ts +3 -1
- package/dist/components/LinkEditorPopover.js +22 -6
- package/dist/components/MediaManager.d.ts +4 -20
- package/dist/components/MediaManager.js +35 -6
- package/dist/components/MediaOverlay.d.ts +36 -0
- package/dist/components/MediaOverlay.js +163 -0
- package/dist/components/SpecialCharacterPopover.d.ts +18 -0
- package/dist/components/SpecialCharacterPopover.js +99 -0
- package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
- package/dist/components/StructuralSuggestionMarkers.js +56 -0
- package/dist/components/SuggestionPanel.d.ts +26 -0
- package/dist/components/SuggestionPanel.js +33 -0
- package/dist/components/TableBorderPopover.d.ts +59 -0
- package/dist/components/TableBorderPopover.js +147 -0
- package/dist/components/TableResizeHandles.d.ts +25 -0
- package/dist/components/TableResizeHandles.js +301 -0
- package/dist/components/TableSizePickerPopover.d.ts +16 -0
- package/dist/components/TableSizePickerPopover.js +126 -0
- package/dist/components/ToolbarPrimitives.d.ts +129 -0
- package/dist/components/ToolbarPrimitives.js +146 -0
- package/dist/components/VersionHistoryPanel.d.ts +9 -0
- package/dist/components/VersionHistoryPanel.js +157 -0
- package/dist/formulaLibrary.d.ts +31 -0
- package/dist/formulaLibrary.js +90 -0
- package/dist/index.d.ts +22 -24
- package/dist/index.js +14 -11
- package/dist/mediaManagerAdapter.d.ts +12 -0
- package/dist/mediaManagerAdapter.js +34 -0
- package/dist/mediaProvider.d.ts +21 -1
- package/dist/specialCharacters.d.ts +22 -0
- package/dist/specialCharacters.js +86 -0
- package/dist/standalone/classic-editor-embed.js +19 -12
- package/dist/suggestionProvider.d.ts +17 -0
- package/dist/suggestionProvider.js +1 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +150 -20
- package/dist/versionProvider.d.ts +29 -0
- package/dist/versionProvider.js +1 -0
- package/package.json +28 -14
- package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
- package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
- package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
- package/dist/adapters/canonicalListCommandBridge.js +0 -430
- package/dist/adapters/coreBold.d.ts +0 -5
- package/dist/adapters/coreBold.js +0 -3
- package/dist/adapters/documentFormats.d.ts +0 -26
- package/dist/adapters/documentFormats.js +0 -49
- package/dist/adapters/docxFormat.d.ts +0 -5
- package/dist/adapters/docxFormat.js +0 -272
- package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
- package/dist/adapters/domBlockCommandBridge.js +0 -79
- package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
- package/dist/adapters/inlineMarkCoreExecution.js +0 -25
- package/dist/adapters/pdfFormat.d.ts +0 -23
- package/dist/adapters/pdfFormat.js +0 -204
- package/dist/adapters/portableDocxAtoms.d.ts +0 -4
- package/dist/adapters/portableDocxAtoms.js +0 -58
- package/dist/adapters/styledDocxFormat.d.ts +0 -8
- package/dist/adapters/styledDocxFormat.js +0 -190
- package/dist/builtInFormatDefinitions.d.ts +0 -5
- package/dist/builtInFormatDefinitions.js +0 -82
- package/dist/canonicalAuthorityFlag.d.ts +0 -32
- package/dist/canonicalAuthorityFlag.js +0 -49
- package/dist/canonicalClipboardRuntime.d.ts +0 -15
- package/dist/canonicalClipboardRuntime.js +0 -76
- package/dist/components/ClassicEditor.d.ts +0 -76
- package/dist/components/ClassicEditor.js +0 -6729
- package/dist/editorController.d.ts +0 -78
- package/dist/editorController.js +0 -298
- package/dist/formatFidelity.d.ts +0 -14
- package/dist/formatFidelity.js +0 -107
- package/dist/formatRuntime.d.ts +0 -50
- package/dist/formatRuntime.js +0 -20
- package/dist/pluginRuntime.d.ts +0 -68
- package/dist/pluginRuntime.js +0 -78
- package/dist/standalone/editor.js +0 -1068
- package/dist/test-harness/atomShadowComparator.d.ts +0 -14
- package/dist/test-harness/atomShadowComparator.js +0 -63
- package/dist/test-harness/blockShadowComparator.d.ts +0 -30
- package/dist/test-harness/blockShadowComparator.js +0 -140
- package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
- package/dist/test-harness/clipboardShadowComparator.js +0 -46
- package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
- package/dist/test-harness/inlineShadowComparator.js +0 -152
- package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
- package/dist/test-harness/legacyAtomEngine.js +0 -15
- package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
- package/dist/test-harness/legacyBlockEngine.js +0 -31
- package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
- package/dist/test-harness/legacyClipboardEngine.js +0 -67
- package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
- package/dist/test-harness/legacyInlineEngine.js +0 -49
- package/dist/test-harness/legacyTableEngine.d.ts +0 -12
- package/dist/test-harness/legacyTableEngine.js +0 -25
- package/dist/test-harness/tableShadowComparator.d.ts +0 -29
- package/dist/test-harness/tableShadowComparator.js +0 -101
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { ColorPickerPopover } from "./ColorPickerPopover.js";
|
|
4
|
+
// Widths start at 2px, not 1px - table_cell's border-collapse table (see
|
|
5
|
+
// theme.ts's th/td rule) already renders a 1px ambient default gridline on
|
|
6
|
+
// every cell. A custom border tied at 1px loses CSS border-collapse's
|
|
7
|
+
// conflict resolution on shared edges half the time (whichever side faces
|
|
8
|
+
// an earlier-in-table-order neighbor keeps the neighbor's default border,
|
|
9
|
+
// confirmed empirically: 1px, and even a fractional 1.5px, lost this way in
|
|
10
|
+
// every engine tested) - only a width strictly greater than the ambient
|
|
11
|
+
// default reliably wins on every side regardless of table position. 2px is
|
|
12
|
+
// the thinnest value that's actually renderable as a real, always-visible
|
|
13
|
+
// custom border under this constraint - see
|
|
14
|
+
// docs/bugs/table-cell-border-thin-preset-loses-border-collapse-tie.md.
|
|
15
|
+
export const BORDER_WIDTH_PRESETS = [
|
|
16
|
+
{ id: "thin", label: "Thin", px: 2 },
|
|
17
|
+
{ id: "medium", label: "Medium", px: 3 },
|
|
18
|
+
{ id: "thick", label: "Thick", px: 4 },
|
|
19
|
+
];
|
|
20
|
+
const STYLE_OPTIONS = ["solid", "dashed", "dotted"];
|
|
21
|
+
const buttonStyle = {
|
|
22
|
+
minHeight: 32, padding: "0 10px", border: "1px solid var(--srte-input-border)", borderRadius: 8,
|
|
23
|
+
background: "var(--srte-input-bg)", color: "var(--srte-menu-text)", cursor: "pointer", fontWeight: 500, fontSize: 13,
|
|
24
|
+
};
|
|
25
|
+
const selectStyle = {
|
|
26
|
+
width: "100%", height: 32, boxSizing: "border-box", padding: "0 8px", borderRadius: 8,
|
|
27
|
+
border: "1px solid var(--srte-input-border)", background: "var(--srte-input-bg)", color: "var(--srte-input-text)",
|
|
28
|
+
font: "inherit", fontSize: 13, cursor: "pointer",
|
|
29
|
+
};
|
|
30
|
+
const fieldLabelStyle = { fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.7, marginBottom: 4 };
|
|
31
|
+
const SIDE_BAR_LENGTH = 40;
|
|
32
|
+
const SIDE_BAR_THICKNESS = 8;
|
|
33
|
+
const SIDE_BOX = 64;
|
|
34
|
+
/**
|
|
35
|
+
* "Border options" - one popover combining every dimension of a cell
|
|
36
|
+
* border, replacing what used to be a separate toolbar-dropdown width
|
|
37
|
+
* <select> and a standalone "Border colour" item. table_cell.attrs only
|
|
38
|
+
* ever stored ONE uniform border for all four sides (`borders`) - "which
|
|
39
|
+
* sides get a border" (per-cell top/right/bottom/left) needed new per-side
|
|
40
|
+
* schema attrs (borderTop/Right/Bottom/Left), added specifically to back
|
|
41
|
+
* this control (see docs/bugs/table-cell-border-color-width-no-ui.md's
|
|
42
|
+
* addendum). One style/width/colour choice applies to whichever sides are
|
|
43
|
+
* toggled on here, rather than letting all four vary independently - real
|
|
44
|
+
* border dialogs (Word, Google Docs) default to this simpler mode too, and
|
|
45
|
+
* a fully independent 4-way style/width/colour matrix wasn't asked for.
|
|
46
|
+
*
|
|
47
|
+
* The colour swatch opens the existing ColorPickerPopover completely
|
|
48
|
+
* unmodified, as a nested popover anchored to the swatch - the dismiss/
|
|
49
|
+
* Escape handling below explicitly accounts for it being a DOM sibling
|
|
50
|
+
* (both are position:fixed overlays, so `rootRef.contains()` alone would
|
|
51
|
+
* never recognize a click inside it as "inside" this popover).
|
|
52
|
+
*/
|
|
53
|
+
export function TableBorderPopover({ x, y, initial, recentColors, onPreview, onApply, onCancel }) {
|
|
54
|
+
const [draft, setDraft] = useState(initial);
|
|
55
|
+
const [colorPickerAnchor, setColorPickerAnchor] = useState(null);
|
|
56
|
+
const hexBeforeNestedPickerRef = useRef(initial.hex);
|
|
57
|
+
const rootRef = useRef(null);
|
|
58
|
+
const swatchRef = useRef(null);
|
|
59
|
+
const [placement, setPlacement] = useState(null);
|
|
60
|
+
const onCancelRef = useRef(onCancel);
|
|
61
|
+
onCancelRef.current = onCancel;
|
|
62
|
+
const onPreviewRef = useRef(onPreview);
|
|
63
|
+
onPreviewRef.current = onPreview;
|
|
64
|
+
useLayoutEffect(() => {
|
|
65
|
+
const el = rootRef.current;
|
|
66
|
+
if (!el)
|
|
67
|
+
return;
|
|
68
|
+
const margin = 8;
|
|
69
|
+
const { width, height } = el.getBoundingClientRect();
|
|
70
|
+
const viewportWidth = window.innerWidth;
|
|
71
|
+
const viewportHeight = window.innerHeight;
|
|
72
|
+
const overflowsRight = x + width > viewportWidth - margin;
|
|
73
|
+
const left = overflowsRight ? x - width : x;
|
|
74
|
+
const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
|
|
75
|
+
const overflowsBottom = y + height > viewportHeight - margin;
|
|
76
|
+
const top = overflowsBottom ? y - height : y;
|
|
77
|
+
const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
|
|
78
|
+
setPlacement({ left: clampedLeft, top: clampedTop });
|
|
79
|
+
}, [x, y]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const dismissIfOutside = (event) => {
|
|
82
|
+
const target = event.target;
|
|
83
|
+
if (rootRef.current?.contains(target))
|
|
84
|
+
return;
|
|
85
|
+
// The nested colour picker is a DOM sibling (both are position:fixed
|
|
86
|
+
// overlays), not a descendant of rootRef - without this, clicking
|
|
87
|
+
// inside it would look like an "outside" click on this popover.
|
|
88
|
+
if (colorPickerAnchor && document.querySelector('[data-srte-color-popover="true"]')?.contains(target))
|
|
89
|
+
return;
|
|
90
|
+
onCancelRef.current();
|
|
91
|
+
};
|
|
92
|
+
window.addEventListener("pointerdown", dismissIfOutside, true);
|
|
93
|
+
window.addEventListener("mousedown", dismissIfOutside, true);
|
|
94
|
+
return () => {
|
|
95
|
+
window.removeEventListener("pointerdown", dismissIfOutside, true);
|
|
96
|
+
window.removeEventListener("mousedown", dismissIfOutside, true);
|
|
97
|
+
};
|
|
98
|
+
}, [colorPickerAnchor]);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const onWindowKeyDown = (event) => {
|
|
101
|
+
if (event.key !== "Escape")
|
|
102
|
+
return;
|
|
103
|
+
// While the nested colour picker is open, let its own Escape handler
|
|
104
|
+
// commit/dismiss just that picker first - a second Escape closes this
|
|
105
|
+
// whole dialog.
|
|
106
|
+
if (colorPickerAnchor)
|
|
107
|
+
return;
|
|
108
|
+
onCancelRef.current();
|
|
109
|
+
};
|
|
110
|
+
window.addEventListener("keydown", onWindowKeyDown);
|
|
111
|
+
return () => window.removeEventListener("keydown", onWindowKeyDown);
|
|
112
|
+
}, [colorPickerAnchor]);
|
|
113
|
+
const update = (patch) => {
|
|
114
|
+
const next = { ...draft, ...patch };
|
|
115
|
+
setDraft(next);
|
|
116
|
+
onPreviewRef.current(next);
|
|
117
|
+
};
|
|
118
|
+
const toggleSide = (side) => update({ sides: { ...draft.sides, [side]: !draft.sides[side] } });
|
|
119
|
+
const edgeButton = (side, style, label) => (_jsx("button", { type: "button", "aria-label": label, "aria-pressed": draft.sides[side], onMouseDown: (event) => event.preventDefault(), onClick: () => toggleSide(side), style: {
|
|
120
|
+
position: "absolute", border: "none", borderRadius: 3, cursor: "pointer", padding: 0,
|
|
121
|
+
background: draft.sides[side] ? "var(--srte-primary)" : "var(--srte-input-border)",
|
|
122
|
+
...style,
|
|
123
|
+
} }));
|
|
124
|
+
return (_jsxs("div", { ref: rootRef, "data-srte-table-border-popover": "true", role: "dialog", "aria-label": "Border options", style: {
|
|
125
|
+
position: "fixed",
|
|
126
|
+
left: placement?.left ?? x,
|
|
127
|
+
top: placement?.top ?? y,
|
|
128
|
+
visibility: placement ? "visible" : "hidden",
|
|
129
|
+
zIndex: 70,
|
|
130
|
+
width: 240,
|
|
131
|
+
boxSizing: "border-box",
|
|
132
|
+
background: "var(--srte-menu-bg)",
|
|
133
|
+
color: "var(--srte-menu-text)",
|
|
134
|
+
border: "1px solid var(--srte-border)",
|
|
135
|
+
borderRadius: 12,
|
|
136
|
+
boxShadow: "var(--srte-menu-shadow)",
|
|
137
|
+
padding: 14,
|
|
138
|
+
display: "flex",
|
|
139
|
+
flexDirection: "column",
|
|
140
|
+
gap: 12,
|
|
141
|
+
}, children: [_jsx("div", { style: { fontWeight: 650 }, children: "Border options" }), _jsxs("div", { style: { display: "flex", gap: 14, alignItems: "center" }, children: [_jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Sides" }), _jsxs("div", { style: { position: "relative", width: SIDE_BOX, height: SIDE_BOX, border: "1px dashed var(--srte-border)", borderRadius: 6 }, children: [edgeButton("top", { top: -SIDE_BAR_THICKNESS / 2, left: (SIDE_BOX - SIDE_BAR_LENGTH) / 2, width: SIDE_BAR_LENGTH, height: SIDE_BAR_THICKNESS }, "Top border"), edgeButton("bottom", { bottom: -SIDE_BAR_THICKNESS / 2, left: (SIDE_BOX - SIDE_BAR_LENGTH) / 2, width: SIDE_BAR_LENGTH, height: SIDE_BAR_THICKNESS }, "Bottom border"), edgeButton("left", { left: -SIDE_BAR_THICKNESS / 2, top: (SIDE_BOX - SIDE_BAR_LENGTH) / 2, width: SIDE_BAR_THICKNESS, height: SIDE_BAR_LENGTH }, "Left border"), edgeButton("right", { right: -SIDE_BAR_THICKNESS / 2, top: (SIDE_BOX - SIDE_BAR_LENGTH) / 2, width: SIDE_BAR_THICKNESS, height: SIDE_BAR_LENGTH }, "Right border")] })] }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [_jsx("button", { type: "button", style: buttonStyle, onMouseDown: (event) => event.preventDefault(), onClick: () => update({ sides: { top: true, right: true, bottom: true, left: true } }), children: "All sides" }), _jsx("button", { type: "button", style: buttonStyle, onMouseDown: (event) => event.preventDefault(), onClick: () => update({ sides: { top: false, right: false, bottom: false, left: false } }), children: "No border" })] })] }), _jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Style" }), _jsx("select", { "aria-label": "Border style", value: draft.style, onChange: (event) => update({ style: event.target.value }), style: selectStyle, children: STYLE_OPTIONS.map((style) => _jsxs("option", { value: style, children: [style[0].toUpperCase(), style.slice(1)] }, style)) })] }), _jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Width" }), _jsx("select", { "aria-label": "Border width", value: BORDER_WIDTH_PRESETS.find((preset) => preset.px === draft.widthPx)?.id ?? BORDER_WIDTH_PRESETS[0].id, onChange: (event) => { const preset = BORDER_WIDTH_PRESETS.find((entry) => entry.id === event.target.value); if (preset)
|
|
142
|
+
update({ widthPx: preset.px }); }, style: selectStyle, children: BORDER_WIDTH_PRESETS.map((preset) => _jsx("option", { value: preset.id, children: preset.label }, preset.id)) })] }), _jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Colour" }), _jsxs("button", { ref: swatchRef, type: "button", "aria-label": "Border colour", onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
143
|
+
hexBeforeNestedPickerRef.current = draft.hex;
|
|
144
|
+
const rect = swatchRef.current?.getBoundingClientRect();
|
|
145
|
+
setColorPickerAnchor({ x: rect?.left ?? x, y: (rect?.bottom ?? y) + 4 });
|
|
146
|
+
}, style: { ...buttonStyle, width: "100%", display: "flex", alignItems: "center", gap: 8, justifyContent: "flex-start" }, children: [_jsx("span", { style: { width: 18, height: 18, borderRadius: 4, border: "1px solid var(--srte-input-border)", background: draft.hex }, "aria-hidden": "true" }), draft.hex] })] }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: [_jsx("button", { type: "button", style: buttonStyle, onClick: onCancel, children: "Cancel" }), _jsx("button", { type: "button", onClick: () => onApply(draft), style: { ...buttonStyle, borderColor: "var(--srte-primary)", background: "var(--srte-primary)", color: "var(--srte-on-primary)" }, children: "Apply" })] }), colorPickerAnchor && _jsx(ColorPickerPopover, { x: colorPickerAnchor.x, y: colorPickerAnchor.y, label: "Border colour", initialValue: draft.hex, recentColors: recentColors, onPreview: (hex) => update({ hex }), onApply: (hex) => { update({ hex }); setColorPickerAnchor(null); }, onCancel: () => { update({ hex: hexBeforeNestedPickerRef.current }); setColorPickerAnchor(null); } })] }));
|
|
147
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface TableResizeHandlesProps {
|
|
2
|
+
tableElement: HTMLTableElement;
|
|
3
|
+
/**
|
|
4
|
+
* `currentWidths` is every column's real, currently-rendered width (not
|
|
5
|
+
* just the dragged one) - see the handleUp comment below for why the
|
|
6
|
+
* caller needs this, not just the one changed index.
|
|
7
|
+
*/
|
|
8
|
+
onResizeColumn: (index: number, width: number, currentWidths: readonly number[]) => void;
|
|
9
|
+
onResizeRow: (index: number, height: number, adjacent?: {
|
|
10
|
+
index: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Phase 11.5 §2.2: column-width/row-height drag handles calling the
|
|
16
|
+
* existing setTableColumnWidthCommand/setTableRowHeightCommand (row/column
|
|
17
|
+
* reorder already had toolbar buttons - "Move row up/down"/"Move column
|
|
18
|
+
* left/right" - wired since before this phase; resize had no UI at all).
|
|
19
|
+
* Commit-on-release, not a live preview during drag - a deliberately
|
|
20
|
+
* bounded scope for the first pass. Recomputes handle geometry from the
|
|
21
|
+
* live table's actual rendered cell rects on every table layout change via
|
|
22
|
+
* ResizeObserver, since table content (and therefore column/row sizes)
|
|
23
|
+
* changes independently of resize actions.
|
|
24
|
+
*/
|
|
25
|
+
export declare function TableResizeHandles({ tableElement, onResizeColumn, onResizeRow }: TableResizeHandlesProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
const HANDLE_THICKNESS = 6;
|
|
4
|
+
const MIN_SIZE = 20;
|
|
5
|
+
/**
|
|
6
|
+
* Phase 11.5 §2.2: column-width/row-height drag handles calling the
|
|
7
|
+
* existing setTableColumnWidthCommand/setTableRowHeightCommand (row/column
|
|
8
|
+
* reorder already had toolbar buttons - "Move row up/down"/"Move column
|
|
9
|
+
* left/right" - wired since before this phase; resize had no UI at all).
|
|
10
|
+
* Commit-on-release, not a live preview during drag - a deliberately
|
|
11
|
+
* bounded scope for the first pass. Recomputes handle geometry from the
|
|
12
|
+
* live table's actual rendered cell rects on every table layout change via
|
|
13
|
+
* ResizeObserver, since table content (and therefore column/row sizes)
|
|
14
|
+
* changes independently of resize actions.
|
|
15
|
+
*/
|
|
16
|
+
export function TableResizeHandles({ tableElement, onResizeColumn, onResizeRow }) {
|
|
17
|
+
const [columns, setColumns] = useState([]);
|
|
18
|
+
const [rows, setRows] = useState([]);
|
|
19
|
+
// `nextStartSize`/`nextIndex` are null for the last row/column's own
|
|
20
|
+
// outer-edge handle - that one still grows/shrinks the table's overall
|
|
21
|
+
// size (no neighbor to redistribute with); every other handle has a
|
|
22
|
+
// real neighbor and redistributes between the two instead.
|
|
23
|
+
const dragRef = useRef(null);
|
|
24
|
+
const [dragging, setDragging] = useState(null);
|
|
25
|
+
// handleUp (below) is defined inside an effect that doesn't depend on
|
|
26
|
+
// `columns`/`rows` (to avoid tearing down the pointer listeners on every
|
|
27
|
+
// ResizeObserver tick) - these refs keep it reading the latest measured
|
|
28
|
+
// sizes anyway, without that staleness risk.
|
|
29
|
+
const columnsRef = useRef([]);
|
|
30
|
+
columnsRef.current = columns;
|
|
31
|
+
const rowsRef = useRef([]);
|
|
32
|
+
rowsRef.current = rows;
|
|
33
|
+
// Shared by the passive observers below (structural/layout changes that
|
|
34
|
+
// happen outside a drag) and by handleMove during an active drag (see
|
|
35
|
+
// that effect for why an active drag can't just wait for these
|
|
36
|
+
// observers to notice).
|
|
37
|
+
const recompute = useCallback(() => {
|
|
38
|
+
const tableRect = tableElement.getBoundingClientRect();
|
|
39
|
+
const firstRowCells = [...tableElement.querySelectorAll("tr")[0]?.children || []];
|
|
40
|
+
const colBoundaries = [];
|
|
41
|
+
firstRowCells.forEach((cell, index) => {
|
|
42
|
+
const rect = cell.getBoundingClientRect();
|
|
43
|
+
colBoundaries.push({ index, position: rect.right, length: tableRect.height, start: tableRect.top, size: rect.width });
|
|
44
|
+
});
|
|
45
|
+
const rowElements = [...tableElement.querySelectorAll(":scope > tr, :scope > tbody > tr")];
|
|
46
|
+
const rowBoundaries = [];
|
|
47
|
+
rowElements.forEach((row, index) => {
|
|
48
|
+
const rect = row.getBoundingClientRect();
|
|
49
|
+
rowBoundaries.push({ index, position: rect.bottom, length: tableRect.width, start: tableRect.left, size: rect.height });
|
|
50
|
+
});
|
|
51
|
+
setColumns(colBoundaries);
|
|
52
|
+
setRows(rowBoundaries);
|
|
53
|
+
}, [tableElement]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
recompute();
|
|
56
|
+
const observer = new ResizeObserver(recompute);
|
|
57
|
+
observer.observe(tableElement);
|
|
58
|
+
// ResizeObserver only fires when tableElement's own box size changes -
|
|
59
|
+
// inserting/removing a row or column into a table that stays the same
|
|
60
|
+
// overall width (the common case: width:100%, or a pinned literal
|
|
61
|
+
// width unaffected by redistributing space across one more/fewer
|
|
62
|
+
// column) never changes that box size, so it never fires. Handles
|
|
63
|
+
// then stay stale: wrong count, wrong position, wrong `boundary.size`
|
|
64
|
+
// for whatever column/row the model now has at that index - dragging
|
|
65
|
+
// one computed a wildly wrong new width from a stale start size. A
|
|
66
|
+
// MutationObserver on the table's own structure (row/cell add/remove/
|
|
67
|
+
// reorder) catches exactly the class of change ResizeObserver misses.
|
|
68
|
+
const mutationObserver = new MutationObserver(recompute);
|
|
69
|
+
mutationObserver.observe(tableElement, { childList: true, subtree: true });
|
|
70
|
+
const ownerWindow = tableElement.ownerDocument.defaultView;
|
|
71
|
+
ownerWindow?.addEventListener("scroll", recompute, true);
|
|
72
|
+
return () => {
|
|
73
|
+
observer.disconnect();
|
|
74
|
+
mutationObserver.disconnect();
|
|
75
|
+
ownerWindow?.removeEventListener("scroll", recompute, true);
|
|
76
|
+
};
|
|
77
|
+
}, [tableElement, recompute]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const ownerWindow = tableElement.ownerDocument.defaultView;
|
|
80
|
+
if (!ownerWindow)
|
|
81
|
+
return;
|
|
82
|
+
// For an internal boundary (a real neighbor exists), clamp the raw
|
|
83
|
+
// pointer delta so neither side ever goes below MIN_SIZE, then apply
|
|
84
|
+
// the *same* clamped delta with opposite signs to both sides - their
|
|
85
|
+
// sum is therefore always exactly startSize + nextStartSize, whatever
|
|
86
|
+
// the clamp did. That invariant is what makes "the border just moves"
|
|
87
|
+
// true: the table's own total size never has to change for these.
|
|
88
|
+
//
|
|
89
|
+
// Rows can't use this arithmetic-only approach: a <tr>'s rendered
|
|
90
|
+
// height can never go below what its own cell content needs (an
|
|
91
|
+
// explicit row height behaves as a minimum, not a hard cap, in table
|
|
92
|
+
// layout) - unlike a column's width under table-layout:fixed, which
|
|
93
|
+
// genuinely can be squeezed below its content's natural width. Naively
|
|
94
|
+
// clamping to a fixed MIN_SIZE the way columns do would, the instant a
|
|
95
|
+
// row's *real* content floor was higher than that, still hand the
|
|
96
|
+
// *other* row the full requested delta anyway - the total table
|
|
97
|
+
// height would silently grow instead of staying constant, and the row
|
|
98
|
+
// actually being dragged would appear frozen while its neighbor
|
|
99
|
+
// visibly ballooned (this is what "resize rows... move cursor up
|
|
100
|
+
// then it is effecting below row height" was - see docs/bugs/
|
|
101
|
+
// row-resize-drag-up-only-grows-the-row-below.md). Measuring the real
|
|
102
|
+
// DOM after attempting a shrink, and handing the other row only what
|
|
103
|
+
// was actually ceded, keeps that same invariant true regardless of
|
|
104
|
+
// where a row's real floor turns out to be.
|
|
105
|
+
const resolveSizes = (drag, event) => {
|
|
106
|
+
const rawDelta = drag.kind === "column" ? event.clientX - drag.startClient : event.clientY - drag.startClient;
|
|
107
|
+
if (drag.kind === "column") {
|
|
108
|
+
if (drag.nextStartSize === null) {
|
|
109
|
+
return { size: Math.max(MIN_SIZE, Math.round(drag.startSize + rawDelta)), nextSize: null };
|
|
110
|
+
}
|
|
111
|
+
const minDelta = -(drag.startSize - MIN_SIZE);
|
|
112
|
+
const maxDelta = drag.nextStartSize - MIN_SIZE;
|
|
113
|
+
const delta = Math.min(Math.max(rawDelta, minDelta), maxDelta);
|
|
114
|
+
return { size: Math.round(drag.startSize + delta), nextSize: Math.round(drag.nextStartSize - delta) };
|
|
115
|
+
}
|
|
116
|
+
const rowElements = [...tableElement.querySelectorAll(":scope > tr, :scope > tbody > tr")];
|
|
117
|
+
if (drag.nextStartSize === null || drag.nextIndex === null) {
|
|
118
|
+
const target = Math.max(MIN_SIZE, Math.round(drag.startSize + rawDelta));
|
|
119
|
+
const rowElement = rowElements[drag.index];
|
|
120
|
+
if (rowElement)
|
|
121
|
+
rowElement.style.height = `${target}px`;
|
|
122
|
+
const actual = rowElement ? Math.round(rowElement.getBoundingClientRect().height) : target;
|
|
123
|
+
return { size: actual, nextSize: null };
|
|
124
|
+
}
|
|
125
|
+
const rowElement = rowElements[drag.index];
|
|
126
|
+
const nextRowElement = rowElements[drag.nextIndex];
|
|
127
|
+
if (rawDelta <= 0) {
|
|
128
|
+
// The dragged (above) row is the one shrinking; the row below
|
|
129
|
+
// only grows by however much the above row actually gave up.
|
|
130
|
+
// There's no equivalent fallback for the dragged row itself here -
|
|
131
|
+
// a row that can't shrink just can't, full stop, no matter how
|
|
132
|
+
// flexible the table's total height is willing to be.
|
|
133
|
+
const target = Math.max(MIN_SIZE, Math.round(drag.startSize + rawDelta));
|
|
134
|
+
if (rowElement)
|
|
135
|
+
rowElement.style.height = `${target}px`;
|
|
136
|
+
const actualSize = rowElement ? Math.round(rowElement.getBoundingClientRect().height) : target;
|
|
137
|
+
return { size: actualSize, nextSize: Math.round(drag.nextStartSize - (actualSize - drag.startSize)) };
|
|
138
|
+
}
|
|
139
|
+
// Dragging down: the row below is asked to shrink to pay for the
|
|
140
|
+
// dragged row's growth, exactly like the mirror case above - but
|
|
141
|
+
// unlike a row shrinking, a row *growing* is never floor-limited (a
|
|
142
|
+
// <tr>'s content only ever imposes a minimum, never a maximum), so
|
|
143
|
+
// there's no symmetric "the dragged row just can't grow" outcome to
|
|
144
|
+
// fall back to here. Making the dragged row's growth conditional on
|
|
145
|
+
// the neighbor actually having room to give (the way the shrink
|
|
146
|
+
// case correctly is) meant that on a table where every row already
|
|
147
|
+
// sits at its own content floor - true of almost any freshly
|
|
148
|
+
// created or pasted table with short cell text - internal
|
|
149
|
+
// row-boundary dragging did *nothing at all* in either direction:
|
|
150
|
+
// shrinking had nowhere to go, and growing was blocked because the
|
|
151
|
+
// neighbor had nowhere to give either. See docs/bugs/
|
|
152
|
+
// row-resize-blocked-when-every-row-is-at-its-floor.md. Grow the
|
|
153
|
+
// dragged row by the full requested amount unconditionally, and let
|
|
154
|
+
// the neighbor give up only what it actually can; if that's less
|
|
155
|
+
// than what was asked, the shortfall becomes real table growth -
|
|
156
|
+
// the only place that space could possibly come from - rather than
|
|
157
|
+
// silently refusing the drag altogether.
|
|
158
|
+
const size = Math.max(MIN_SIZE, Math.round(drag.startSize + rawDelta));
|
|
159
|
+
if (rowElement)
|
|
160
|
+
rowElement.style.height = `${size}px`;
|
|
161
|
+
const target = Math.max(MIN_SIZE, Math.round(drag.nextStartSize - rawDelta));
|
|
162
|
+
if (nextRowElement)
|
|
163
|
+
nextRowElement.style.height = `${target}px`;
|
|
164
|
+
const nextSize = nextRowElement ? Math.round(nextRowElement.getBoundingClientRect().height) : target;
|
|
165
|
+
return { size, nextSize };
|
|
166
|
+
};
|
|
167
|
+
const handleMove = (event) => {
|
|
168
|
+
const drag = dragRef.current;
|
|
169
|
+
if (!drag)
|
|
170
|
+
return;
|
|
171
|
+
setDragging({ kind: drag.kind, index: drag.index, position: drag.kind === "column" ? event.clientX : event.clientY });
|
|
172
|
+
// Live preview: mutate the real <col>/<tr> directly during the drag
|
|
173
|
+
// (commit-on-release still only touches the model, via onResizeColumn/
|
|
174
|
+
// onResizeRow below) - previously nothing rendered until pointerup, so
|
|
175
|
+
// the drag looked frozen until it suddenly jumped to the final size.
|
|
176
|
+
// Undoing this on the next real render is automatic: committing
|
|
177
|
+
// triggers the renderer's own re-render from authoritative model
|
|
178
|
+
// state, which overwrites whatever this loop wrote.
|
|
179
|
+
const { size: liveSize, nextSize: liveNextSize } = resolveSizes(drag, event);
|
|
180
|
+
if (drag.kind === "column") {
|
|
181
|
+
const cols = [...(tableElement.querySelector("colgroup")?.children || [])];
|
|
182
|
+
const col = cols[drag.index];
|
|
183
|
+
if (col)
|
|
184
|
+
col.style.width = `${liveSize}px`;
|
|
185
|
+
if (liveNextSize !== null && drag.nextIndex !== null) {
|
|
186
|
+
const nextCol = cols[drag.nextIndex];
|
|
187
|
+
if (nextCol)
|
|
188
|
+
nextCol.style.width = `${liveNextSize}px`;
|
|
189
|
+
}
|
|
190
|
+
// The stylesheet's `table-layout: fixed` + `width: 100%` treats
|
|
191
|
+
// <col> widths as proportions of the table's own rendered width,
|
|
192
|
+
// not literal pixels, unless the table's own width matches their
|
|
193
|
+
// sum (see surface/renderer.ts's table-width fix) - keep that sum
|
|
194
|
+
// live too, or every other column visibly compresses for the
|
|
195
|
+
// duration of the drag even though the fix restores them on
|
|
196
|
+
// release. Redistributing between two columns (liveNextSize !==
|
|
197
|
+
// null) never changes this sum by construction, so only the
|
|
198
|
+
// last column's own resize (liveNextSize === null) needs this.
|
|
199
|
+
if (liveNextSize === null) {
|
|
200
|
+
const total = cols.reduce((sum, candidate, index) => sum + (index === drag.index ? liveSize : (parseFloat(candidate.style.width) || 0)), 0);
|
|
201
|
+
if (total > 0)
|
|
202
|
+
tableElement.style.width = `${total}px`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const rowElements = [...tableElement.querySelectorAll(":scope > tr, :scope > tbody > tr")];
|
|
207
|
+
const rowElement = rowElements[drag.index];
|
|
208
|
+
if (rowElement)
|
|
209
|
+
rowElement.style.height = `${liveSize}px`;
|
|
210
|
+
if (liveNextSize !== null && drag.nextIndex !== null) {
|
|
211
|
+
const nextRow = rowElements[drag.nextIndex];
|
|
212
|
+
if (nextRow)
|
|
213
|
+
nextRow.style.height = `${liveNextSize}px`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// The handles' own on-screen position/length come from `columns`/
|
|
217
|
+
// `rows` state, which the effect above only refreshes via
|
|
218
|
+
// ResizeObserver/MutationObserver/scroll - none of which reliably
|
|
219
|
+
// fire here. Column drags happen to nudge the table's own rendered
|
|
220
|
+
// width by sub-pixel rounding (table-layout: fixed redistributing a
|
|
221
|
+
// stray pixel), which incidentally retriggers ResizeObserver; an
|
|
222
|
+
// internal row-boundary drag keeps the table's total height exactly
|
|
223
|
+
// constant by construction (see resolveSizes above), so it never
|
|
224
|
+
// does. Left alone, the row handle (and the blue line it renders)
|
|
225
|
+
// stays frozen at its pre-drag position while the real row border
|
|
226
|
+
// moves live underneath it - reported as a misplaced, "very slow"
|
|
227
|
+
// blue line that overflows past the table/editor as the gap between
|
|
228
|
+
// the frozen handle and the actual, now-taller table grows. Forcing
|
|
229
|
+
// a synchronous re-measure after every live style mutation (a
|
|
230
|
+
// getBoundingClientRect read right after a style write forces the
|
|
231
|
+
// browser to flush layout first, so this always reflects the
|
|
232
|
+
// mutation just above, not a stale value) keeps both column and row
|
|
233
|
+
// handles tracking the real border on every pointermove instead of
|
|
234
|
+
// waiting on an observer that may not fire at all.
|
|
235
|
+
recompute();
|
|
236
|
+
};
|
|
237
|
+
const handleUp = (event) => {
|
|
238
|
+
const drag = dragRef.current;
|
|
239
|
+
if (!drag)
|
|
240
|
+
return;
|
|
241
|
+
const { size, nextSize } = resolveSizes(drag, event);
|
|
242
|
+
if (drag.kind === "column") {
|
|
243
|
+
// A table pasted/created with no columnWidths yet (natural,
|
|
244
|
+
// stretched rendering - see docs/bugs/table-shrinks-after-paste.md)
|
|
245
|
+
// has no real per-column data for the command layer to fall back
|
|
246
|
+
// on; without this, setTableColumnWidthCommand's own fallback
|
|
247
|
+
// (Array(columns).fill(120)) would silently reset every OTHER
|
|
248
|
+
// column to a fabricated 120px default the instant any one column
|
|
249
|
+
// was resized, shrinking the whole table - passing the real,
|
|
250
|
+
// currently-measured width of every column lets the caller seed a
|
|
251
|
+
// complete, accurate columnWidths array instead of guessing.
|
|
252
|
+
const currentWidths = columnsRef.current.map((boundary) => boundary.size);
|
|
253
|
+
if (nextSize !== null && drag.nextIndex !== null)
|
|
254
|
+
currentWidths[drag.nextIndex] = nextSize;
|
|
255
|
+
onResizeColumn(drag.index, size, currentWidths);
|
|
256
|
+
}
|
|
257
|
+
else if (nextSize !== null && drag.nextIndex !== null) {
|
|
258
|
+
onResizeRow(drag.index, size, { index: drag.nextIndex, height: nextSize });
|
|
259
|
+
}
|
|
260
|
+
else
|
|
261
|
+
onResizeRow(drag.index, size);
|
|
262
|
+
dragRef.current = null;
|
|
263
|
+
setDragging(null);
|
|
264
|
+
};
|
|
265
|
+
ownerWindow.addEventListener("pointermove", handleMove);
|
|
266
|
+
ownerWindow.addEventListener("pointerup", handleUp);
|
|
267
|
+
return () => {
|
|
268
|
+
ownerWindow.removeEventListener("pointermove", handleMove);
|
|
269
|
+
ownerWindow.removeEventListener("pointerup", handleUp);
|
|
270
|
+
};
|
|
271
|
+
}, [tableElement, onResizeColumn, onResizeRow, recompute]);
|
|
272
|
+
const startDrag = (kind, boundary) => (event) => {
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
const siblings = kind === "column" ? columnsRef.current : rowsRef.current;
|
|
275
|
+
const next = siblings[boundary.index + 1] ?? null;
|
|
276
|
+
dragRef.current = {
|
|
277
|
+
kind, index: boundary.index, startClient: kind === "column" ? event.clientX : event.clientY, startSize: boundary.size,
|
|
278
|
+
nextIndex: next?.index ?? null, nextStartSize: next?.size ?? null,
|
|
279
|
+
};
|
|
280
|
+
setDragging({ kind, index: boundary.index, position: kind === "column" ? event.clientX : event.clientY });
|
|
281
|
+
};
|
|
282
|
+
return (_jsxs("div", { "data-srte-table-resize-handles": "true", style: { position: "fixed", inset: 0, pointerEvents: "none", zIndex: 60 }, children: [rows.map((boundary) => (_jsx("div", { role: "separator", "aria-orientation": "horizontal", "aria-label": `Resize row ${boundary.index + 1}`, "data-srte-row-resize-handle": boundary.index, onPointerDown: startDrag("row", boundary), style: {
|
|
283
|
+
position: "fixed",
|
|
284
|
+
left: boundary.start,
|
|
285
|
+
top: boundary.position - HANDLE_THICKNESS / 2,
|
|
286
|
+
width: boundary.length,
|
|
287
|
+
height: HANDLE_THICKNESS,
|
|
288
|
+
cursor: "row-resize",
|
|
289
|
+
pointerEvents: "auto",
|
|
290
|
+
background: dragging?.kind === "row" && dragging.index === boundary.index ? "var(--srte-primary)" : "transparent",
|
|
291
|
+
} }, `row-${boundary.index}`))), columns.map((boundary) => (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": `Resize column ${boundary.index + 1}`, "data-srte-column-resize-handle": boundary.index, onPointerDown: startDrag("column", boundary), style: {
|
|
292
|
+
position: "fixed",
|
|
293
|
+
left: boundary.position - HANDLE_THICKNESS / 2,
|
|
294
|
+
top: boundary.start,
|
|
295
|
+
width: HANDLE_THICKNESS,
|
|
296
|
+
height: boundary.length,
|
|
297
|
+
cursor: "col-resize",
|
|
298
|
+
pointerEvents: "auto",
|
|
299
|
+
background: dragging?.kind === "column" && dragging.index === boundary.index ? "var(--srte-primary)" : "transparent",
|
|
300
|
+
} }, `col-${boundary.index}`)))] }));
|
|
301
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TableSizePickerPopoverProps {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
onInsert: (rows: number, columns: number) => void;
|
|
5
|
+
onCancel: () => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* "Insert table" size picker - CKEditor's own pattern: hover an N×M grid to
|
|
9
|
+
* preview the size, click a cell to insert immediately (one decisive click,
|
|
10
|
+
* no separate Apply/Confirm step, since a grid cell click is already a
|
|
11
|
+
* single deliberate choice - the same reasoning ColorPickerPopover's
|
|
12
|
+
* recent-swatch buttons and the removed preset grid used). A numeric
|
|
13
|
+
* fallback below covers sizes past the grid's cap without capping what the
|
|
14
|
+
* tool can actually do.
|
|
15
|
+
*/
|
|
16
|
+
export declare function TableSizePickerPopover({ x, y, onInsert, onCancel }: TableSizePickerPopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
const MAX_GRID = 8;
|
|
4
|
+
const buttonStyle = {
|
|
5
|
+
minHeight: 32,
|
|
6
|
+
padding: "0 10px",
|
|
7
|
+
border: "1px solid var(--srte-input-border)",
|
|
8
|
+
borderRadius: 8,
|
|
9
|
+
background: "var(--srte-input-bg)",
|
|
10
|
+
color: "var(--srte-menu-text)",
|
|
11
|
+
cursor: "pointer",
|
|
12
|
+
fontWeight: 500,
|
|
13
|
+
fontSize: 13,
|
|
14
|
+
};
|
|
15
|
+
const inputStyle = {
|
|
16
|
+
width: 48,
|
|
17
|
+
height: 32,
|
|
18
|
+
boxSizing: "border-box",
|
|
19
|
+
padding: "0 8px",
|
|
20
|
+
border: "1px solid var(--srte-input-border)",
|
|
21
|
+
borderRadius: 8,
|
|
22
|
+
outline: "none",
|
|
23
|
+
background: "var(--srte-input-bg)",
|
|
24
|
+
color: "var(--srte-input-text)",
|
|
25
|
+
font: "inherit",
|
|
26
|
+
textAlign: "center",
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* "Insert table" size picker - CKEditor's own pattern: hover an N×M grid to
|
|
30
|
+
* preview the size, click a cell to insert immediately (one decisive click,
|
|
31
|
+
* no separate Apply/Confirm step, since a grid cell click is already a
|
|
32
|
+
* single deliberate choice - the same reasoning ColorPickerPopover's
|
|
33
|
+
* recent-swatch buttons and the removed preset grid used). A numeric
|
|
34
|
+
* fallback below covers sizes past the grid's cap without capping what the
|
|
35
|
+
* tool can actually do.
|
|
36
|
+
*/
|
|
37
|
+
export function TableSizePickerPopover({ x, y, onInsert, onCancel }) {
|
|
38
|
+
const rootRef = useRef(null);
|
|
39
|
+
const rowsRef = useRef(null);
|
|
40
|
+
const [placement, setPlacement] = useState(null);
|
|
41
|
+
const [hover, setHover] = useState(null);
|
|
42
|
+
const [customRows, setCustomRows] = useState("2");
|
|
43
|
+
const [customColumns, setCustomColumns] = useState("2");
|
|
44
|
+
const onCancelRef = useRef(onCancel);
|
|
45
|
+
onCancelRef.current = onCancel;
|
|
46
|
+
useLayoutEffect(() => {
|
|
47
|
+
const el = rootRef.current;
|
|
48
|
+
if (!el)
|
|
49
|
+
return;
|
|
50
|
+
const margin = 8;
|
|
51
|
+
const { width, height } = el.getBoundingClientRect();
|
|
52
|
+
const viewportWidth = window.innerWidth;
|
|
53
|
+
const viewportHeight = window.innerHeight;
|
|
54
|
+
const overflowsRight = x + width > viewportWidth - margin;
|
|
55
|
+
const left = overflowsRight ? x - width : x;
|
|
56
|
+
const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
|
|
57
|
+
const overflowsBottom = y + height > viewportHeight - margin;
|
|
58
|
+
const top = overflowsBottom ? y - height : y;
|
|
59
|
+
const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
|
|
60
|
+
setPlacement({ left: clampedLeft, top: clampedTop });
|
|
61
|
+
}, [x, y]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const dismissIfOutside = (event) => {
|
|
64
|
+
if (rootRef.current && !rootRef.current.contains(event.target))
|
|
65
|
+
onCancelRef.current();
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener("pointerdown", dismissIfOutside, true);
|
|
68
|
+
window.addEventListener("mousedown", dismissIfOutside, true);
|
|
69
|
+
return () => {
|
|
70
|
+
window.removeEventListener("pointerdown", dismissIfOutside, true);
|
|
71
|
+
window.removeEventListener("mousedown", dismissIfOutside, true);
|
|
72
|
+
};
|
|
73
|
+
}, []);
|
|
74
|
+
// Escape must dismiss regardless of focus - unlike LinkEditorPopover/
|
|
75
|
+
// ColorPickerPopover, nothing here is autofocused on open (the grid is
|
|
76
|
+
// meant to be hovered, not tabbed into first), so focus stays wherever it
|
|
77
|
+
// was (typically the "Insert table" toolbar button) and the div's own
|
|
78
|
+
// onKeyDown below would never see the keypress.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const onWindowKeyDown = (event) => {
|
|
81
|
+
if (event.key === "Escape")
|
|
82
|
+
onCancelRef.current();
|
|
83
|
+
};
|
|
84
|
+
window.addEventListener("keydown", onWindowKeyDown);
|
|
85
|
+
return () => window.removeEventListener("keydown", onWindowKeyDown);
|
|
86
|
+
}, []);
|
|
87
|
+
const insertCustom = () => {
|
|
88
|
+
const rows = Math.min(50, Math.max(1, Math.round(Number(customRows)) || 0));
|
|
89
|
+
const columns = Math.min(50, Math.max(1, Math.round(Number(customColumns)) || 0));
|
|
90
|
+
if (!rows || !columns)
|
|
91
|
+
return;
|
|
92
|
+
onInsert(rows, columns);
|
|
93
|
+
};
|
|
94
|
+
return (_jsxs("div", { ref: rootRef, "data-srte-table-size-popover": "true", role: "dialog", "aria-label": "Insert table", style: {
|
|
95
|
+
position: "fixed",
|
|
96
|
+
left: placement?.left ?? x,
|
|
97
|
+
top: placement?.top ?? y,
|
|
98
|
+
visibility: placement ? "visible" : "hidden",
|
|
99
|
+
zIndex: 70,
|
|
100
|
+
width: 232,
|
|
101
|
+
boxSizing: "border-box",
|
|
102
|
+
background: "var(--srte-menu-bg)",
|
|
103
|
+
color: "var(--srte-menu-text)",
|
|
104
|
+
border: "1px solid var(--srte-border)",
|
|
105
|
+
borderRadius: 12,
|
|
106
|
+
boxShadow: "var(--srte-menu-shadow)",
|
|
107
|
+
padding: 14,
|
|
108
|
+
}, children: [_jsx("div", { style: { fontWeight: 650, marginBottom: 10 }, children: "Insert table" }), _jsx("div", { "data-srte-table-size-grid": "true", style: { display: "grid", gridTemplateColumns: `repeat(${MAX_GRID}, 1fr)`, gap: 3, marginBottom: 8 }, onMouseLeave: () => setHover(null), children: Array.from({ length: MAX_GRID * MAX_GRID }, (_, index) => {
|
|
109
|
+
const row = Math.floor(index / MAX_GRID);
|
|
110
|
+
const column = index % MAX_GRID;
|
|
111
|
+
const active = hover ? row <= hover.row && column <= hover.column : false;
|
|
112
|
+
return (_jsx("button", { type: "button", "data-srte-table-size-cell": `${row + 1}x${column + 1}`, "aria-label": `Insert a ${row + 1} by ${column + 1} table`, onMouseEnter: () => setHover({ row, column }), onFocus: () => setHover({ row, column }), onClick: () => onInsert(row + 1, column + 1), style: {
|
|
113
|
+
width: 20, height: 20, padding: 0, boxSizing: "border-box",
|
|
114
|
+
border: `1px solid ${active ? "var(--srte-primary)" : "var(--srte-input-border)"}`,
|
|
115
|
+
borderRadius: 3,
|
|
116
|
+
background: active ? "var(--srte-primary)" : "var(--srte-input-bg)",
|
|
117
|
+
cursor: "pointer",
|
|
118
|
+
} }, index));
|
|
119
|
+
}) }), _jsx("div", { "data-srte-table-size-label": "true", style: { fontSize: 12, marginBottom: 12, minHeight: 16 }, children: hover ? `${hover.row + 1} × ${hover.column + 1} table` : "Hover to choose a size" }), _jsx("div", { style: { fontSize: 12, fontWeight: 600, marginBottom: 6 }, children: "Custom size" }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 12 }, children: [_jsx("input", { ref: rowsRef, type: "number", min: 1, max: 50, "aria-label": "Rows", value: customRows, onChange: (event) => setCustomRows(event.target.value), onKeyDown: (event) => { if (event.key === "Enter") {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
insertCustom();
|
|
122
|
+
} }, style: inputStyle }), _jsx("span", { children: "\u00D7" }), _jsx("input", { type: "number", min: 1, max: 50, "aria-label": "Columns", value: customColumns, onChange: (event) => setCustomColumns(event.target.value), onKeyDown: (event) => { if (event.key === "Enter") {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
insertCustom();
|
|
125
|
+
} }, style: inputStyle }), _jsx("button", { type: "button", onClick: insertCustom, style: { ...buttonStyle, marginLeft: "auto", borderColor: "var(--srte-primary)", background: "var(--srte-primary)", color: "var(--srte-on-primary)" }, children: "Insert" })] }), _jsx("div", { style: { display: "flex", justifyContent: "flex-end" }, children: _jsx("button", { type: "button", onClick: onCancel, style: buttonStyle, children: "Cancel" }) })] }));
|
|
126
|
+
}
|