mardora 1.2.0

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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/dist/chunk-3OCUX4OO.js +7690 -0
  4. package/dist/chunk-3OCUX4OO.js.map +1 -0
  5. package/dist/chunk-3ZOCCFDL.cjs +74 -0
  6. package/dist/chunk-3ZOCCFDL.cjs.map +1 -0
  7. package/dist/chunk-7JOEPNEV.cjs +7740 -0
  8. package/dist/chunk-7JOEPNEV.cjs.map +1 -0
  9. package/dist/chunk-BIKZQZ6W.js +33 -0
  10. package/dist/chunk-BIKZQZ6W.js.map +1 -0
  11. package/dist/chunk-EQJESPP2.js +234 -0
  12. package/dist/chunk-EQJESPP2.js.map +1 -0
  13. package/dist/chunk-G4SE26YY.js +70 -0
  14. package/dist/chunk-G4SE26YY.js.map +1 -0
  15. package/dist/chunk-KNDWF2DP.cjs +35 -0
  16. package/dist/chunk-KNDWF2DP.cjs.map +1 -0
  17. package/dist/chunk-MLBEBFHB.cjs +2971 -0
  18. package/dist/chunk-MLBEBFHB.cjs.map +1 -0
  19. package/dist/chunk-P7JFCYU3.js +905 -0
  20. package/dist/chunk-P7JFCYU3.js.map +1 -0
  21. package/dist/chunk-SWFUKJDO.cjs +243 -0
  22. package/dist/chunk-SWFUKJDO.cjs.map +1 -0
  23. package/dist/chunk-WFVCG4LD.cjs +926 -0
  24. package/dist/chunk-WFVCG4LD.cjs.map +1 -0
  25. package/dist/chunk-XL6WFGJT.js +2901 -0
  26. package/dist/chunk-XL6WFGJT.js.map +1 -0
  27. package/dist/editor/index.cjs +277 -0
  28. package/dist/editor/index.cjs.map +1 -0
  29. package/dist/editor/index.d.cts +186 -0
  30. package/dist/editor/index.d.ts +186 -0
  31. package/dist/editor/index.js +4 -0
  32. package/dist/editor/index.js.map +1 -0
  33. package/dist/index.cjs +405 -0
  34. package/dist/index.cjs.map +1 -0
  35. package/dist/index.d.cts +13 -0
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.js +8 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/lib/index.cjs +12 -0
  40. package/dist/lib/index.cjs.map +1 -0
  41. package/dist/lib/index.d.cts +16 -0
  42. package/dist/lib/index.d.ts +16 -0
  43. package/dist/lib/index.js +3 -0
  44. package/dist/lib/index.js.map +1 -0
  45. package/dist/mardora-DCwjomil.d.cts +640 -0
  46. package/dist/mardora-DCwjomil.d.ts +640 -0
  47. package/dist/plugins/index.cjs +104 -0
  48. package/dist/plugins/index.cjs.map +1 -0
  49. package/dist/plugins/index.d.cts +740 -0
  50. package/dist/plugins/index.d.ts +740 -0
  51. package/dist/plugins/index.js +7 -0
  52. package/dist/plugins/index.js.map +1 -0
  53. package/dist/preview/index.cjs +38 -0
  54. package/dist/preview/index.cjs.map +1 -0
  55. package/dist/preview/index.d.cts +101 -0
  56. package/dist/preview/index.d.ts +101 -0
  57. package/dist/preview/index.js +5 -0
  58. package/dist/preview/index.js.map +1 -0
  59. package/dist/types-NBsaxl4d.d.cts +71 -0
  60. package/dist/types-Pw2SWWAR.d.ts +71 -0
  61. package/package.json +92 -0
  62. package/src/editor/attachments/extension.ts +181 -0
  63. package/src/editor/attachments/format.ts +63 -0
  64. package/src/editor/attachments/index.ts +3 -0
  65. package/src/editor/attachments/types.ts +37 -0
  66. package/src/editor/heading-fold/config.ts +25 -0
  67. package/src/editor/heading-fold/extension.ts +268 -0
  68. package/src/editor/heading-fold/extract.ts +88 -0
  69. package/src/editor/heading-fold/index.ts +5 -0
  70. package/src/editor/heading-fold/theme.ts +85 -0
  71. package/src/editor/heading-fold/types.ts +24 -0
  72. package/src/editor/i18n.ts +13 -0
  73. package/src/editor/icons/index.ts +367 -0
  74. package/src/editor/index.ts +16 -0
  75. package/src/editor/mardora.ts +257 -0
  76. package/src/editor/media-lightbox-theme.ts +146 -0
  77. package/src/editor/media-lightbox.ts +125 -0
  78. package/src/editor/plugin.ts +294 -0
  79. package/src/editor/selection-toolbar/activation.ts +123 -0
  80. package/src/editor/selection-toolbar/commands.ts +279 -0
  81. package/src/editor/selection-toolbar/extension.ts +564 -0
  82. package/src/editor/selection-toolbar/i18n.ts +164 -0
  83. package/src/editor/selection-toolbar/index.ts +7 -0
  84. package/src/editor/selection-toolbar/menu.ts +252 -0
  85. package/src/editor/selection-toolbar/position.ts +43 -0
  86. package/src/editor/selection-toolbar/theme.ts +195 -0
  87. package/src/editor/selection-toolbar/types.ts +155 -0
  88. package/src/editor/slash/default-commands.ts +190 -0
  89. package/src/editor/slash/extension.ts +319 -0
  90. package/src/editor/slash/index.ts +7 -0
  91. package/src/editor/slash/insertions.ts +26 -0
  92. package/src/editor/slash/menu.ts +123 -0
  93. package/src/editor/slash/position.ts +61 -0
  94. package/src/editor/slash/query.ts +33 -0
  95. package/src/editor/slash/theme.ts +113 -0
  96. package/src/editor/slash/types.ts +40 -0
  97. package/src/editor/table-of-contents/extension.ts +202 -0
  98. package/src/editor/table-of-contents/extract.ts +53 -0
  99. package/src/editor/table-of-contents/index.ts +7 -0
  100. package/src/editor/table-of-contents/panel.ts +83 -0
  101. package/src/editor/table-of-contents/slug.ts +50 -0
  102. package/src/editor/table-of-contents/storage.ts +35 -0
  103. package/src/editor/table-of-contents/theme.ts +153 -0
  104. package/src/editor/table-of-contents/types.ts +44 -0
  105. package/src/editor/theme.ts +72 -0
  106. package/src/editor/utils.ts +176 -0
  107. package/src/editor/view-plugin.ts +189 -0
  108. package/src/index.ts +5 -0
  109. package/src/lib/index.ts +2 -0
  110. package/src/lib/input-handler.ts +47 -0
  111. package/src/plugins/code-plugin.theme.ts +545 -0
  112. package/src/plugins/code-plugin.ts +1892 -0
  113. package/src/plugins/emoji-plugin.ts +140 -0
  114. package/src/plugins/heading-plugin.ts +194 -0
  115. package/src/plugins/hr-plugin.ts +102 -0
  116. package/src/plugins/html-plugin.ts +353 -0
  117. package/src/plugins/image-plugin.ts +806 -0
  118. package/src/plugins/index.ts +71 -0
  119. package/src/plugins/inline-plugin.ts +311 -0
  120. package/src/plugins/link-plugin.ts +509 -0
  121. package/src/plugins/list-plugin.ts +492 -0
  122. package/src/plugins/math-plugin.ts +526 -0
  123. package/src/plugins/mermaid-plugin.ts +513 -0
  124. package/src/plugins/paragraph-plugin.ts +38 -0
  125. package/src/plugins/quote-plugin.ts +733 -0
  126. package/src/plugins/table-controls-theme.ts +126 -0
  127. package/src/plugins/table-controls.ts +423 -0
  128. package/src/plugins/table-model.ts +661 -0
  129. package/src/plugins/table-plugin.ts +2111 -0
  130. package/src/preview/context.ts +45 -0
  131. package/src/preview/css-generator.ts +64 -0
  132. package/src/preview/default-renderers.ts +29 -0
  133. package/src/preview/index.ts +29 -0
  134. package/src/preview/preview.ts +41 -0
  135. package/src/preview/renderer.ts +184 -0
  136. package/src/preview/syntax-theme.ts +112 -0
  137. package/src/preview/toc.ts +23 -0
  138. package/src/preview/types.ts +89 -0
@@ -0,0 +1,126 @@
1
+ import { createTheme } from "../editor";
2
+
3
+ export const tableControlsTheme = createTheme({
4
+ default: {
5
+ ".cm-mardora-table-wrapper": {
6
+ overflow: "visible",
7
+ },
8
+ ".cm-mardora-table-controls-overlay": {
9
+ position: "absolute",
10
+ inset: "0",
11
+ pointerEvents: "none",
12
+ zIndex: "20",
13
+ },
14
+ ".cm-mardora-table-handle": {
15
+ position: "absolute",
16
+ width: "1.25rem",
17
+ height: "1.25rem",
18
+ border: "1px solid var(--color-border, #d7dee7)",
19
+ borderRadius: "0.375rem",
20
+ backgroundColor: "var(--color-background, #ffffff)",
21
+ color: "var(--color-muted-foreground, #64748b)",
22
+ display: "inline-flex",
23
+ alignItems: "center",
24
+ justifyContent: "center",
25
+ gap: "0.09375rem",
26
+ pointerEvents: "auto",
27
+ opacity: "1",
28
+ cursor: "pointer",
29
+ padding: "0",
30
+ transition: "background-color 120ms ease, border-color 120ms ease, color 120ms ease",
31
+ },
32
+ ".cm-mardora-table-handle:hover, .cm-mardora-table-handle:focus-visible": {
33
+ borderColor: "rgba(37, 99, 235, 0.55)",
34
+ backgroundColor: "rgba(37, 99, 235, 0.08)",
35
+ color: "rgba(37, 99, 235, 0.95)",
36
+ outline: "none",
37
+ },
38
+ ".cm-mardora-table-handle span": {
39
+ width: "0.1875rem",
40
+ height: "0.1875rem",
41
+ borderRadius: "999px",
42
+ backgroundColor: "currentColor",
43
+ display: "block",
44
+ },
45
+ ".cm-mardora-table-row-handle": {
46
+ flexDirection: "column",
47
+ },
48
+ ".cm-mardora-table-control-menu": {
49
+ position: "absolute",
50
+ minWidth: "12rem",
51
+ padding: "0.25rem",
52
+ border: "1px solid var(--color-border, #d7dee7)",
53
+ borderRadius: "0.5rem",
54
+ backgroundColor: "var(--color-background, #ffffff)",
55
+ boxShadow: "0 18px 44px rgba(15, 23, 42, 0.16)",
56
+ pointerEvents: "auto",
57
+ },
58
+ ".cm-mardora-table-control-menu-item": {
59
+ width: "100%",
60
+ minHeight: "2rem",
61
+ border: "0",
62
+ borderRadius: "0.375rem",
63
+ backgroundColor: "transparent",
64
+ color: "var(--color-text, #0f172a)",
65
+ display: "flex",
66
+ alignItems: "center",
67
+ gap: "0.5rem",
68
+ padding: "0 0.625rem",
69
+ fontSize: "0.875rem",
70
+ textAlign: "left",
71
+ cursor: "pointer",
72
+ },
73
+ ".cm-mardora-table-control-menu-item svg": {
74
+ width: "0.875rem",
75
+ height: "0.875rem",
76
+ flexShrink: "0",
77
+ color: "var(--color-muted-foreground, #64748b)",
78
+ strokeWidth: "2",
79
+ },
80
+ ".cm-mardora-table-control-menu-item span": {
81
+ minWidth: "0",
82
+ flex: "1",
83
+ },
84
+ ".cm-mardora-table-control-menu-item:hover": {
85
+ backgroundColor: "rgba(15, 23, 42, 0.06)",
86
+ },
87
+ ".cm-mardora-table-control-menu-item:disabled": {
88
+ color: "var(--color-muted-foreground, #94a3b8)",
89
+ cursor: "not-allowed",
90
+ },
91
+ ".cm-mardora-table-cell-row-selected, .cm-mardora-table-cell-column-selected": {
92
+ backgroundColor: "rgba(37, 99, 235, 0.12)",
93
+ boxShadow: "inset 0 0 0 1px rgba(37, 99, 235, 0.38)",
94
+ },
95
+ },
96
+ dark: {
97
+ ".cm-mardora-table-handle": {
98
+ borderColor: "var(--color-border, #30363d)",
99
+ backgroundColor: "var(--color-background, #161b22)",
100
+ color: "var(--color-muted-foreground, #94a3b8)",
101
+ },
102
+ ".cm-mardora-table-handle:hover, .cm-mardora-table-handle:focus-visible": {
103
+ borderColor: "rgba(96, 165, 250, 0.68)",
104
+ backgroundColor: "rgba(96, 165, 250, 0.14)",
105
+ color: "rgba(147, 197, 253, 0.98)",
106
+ },
107
+ ".cm-mardora-table-control-menu": {
108
+ borderColor: "var(--color-border, #30363d)",
109
+ backgroundColor: "var(--color-background, #161b22)",
110
+ boxShadow: "0 18px 44px rgba(0, 0, 0, 0.42)",
111
+ },
112
+ ".cm-mardora-table-control-menu-item": {
113
+ color: "var(--color-text, #e6edf3)",
114
+ },
115
+ ".cm-mardora-table-control-menu-item svg": {
116
+ color: "var(--color-muted-foreground, #94a3b8)",
117
+ },
118
+ ".cm-mardora-table-control-menu-item:hover": {
119
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
120
+ },
121
+ ".cm-mardora-table-cell-row-selected, .cm-mardora-table-cell-column-selected": {
122
+ backgroundColor: "rgba(96, 165, 250, 0.18)",
123
+ boxShadow: "inset 0 0 0 1px rgba(96, 165, 250, 0.5)",
124
+ },
125
+ },
126
+ });
@@ -0,0 +1,423 @@
1
+ import { Extension, StateEffect, StateField } from "@codemirror/state";
2
+ import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
3
+ import { createMardoraIcon, type MardoraIconName } from "../editor/icons";
4
+ import type { TableInfo } from "./table-model";
5
+
6
+ export type TableControlKind = "row" | "column";
7
+ export type TableControlAction =
8
+ | "insert-row-above"
9
+ | "insert-row-below"
10
+ | "move-row-up"
11
+ | "move-row-down"
12
+ | "copy-row"
13
+ | "delete-row"
14
+ | "insert-column-left"
15
+ | "insert-column-right"
16
+ | "move-column-left"
17
+ | "move-column-right"
18
+ | "copy-column"
19
+ | "delete-column"
20
+ | "delete-table";
21
+
22
+ export interface ActiveTableControl {
23
+ readonly tableFrom: number;
24
+ readonly kind: TableControlKind;
25
+ readonly rowIndex?: number;
26
+ readonly columnIndex?: number;
27
+ readonly menuOpen: boolean;
28
+ }
29
+
30
+ export interface TableMenuStateInput {
31
+ readonly kind: TableControlKind;
32
+ readonly rowIndex?: number;
33
+ readonly columnIndex?: number;
34
+ readonly bodyRowCount: number;
35
+ readonly columnCount: number;
36
+ }
37
+
38
+ export interface TableControlMenuState {
39
+ readonly canMoveRowUp: boolean;
40
+ readonly canMoveRowDown: boolean;
41
+ readonly canDeleteRow: boolean;
42
+ readonly canMoveColumnLeft: boolean;
43
+ readonly canMoveColumnRight: boolean;
44
+ readonly canDeleteColumn: boolean;
45
+ }
46
+
47
+ export interface TableControlMenuItem {
48
+ readonly action: TableControlAction;
49
+ readonly label: string;
50
+ readonly icon: MardoraIconName;
51
+ readonly enabled: boolean;
52
+ }
53
+
54
+ export function createTableControlMenuState(input: TableMenuStateInput): TableControlMenuState {
55
+ const rowIndex = input.rowIndex ?? 0;
56
+ const columnIndex = input.columnIndex ?? 0;
57
+ return {
58
+ canMoveRowUp: input.kind === "row" && rowIndex > 1,
59
+ canMoveRowDown: input.kind === "row" && rowIndex >= 1 && rowIndex < input.bodyRowCount,
60
+ canDeleteRow: input.kind === "row" && rowIndex >= 1,
61
+ canMoveColumnLeft: input.kind === "column" && columnIndex > 1,
62
+ canMoveColumnRight: input.kind === "column" && columnIndex >= 1 && columnIndex < input.columnCount,
63
+ canDeleteColumn: input.kind === "column" && input.columnCount > 1,
64
+ };
65
+ }
66
+
67
+ export function createTableControlMenuItems(input: TableMenuStateInput): TableControlMenuItem[] {
68
+ const state = createTableControlMenuState(input);
69
+ return input.kind === "row"
70
+ ? [
71
+ { action: "insert-row-above", label: "在上方插入一行", icon: "arrow-up-to-line", enabled: true },
72
+ { action: "insert-row-below", label: "在下方插入一行", icon: "arrow-down-to-line", enabled: true },
73
+ { action: "move-row-up", label: "当前行上移", icon: "arrow-up", enabled: state.canMoveRowUp },
74
+ { action: "move-row-down", label: "当前行下移", icon: "arrow-down", enabled: state.canMoveRowDown },
75
+ { action: "copy-row", label: "拷贝行", icon: "copy", enabled: true },
76
+ { action: "delete-row", label: "删除行", icon: "trash-2", enabled: state.canDeleteRow },
77
+ { action: "delete-table", label: "删除表格", icon: "table-delete", enabled: true },
78
+ ]
79
+ : [
80
+ { action: "insert-column-left", label: "在左侧插入列", icon: "arrow-left-to-line", enabled: true },
81
+ { action: "insert-column-right", label: "在右侧插入列", icon: "arrow-right-to-line", enabled: true },
82
+ { action: "move-column-left", label: "左移列", icon: "arrow-left", enabled: state.canMoveColumnLeft },
83
+ { action: "move-column-right", label: "右移列", icon: "arrow-right", enabled: state.canMoveColumnRight },
84
+ { action: "copy-column", label: "拷贝列", icon: "copy", enabled: true },
85
+ { action: "delete-column", label: "删除列", icon: "trash-2", enabled: state.canDeleteColumn },
86
+ { action: "delete-table", label: "删除表格", icon: "table-delete", enabled: true },
87
+ ];
88
+ }
89
+
90
+ function areTableControlsEqual(left: ActiveTableControl | null, right: ActiveTableControl | null): boolean {
91
+ return (
92
+ left?.tableFrom === right?.tableFrom &&
93
+ left?.kind === right?.kind &&
94
+ left?.rowIndex === right?.rowIndex &&
95
+ left?.columnIndex === right?.columnIndex &&
96
+ left?.menuOpen === right?.menuOpen
97
+ );
98
+ }
99
+
100
+ function isSameTableControlTarget(left: ActiveTableControl | null, right: ActiveTableControl | null): boolean {
101
+ return (
102
+ left?.tableFrom === right?.tableFrom &&
103
+ left?.kind === right?.kind &&
104
+ left?.rowIndex === right?.rowIndex &&
105
+ left?.columnIndex === right?.columnIndex
106
+ );
107
+ }
108
+
109
+ export function resolveTableControlHandleClick(
110
+ current: ActiveTableControl | null,
111
+ target: ActiveTableControl
112
+ ): ActiveTableControl | null {
113
+ if (current?.menuOpen && isSameTableControlTarget(current, target)) {
114
+ return null;
115
+ }
116
+ return { ...target, menuOpen: true };
117
+ }
118
+
119
+ export const setActiveTableControlEffect = StateEffect.define<ActiveTableControl | null>();
120
+
121
+ export function hasTableControlStateChange(effects: readonly StateEffect<unknown>[]): boolean {
122
+ return effects.some((effect) => effect.is(setActiveTableControlEffect));
123
+ }
124
+
125
+ export const activeTableControlField = StateField.define<ActiveTableControl | null>({
126
+ create: () => null,
127
+ update(value, transaction) {
128
+ for (const effect of transaction.effects) {
129
+ if (effect.is(setActiveTableControlEffect)) {
130
+ return effect.value;
131
+ }
132
+ }
133
+ if (transaction.docChanged) {
134
+ return null;
135
+ }
136
+ return value;
137
+ },
138
+ });
139
+
140
+ export interface TableControlsConfig {
141
+ readonly getTableInfoByFrom: (view: EditorView, tableFrom: number) => TableInfo | null;
142
+ readonly runAction: (view: EditorView, control: ActiveTableControl, action: TableControlAction) => void;
143
+ }
144
+
145
+ export function tableControls(config: TableControlsConfig): Extension[] {
146
+ const plugin = ViewPlugin.define((view) => new TableControlsView(view, config));
147
+ return [
148
+ activeTableControlField,
149
+ plugin,
150
+ EditorView.domEventHandlers({
151
+ keydown(event, view) {
152
+ if (event.key !== "Escape" || !view.state.field(activeTableControlField, false)) {
153
+ return false;
154
+ }
155
+ view.dispatch({ effects: setActiveTableControlEffect.of(null) });
156
+ event.preventDefault();
157
+ return true;
158
+ },
159
+ }),
160
+ ];
161
+ }
162
+
163
+ class TableControlsView {
164
+ private readonly overlay: HTMLElement;
165
+ private hoverControl: ActiveTableControl | null = null;
166
+
167
+ constructor(
168
+ private readonly view: EditorView,
169
+ private readonly config: TableControlsConfig
170
+ ) {
171
+ this.overlay = view.dom.ownerDocument.createElement("div");
172
+ this.overlay.className = "cm-mardora-table-controls-overlay";
173
+ this.overlay.addEventListener("mousedown", this.handleOverlayMouseDown);
174
+ view.dom.appendChild(this.overlay);
175
+ view.dom.addEventListener("mousemove", this.handleMouseMove);
176
+ view.dom.addEventListener("mouseleave", this.handleMouseLeave);
177
+ view.dom.ownerDocument.addEventListener("mousedown", this.handleDocumentMouseDown, true);
178
+ this.render();
179
+ }
180
+
181
+ update(update: ViewUpdate): void {
182
+ const controlStateChanged = update.transactions.some((transaction) => hasTableControlStateChange(transaction.effects));
183
+ if (
184
+ update.docChanged ||
185
+ update.selectionSet ||
186
+ update.viewportChanged ||
187
+ update.geometryChanged ||
188
+ controlStateChanged
189
+ ) {
190
+ this.render();
191
+ }
192
+ }
193
+
194
+ destroy(): void {
195
+ this.view.dom.removeEventListener("mousemove", this.handleMouseMove);
196
+ this.view.dom.removeEventListener("mouseleave", this.handleMouseLeave);
197
+ this.view.dom.ownerDocument.removeEventListener("mousedown", this.handleDocumentMouseDown, true);
198
+ this.overlay.removeEventListener("mousedown", this.handleOverlayMouseDown);
199
+ this.overlay.remove();
200
+ }
201
+
202
+ private readonly handleMouseMove = (event: MouseEvent): void => {
203
+ if (event.target instanceof Element && event.target.closest(".cm-mardora-table-controls-overlay")) {
204
+ return;
205
+ }
206
+
207
+ const cell = event.target instanceof Element ? event.target.closest(".cm-mardora-table-cell") : null;
208
+ if (!cell || !this.view.dom.contains(cell)) {
209
+ if (!this.currentControl()?.menuOpen) {
210
+ this.setHoverControl(null);
211
+ }
212
+ return;
213
+ }
214
+
215
+ const tableFrom = Number(cell.getAttribute("data-mardora-table-from"));
216
+ const rowIndex = Number(cell.getAttribute("data-mardora-row-index"));
217
+ const columnIndex = Number(cell.getAttribute("data-mardora-column-index"));
218
+ const rowKind = cell.getAttribute("data-mardora-row-kind");
219
+ if (!Number.isFinite(tableFrom) || !Number.isFinite(rowIndex) || !Number.isFinite(columnIndex)) {
220
+ return;
221
+ }
222
+
223
+ this.setHoverControl(
224
+ rowKind === "body"
225
+ ? { tableFrom, kind: "row", rowIndex, columnIndex, menuOpen: false }
226
+ : { tableFrom, kind: "column", columnIndex, menuOpen: false }
227
+ );
228
+ };
229
+
230
+ private readonly handleMouseLeave = (): void => {
231
+ if (this.currentControl()?.menuOpen) {
232
+ return;
233
+ }
234
+ this.setHoverControl(null);
235
+ };
236
+
237
+ private readonly handleDocumentMouseDown = (event: MouseEvent): void => {
238
+ if (event.target instanceof Node && this.overlay.contains(event.target)) {
239
+ return;
240
+ }
241
+ if (this.view.state.field(activeTableControlField, false)) {
242
+ this.view.dispatch({ effects: setActiveTableControlEffect.of(null) });
243
+ }
244
+ };
245
+
246
+ private readonly handleOverlayMouseDown = (event: MouseEvent): void => {
247
+ const target = event.target instanceof Element ? event.target : null;
248
+ const button = target?.closest<HTMLElement>("[data-mardora-table-control-action]");
249
+ if (!button) {
250
+ return;
251
+ }
252
+ event.preventDefault();
253
+ event.stopPropagation();
254
+
255
+ const control = this.currentControl();
256
+ if (!control) {
257
+ return;
258
+ }
259
+
260
+ const action = button.dataset.mardoraTableControlAction || "";
261
+ if (button.getAttribute("aria-disabled") === "true") {
262
+ return;
263
+ }
264
+
265
+ if (action === "open-row-menu") {
266
+ if (control.rowIndex === undefined || control.rowIndex <= 0) {
267
+ return;
268
+ }
269
+ const activeControl = this.view.state.field(activeTableControlField, false) ?? null;
270
+ this.view.dispatch({
271
+ effects: setActiveTableControlEffect.of(
272
+ resolveTableControlHandleClick(activeControl, {
273
+ tableFrom: control.tableFrom,
274
+ kind: "row",
275
+ rowIndex: control.rowIndex,
276
+ ...(control.columnIndex === undefined ? {} : { columnIndex: control.columnIndex }),
277
+ menuOpen: false,
278
+ })
279
+ ),
280
+ });
281
+ return;
282
+ }
283
+
284
+ if (action === "open-column-menu") {
285
+ if (control.columnIndex === undefined) {
286
+ return;
287
+ }
288
+ const activeControl = this.view.state.field(activeTableControlField, false) ?? null;
289
+ this.view.dispatch({
290
+ effects: setActiveTableControlEffect.of(
291
+ resolveTableControlHandleClick(activeControl, {
292
+ tableFrom: control.tableFrom,
293
+ kind: "column",
294
+ columnIndex: control.columnIndex,
295
+ menuOpen: false,
296
+ })
297
+ ),
298
+ });
299
+ return;
300
+ }
301
+
302
+ this.config.runAction(this.view, control, action as TableControlAction);
303
+ this.hoverControl = null;
304
+ this.view.dispatch({ effects: setActiveTableControlEffect.of(null) });
305
+ };
306
+
307
+ private currentControl(): ActiveTableControl | null {
308
+ return this.view.state.field(activeTableControlField, false) ?? this.hoverControl ?? null;
309
+ }
310
+
311
+ private setHoverControl(control: ActiveTableControl | null): void {
312
+ if (areTableControlsEqual(this.hoverControl, control)) {
313
+ return;
314
+ }
315
+ this.hoverControl = control;
316
+ this.render();
317
+ }
318
+
319
+ private render(): void {
320
+ const control = this.currentControl();
321
+ this.overlay.replaceChildren();
322
+ if (!control) {
323
+ return;
324
+ }
325
+
326
+ const table = this.config.getTableInfoByFrom(this.view, control.tableFrom);
327
+ if (!table) {
328
+ return;
329
+ }
330
+
331
+ const rowButton = control.rowIndex !== undefined && control.rowIndex > 0 ? this.createHandle("row") : null;
332
+ const columnButton = control.columnIndex !== undefined ? this.createHandle("column") : null;
333
+ if (rowButton) this.overlay.appendChild(rowButton);
334
+ if (columnButton) this.overlay.appendChild(columnButton);
335
+ if (control.menuOpen) this.overlay.appendChild(this.createMenu(control, table));
336
+ this.positionOverlay(control, table);
337
+ }
338
+
339
+ private createHandle(kind: TableControlKind): HTMLButtonElement {
340
+ const button = this.view.dom.ownerDocument.createElement("button");
341
+ button.type = "button";
342
+ button.className = `cm-mardora-table-handle cm-mardora-table-${kind}-handle`;
343
+ button.dataset.mardoraTableControlAction = kind === "row" ? "open-row-menu" : "open-column-menu";
344
+ button.setAttribute("aria-label", kind === "row" ? "Table row actions" : "Table column actions");
345
+ button.innerHTML = "<span></span><span></span><span></span>";
346
+ return button;
347
+ }
348
+
349
+ private createMenu(control: ActiveTableControl, table: TableInfo): HTMLElement {
350
+ const input: TableMenuStateInput = {
351
+ kind: control.kind,
352
+ bodyRowCount: table.bodyCells.length,
353
+ columnCount: table.columnCount,
354
+ ...(control.rowIndex === undefined ? {} : { rowIndex: control.rowIndex }),
355
+ ...(control.columnIndex === undefined ? {} : { columnIndex: control.columnIndex + 1 }),
356
+ };
357
+ const menu = this.view.dom.ownerDocument.createElement("div");
358
+ menu.className = "cm-mardora-table-control-menu";
359
+ menu.setAttribute("role", "menu");
360
+
361
+ for (const { action, label, icon, enabled } of createTableControlMenuItems(input)) {
362
+ const item = this.view.dom.ownerDocument.createElement("button");
363
+ item.type = "button";
364
+ item.className = "cm-mardora-table-control-menu-item";
365
+ item.dataset.mardoraTableControlAction = action;
366
+ item.setAttribute("role", "menuitem");
367
+ item.setAttribute("aria-disabled", enabled ? "false" : "true");
368
+ if (!enabled) item.disabled = true;
369
+ const svg = createMardoraIcon(icon);
370
+ if (svg) item.appendChild(svg);
371
+ const text = this.view.dom.ownerDocument.createElement("span");
372
+ text.textContent = label;
373
+ item.appendChild(text);
374
+ menu.appendChild(item);
375
+ }
376
+ return menu;
377
+ }
378
+
379
+ private positionOverlay(control: ActiveTableControl, table: TableInfo): void {
380
+ this.view.requestMeasure({
381
+ read: () => {
382
+ const editorRect = this.view.dom.getBoundingClientRect();
383
+ const rowRect =
384
+ control.rowIndex !== undefined && control.rowIndex > 0
385
+ ? this.findCellElement(table.from, control.rowIndex, 0)?.getBoundingClientRect() ?? null
386
+ : null;
387
+ const columnRect =
388
+ control.columnIndex !== undefined
389
+ ? this.findCellElement(table.from, 0, control.columnIndex)?.getBoundingClientRect() ?? null
390
+ : null;
391
+ return { editorRect, rowRect, columnRect };
392
+ },
393
+ write: (measure) => {
394
+ const rowHandle = this.overlay.querySelector<HTMLElement>(".cm-mardora-table-row-handle");
395
+ const columnHandle = this.overlay.querySelector<HTMLElement>(".cm-mardora-table-column-handle");
396
+ if (rowHandle && measure.rowRect) {
397
+ rowHandle.style.left = `${measure.rowRect.left - measure.editorRect.left - 10}px`;
398
+ rowHandle.style.top = `${measure.rowRect.top - measure.editorRect.top + measure.rowRect.height / 2 - 10}px`;
399
+ }
400
+ if (columnHandle && measure.columnRect) {
401
+ columnHandle.style.left = `${measure.columnRect.left - measure.editorRect.left + measure.columnRect.width / 2 - 10}px`;
402
+ columnHandle.style.top = `${measure.columnRect.top - measure.editorRect.top - 10}px`;
403
+ }
404
+ const menu = this.overlay.querySelector<HTMLElement>(".cm-mardora-table-control-menu");
405
+ const anchor = control.kind === "row" ? rowHandle : columnHandle;
406
+ if (menu && anchor) {
407
+ menu.style.left = `${Number.parseFloat(anchor.style.left) + 28}px`;
408
+ menu.style.top = `${Number.parseFloat(anchor.style.top)}px`;
409
+ }
410
+ },
411
+ });
412
+ }
413
+
414
+ private findCellElement(tableFrom: number, rowIndex: number, columnIndex: number): HTMLElement | null {
415
+ const selector = [
416
+ ".cm-mardora-table-cell",
417
+ `[data-mardora-table-from="${tableFrom}"]`,
418
+ `[data-mardora-row-index="${rowIndex}"]`,
419
+ `[data-mardora-column-index="${columnIndex}"]`,
420
+ ].join("");
421
+ return this.view.dom.querySelector<HTMLElement>(selector);
422
+ }
423
+ }