@componyx/ui 0.0.0-stage → 1.0.4

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 (169) hide show
  1. package/AppSkin/AppSkin.css +1268 -0
  2. package/AppSkin/AppSkin.js +2748 -0
  3. package/AppSkin/Themes/Default.css +241 -0
  4. package/Base/Base.js +3407 -0
  5. package/Base/ComponentIcons.css +66 -0
  6. package/Base/Font/ComponentIcons.woff +0 -0
  7. package/Base/Font/UI.woff +0 -0
  8. package/Base/Library.js +8432 -0
  9. package/Base/NavigationManager.js +462 -0
  10. package/Base/ResponsiveTemplates.js +371 -0
  11. package/Base/Sanitizer.js +288 -0
  12. package/Base/Themes/Default.css +255 -0
  13. package/Base/UI.css +618 -0
  14. package/Base/UIResourcePath.js +6 -0
  15. package/Bindary/Bindary.js +2011 -0
  16. package/Bindary/Modules/Connection.js +216 -0
  17. package/Bindary/Modules/Core.js +854 -0
  18. package/Bindary/Modules/DataUpdater.js +351 -0
  19. package/Bindary/Modules/ExpressionEngine.js +347 -0
  20. package/Bindary/Modules/ProxyManager.js +482 -0
  21. package/Bindary/Modules/Renderer.js +605 -0
  22. package/Bindary/Modules/Router.js +291 -0
  23. package/Bindary/Modules/Scroller.js +89 -0
  24. package/Bindary/Modules/Template.js +1273 -0
  25. package/BookmarkSpy/BookmarkSpy.js +430 -0
  26. package/Box/Box.css +36 -0
  27. package/Box/Box.js +1685 -0
  28. package/Button/Button.css +470 -0
  29. package/Button/Button.js +1398 -0
  30. package/Button/Themes/Default.css +200 -0
  31. package/Carousel/Carousel.css +273 -0
  32. package/Carousel/Carousel.js +607 -0
  33. package/ColorButton/ColorButton.css +39 -0
  34. package/ColorButton/ColorButton.js +336 -0
  35. package/ColorPicker/ColorPicker.css +359 -0
  36. package/ColorPicker/ColorPicker.js +972 -0
  37. package/ComboBox/ComboBox.css +357 -0
  38. package/ComboBox/ComboBox.js +2342 -0
  39. package/ComboBox/Themes/Default.css +23 -0
  40. package/DataPager/DataPager.css +72 -0
  41. package/DataPager/DataPager.js +674 -0
  42. package/DatePicker/DatePicker.css +227 -0
  43. package/DatePicker/DatePicker.js +1531 -0
  44. package/DatePicker/Themes/Default.css +26 -0
  45. package/Dialog/Dialog.css +106 -0
  46. package/Dialog/Dialog.js +572 -0
  47. package/Dialog/Themes/Default.css +10 -0
  48. package/Editor/Data/Emoji.json +8285 -0
  49. package/Editor/Data/Special.json +1042 -0
  50. package/Editor/Editor.css +1562 -0
  51. package/Editor/Editor.js +4058 -0
  52. package/Editor/Font/Editor.woff +0 -0
  53. package/Editor/Modules/ComponentFactory.js +565 -0
  54. package/Editor/Modules/ContentManager.js +748 -0
  55. package/Editor/Modules/DialogManager.js +1234 -0
  56. package/Editor/Modules/EventManager.js +458 -0
  57. package/Editor/Modules/Format.js +950 -0
  58. package/Editor/Modules/History.js +191 -0
  59. package/Editor/Modules/LayoutState.js +379 -0
  60. package/Editor/Modules/ListManager.js +296 -0
  61. package/Editor/Modules/MenuManager.js +388 -0
  62. package/Editor/Modules/NodeManager.js +681 -0
  63. package/Editor/Modules/ParagraphButtons.js +122 -0
  64. package/Editor/Modules/Resizer.js +176 -0
  65. package/Editor/Modules/SelectionRange.js +564 -0
  66. package/Editor/Modules/SourceViewBuilder.js +212 -0
  67. package/Editor/Modules/SymbolPicker.js +208 -0
  68. package/Editor/Modules/Table.js +2327 -0
  69. package/Editor/Modules/TableDialog.js +757 -0
  70. package/Editor/Modules/Toolbar.js +375 -0
  71. package/Editor/Modules/TooltipMenu.js +125 -0
  72. package/Editor/Modules/Utility.js +131 -0
  73. package/Editor/Themes/Default.css +204 -0
  74. package/FileUpload/FileUpload.css +156 -0
  75. package/FileUpload/FileUpload.js +898 -0
  76. package/FileUpload/Themes/Default.css +17 -0
  77. package/Form/Form.css +1934 -0
  78. package/Form/Form.js +4943 -0
  79. package/Form/Modules/ActionManager.js +368 -0
  80. package/Form/Modules/BuildPanelManager.js +319 -0
  81. package/Form/Modules/ComponentFactory.js +468 -0
  82. package/Form/Modules/ComponentPanel.js +265 -0
  83. package/Form/Modules/ConfigPanelManager.js +685 -0
  84. package/Form/Modules/DataBinder.js +602 -0
  85. package/Form/Modules/DataObserver.js +717 -0
  86. package/Form/Modules/Draggable.js +756 -0
  87. package/Form/Modules/OptionsPanel.js +342 -0
  88. package/Form/Modules/Renderer.js +1889 -0
  89. package/Form/Modules/RuleEngine.js +585 -0
  90. package/Form/Modules/RulesPanel.js +1158 -0
  91. package/Form/Modules/Types.js +1709 -0
  92. package/Form/Modules/ValidationManager.js +194 -0
  93. package/Form/Themes/Default.css +216 -0
  94. package/FormField/FormField.css +468 -0
  95. package/FormField/FormField.js +576 -0
  96. package/FormField/Themes/Default.css +99 -0
  97. package/Grid/Grid.css +544 -0
  98. package/Grid/Grid.js +3761 -0
  99. package/Grid/Themes/Default.css +43 -0
  100. package/LICENSE +126 -0
  101. package/MaskedTextBox/MaskedTextBox.css +28 -0
  102. package/MaskedTextBox/MaskedTextBox.js +642 -0
  103. package/MaskedTextBox/Themes/Default.css +6 -0
  104. package/Menu/Menu.css +328 -0
  105. package/Menu/Menu.js +2386 -0
  106. package/Menu/Themes/Default.css +24 -0
  107. package/NumericBox/NumericBox.css +120 -0
  108. package/NumericBox/NumericBox.js +984 -0
  109. package/PanelBar/PanelBar.css +123 -0
  110. package/PanelBar/PanelBar.js +505 -0
  111. package/PanelBar/Themes/Default.css +51 -0
  112. package/PieChart/PieChart.css +95 -0
  113. package/PieChart/PieChart.js +820 -0
  114. package/PieChart/Themes/Default.css +9 -0
  115. package/README.md +178 -3
  116. package/Slider/Slider.css +348 -0
  117. package/Slider/Slider.js +1306 -0
  118. package/Slider/Themes/Default.css +44 -0
  119. package/TabStrip/TabStrip.css +52 -0
  120. package/TabStrip/TabStrip.js +362 -0
  121. package/TabStrip/Themes/Default.css +35 -0
  122. package/TimePicker/Themes/Default.css +30 -0
  123. package/TimePicker/TimePicker.css +253 -0
  124. package/TimePicker/TimePicker.js +1485 -0
  125. package/TooltipManager/Themes/Default.css +21 -0
  126. package/TooltipManager/TooltipManager.css +227 -0
  127. package/TooltipManager/TooltipManager.js +791 -0
  128. package/Validator/Validator.css +54 -0
  129. package/Validator/Validator.js +2508 -0
  130. package/componyx.html-data.json +619 -0
  131. package/index.js +27 -0
  132. package/package.json +43 -4
  133. package/types/AppSkin.d.ts +842 -0
  134. package/types/Base.d.ts +1034 -0
  135. package/types/Bindary.d.ts +1196 -0
  136. package/types/BookmarkSpy.d.ts +112 -0
  137. package/types/Box.d.ts +374 -0
  138. package/types/Button.d.ts +455 -0
  139. package/types/Carousel.d.ts +136 -0
  140. package/types/ColorButton.d.ts +120 -0
  141. package/types/ColorPicker.d.ts +225 -0
  142. package/types/ComboBox.d.ts +507 -0
  143. package/types/DataPager.d.ts +152 -0
  144. package/types/DatePicker.d.ts +354 -0
  145. package/types/Dialog.d.ts +160 -0
  146. package/types/Editor.d.ts +1289 -0
  147. package/types/FileUpload.d.ts +206 -0
  148. package/types/Form.d.ts +3078 -0
  149. package/types/FormField.d.ts +204 -0
  150. package/types/Grid.d.ts +911 -0
  151. package/types/Library.d.ts +1899 -0
  152. package/types/MaskedTextBox.d.ts +91 -0
  153. package/types/Menu.d.ts +577 -0
  154. package/types/NumericBox.d.ts +310 -0
  155. package/types/PanelBar.d.ts +223 -0
  156. package/types/PieChart.d.ts +224 -0
  157. package/types/ResponsiveTemplates.d.ts +108 -0
  158. package/types/Slider.d.ts +227 -0
  159. package/types/TabStrip.d.ts +185 -0
  160. package/types/TimePicker.d.ts +265 -0
  161. package/types/TooltipManager.d.ts +178 -0
  162. package/types/UI.d.ts +29 -0
  163. package/types/Validator.d.ts +653 -0
  164. package/types/index.d.ts +59 -0
  165. package/ui.css +11783 -0
  166. package/ui.js +79477 -0
  167. package/ui.min.css +138 -0
  168. package/ui.min.js +264 -0
  169. package/ui.min.js.map +1 -0
@@ -0,0 +1,2327 @@
1
+ /*! Componyx.UI | (c) E.H. Daanen / Componyx | BUSL-1.1 | componyx.com/license */
2
+ componyx.UI.editor_modules = componyx.UI.editor_modules || {};
3
+
4
+ /**
5
+ * Table module — manages table editing.
6
+ *
7
+ * @class Table
8
+ * @memberof componyx.UI.editor_modules
9
+ * @ignore
10
+ */
11
+ componyx.UI.editor_modules.Table = class Table
12
+ {
13
+ /**
14
+ * @type {Set<HTMLTableCellElement>}
15
+ * @ignore
16
+ */
17
+ #selectedCells = new Set();
18
+ /**
19
+ * @type {HTMLTableCellElement}
20
+ * @ignore
21
+ */
22
+ #anchorCell = null;
23
+ #isSelecting = false;
24
+ #tableObservers = new WeakMap();
25
+ #dragging = null;
26
+ #rafId = null;
27
+ #lastEvent = null;
28
+ #pickerCssClass;
29
+ #pickerGridCssClass;
30
+ #pickerLabelCssClass;
31
+ #wrapperCssClass;
32
+ #selectedCssClass;
33
+ #anchorCssClass;
34
+ #colHandleCssClass;
35
+ #colHandleActiveCssClass;
36
+ #menuButtonCssClass;
37
+ #activeMenuButton;
38
+ #contextCell;
39
+
40
+ constructor(editor)
41
+ {
42
+ this.editor = editor;
43
+ this.cf = editor.componentFactory;
44
+ this.utility = editor.utility;
45
+ this.#pickerCssClass = editor.getCssClass(editor.classOption.TABLE_PICKER);
46
+ this.#pickerGridCssClass = editor.getCssClass(editor.classOption.TABLE_PICKER_GRID);
47
+ this.#pickerLabelCssClass = editor.getCssClass(editor.classOption.TABLE_PICKER_LABEL);
48
+ this.#wrapperCssClass = editor.getCssClass(editor.classOption.TABLE_WRAPPER);
49
+ this.#anchorCssClass = editor.getCssClass(editor.classOption.TABLE_ANCHOR);
50
+ this.#selectedCssClass = editor.getCssClass(editor.classOption.TABLE_SELECTED);
51
+ this.#colHandleCssClass = editor.getCssClass(editor.classOption.TABLE_COL_HANDLE);
52
+ this.#colHandleActiveCssClass = editor.getCssClass(editor.classOption.TABLE_COL_HANDLE_ACTIVE);
53
+ this.#menuButtonCssClass = editor.getCssClass(editor.classOption.TABLE_MENU_BUTTON);
54
+ this.menu = null;
55
+ this.pickerBox = null;
56
+
57
+ this.editor.events.onUndo.priorityAdd(() => { this.afterHistoryAction(); });
58
+ this.editor.events.onRedo.priorityAdd(() => { this.afterHistoryAction(); });
59
+ }
60
+
61
+ bindEvents()
62
+ {
63
+ if (!this.editor.getEditorElement())
64
+ return;
65
+
66
+ const el = this.editor.getEditorElement(),
67
+ doc = this.editor.getDoc();
68
+
69
+ $lib.on(el, 'cut', this.#cut, null, this, true);
70
+ $lib.on(el, 'copy', this.#copy, null, this, true);
71
+ $lib.on(el, 'paste', this.#paste, null, this, true);
72
+ $lib.on(el, 'pointerdown', this.#pointerDown, null, this, true);
73
+ $lib.on(el, 'pointerover', this.#pointerOver, null, this, true);
74
+ $lib.on(el, 'pointermove', this.#pointerMove, null, this, true);
75
+ $lib.on(el, 'keydown', this.#keyDown, null, this, true);
76
+ $lib.on(el, 'contextmenu', this.#contextMenu, null, this, true);
77
+
78
+ // Resizing cols
79
+ $lib.on(doc, 'selectionchange', this.#selectionChange, null, this, true);
80
+ $lib.on(doc, 'pointerdown', this.removeMenuButton, null, this, true);
81
+ $lib.on(doc, 'pointerdown pointerup', this.#resizeEnd, null, this, true);
82
+ $lib.on(doc, 'pointerup', this.#pointerUp, null, this, true);
83
+ $lib.on(doc, 'pointermove', this.#scheduleResize, null, this, true);
84
+ }
85
+
86
+ disposeEvents()
87
+ {
88
+ if (!this.editor.getEditorElement())
89
+ return;
90
+
91
+ const el = this.editor.getEditorElement(),
92
+ doc = this.editor.getDoc();
93
+
94
+ $lib.off(el, 'cut', this.#cut);
95
+ $lib.off(el, 'copy', this.#copy);
96
+ $lib.off(el, 'paste', this.#paste);
97
+ $lib.off(el, 'pointerdown', this.#pointerDown);
98
+ $lib.off(el, 'pointerover', this.#pointerOver);
99
+ $lib.off(el, 'pointermove', this.#pointerMove);
100
+ $lib.off(el, 'pointerup', this.#pointerUp);
101
+ $lib.off(el, 'keydown', this.#keyDown);
102
+ $lib.off(el, 'contextmenu', this.#contextMenu);
103
+
104
+ $lib.off(doc, 'selectionchange', this.#selectionChange);
105
+ $lib.off(doc, 'pointerdown', this.removeMenuButton);
106
+ $lib.off(doc, 'pointerdown pointerup', this.#resizeEnd);
107
+ $lib.off(doc, 'pointerup', this.#pointerUp);
108
+ $lib.off(doc, 'pointermove', this.#scheduleResize);
109
+
110
+ this.clearSelection();
111
+ }
112
+
113
+ destroy()
114
+ {
115
+ this.disposeEvents();
116
+ this.editor.events.onUndo.priorityRemove(2);
117
+ this.editor.events.onRedo.priorityRemove(2);
118
+ }
119
+
120
+ clearAll(source, clearWrapperCssClass = false)
121
+ {
122
+ const contentEl = source || this.editor.getEditorElement();
123
+ if (!contentEl)
124
+ return;
125
+
126
+ // Remove col handles and menu buttons from all table wrappers
127
+ contentEl.querySelectorAll(`figure.${this.#wrapperCssClass}`).forEach(wrapper =>
128
+ {
129
+ wrapper.querySelectorAll(`:scope > .${this.#colHandleCssClass}`).forEach(h => h.remove());
130
+ wrapper.querySelectorAll(`:scope > a.${this.#menuButtonCssClass}`).forEach(b => b.remove());
131
+
132
+ if (clearWrapperCssClass)
133
+ {
134
+ wrapper.classList.remove(this.#wrapperCssClass);
135
+ if (!wrapper.className)
136
+ wrapper.removeAttribute('class');
137
+ }
138
+ });
139
+
140
+ // Strip selection classes from all cells
141
+ contentEl.querySelectorAll('td, th').forEach(cell =>
142
+ {
143
+ cell.classList.remove(this.#selectedCssClass, this.#anchorCssClass);
144
+ if (!cell.className)
145
+ cell.removeAttribute('class');
146
+ });
147
+ }
148
+
149
+ clearSelection()
150
+ {
151
+ this.#clearHighlights();
152
+ this.#selectedCells = new Set();
153
+ this.#anchorCell = null;
154
+ this.#contextCell = null;
155
+ this.removeMenuButton();
156
+
157
+ if (this.menu)
158
+ this.menu.collapseAll(true);
159
+ }
160
+
161
+ updateContextCell(cell)
162
+ {
163
+ this.#contextCell = cell;
164
+ }
165
+
166
+ afterHistoryAction()
167
+ {
168
+ this.clearAll();
169
+
170
+ this.editor.getEditorElement().querySelectorAll('table').forEach(t =>
171
+ {
172
+ this.#rebuildColgroup(t);
173
+ this.#buildHandles(t);
174
+ });
175
+ }
176
+
177
+ createPicker()
178
+ {
179
+ const content = this.createPickerBox(),
180
+ grid = $lib.element({ container: content, attrs: { class: this.#pickerGridCssClass } }),
181
+ label = $lib.element({ container: content, content: '0 x 0', attrs: { class: this.#pickerLabelCssClass } });
182
+
183
+ for (let index = 0; index < 100; index++)
184
+ {
185
+ const c = $lib.element();
186
+ c.dataset.row = Math.floor(index / 10) + 1;
187
+ c.dataset.col = (index % 10) + 1;
188
+ grid.appendChild(c);
189
+ }
190
+
191
+ $lib.on(grid, 'pointerover', e =>
192
+ {
193
+ const c = e.target.closest('[data-row]');
194
+ if (!c) return;
195
+
196
+ const r = +c.dataset.row, cl = +c.dataset.col; // convert strs to integers
197
+ [...grid.children].forEach(x => x.classList.toggle(this.#selectedCssClass, +x.dataset.row <= r && +x.dataset.col <= cl)); // force toggle on/off
198
+ label.textContent = `${r} × ${cl}`;
199
+ });
200
+
201
+ $lib.on(grid, 'click', e =>
202
+ {
203
+ const c = e.target.closest('[data-row]');
204
+ if (!c) return;
205
+
206
+ this.insertTable(+c.dataset.row, +c.dataset.col);
207
+ this.pickerBox.events.onHideComplete.add(() =>
208
+ {
209
+ [...grid.children].forEach(x => x.classList.remove(this.#selectedCssClass));
210
+ label.textContent = `0 × 0`;
211
+ });
212
+ this.pickerBox.hide();
213
+ });
214
+ }
215
+
216
+ createPickerBox()
217
+ {
218
+ const editor = this.editor,
219
+ content = $lib.element();
220
+
221
+ this.pickerBox = this.cf.createBox('TablePicker', editor.tablePickerBoxId, editor.element, { cssClass: this.#pickerCssClass, content: content });
222
+
223
+ return content;
224
+ }
225
+
226
+ showMenuButton(cell)
227
+ {
228
+ let wrapperEl = cell.closest('table').closest(`figure.${this.#wrapperCssClass}`),
229
+ button = wrapperEl.querySelector(`a.${this.#menuButtonCssClass}`);
230
+
231
+ if (!button)
232
+ {
233
+ const buttonCss = `${this.utility.buttonLucentCss} ${this.editor.getThemeCSS()} ${this.#menuButtonCssClass} ico-cog`;
234
+ button = $lib.element({ container: wrapperEl, tag: 'a', props: { className: buttonCss, contentEditable: false } });
235
+
236
+ $lib.on(button, 'click', this.#showMenu, null, this);
237
+ }
238
+
239
+ this.#contextCell = cell; // capture before focus can shift
240
+ this.#activeMenuButton = button;
241
+ this.positionMenuButton(cell);
242
+ }
243
+
244
+ positionMenuButton(cell)
245
+ {
246
+ const btn = this.#activeMenuButton;
247
+ if (!btn) return;
248
+
249
+ const wrapperEl = btn.closest(`figure.${this.#wrapperCssClass}`),
250
+ wrapperRect = wrapperEl.getBoundingClientRect(),
251
+ cellRect = cell.getBoundingClientRect(),
252
+ btnWidth = btn.offsetWidth || 30,
253
+ btnHeight = btn.offsetHeight || 30;
254
+
255
+ btn.style.left = `${cellRect.right - wrapperRect.left - btnWidth}px`;
256
+ btn.style.top = `${cellRect.top - wrapperRect.top - btnHeight}px`;
257
+ btn.style.display = '';
258
+ }
259
+
260
+ hideMenuButton()
261
+ {
262
+ if (!this.#activeMenuButton)
263
+ return;
264
+
265
+ this.#activeMenuButton.style.display = 'none';
266
+ }
267
+
268
+ removeMenuButton(e)
269
+ {
270
+ if (!this.#activeMenuButton || this.#dragging)
271
+ return;
272
+
273
+ const target = e?.target;
274
+
275
+ if (!target) // No target (manual call) -> remove
276
+ {
277
+ this.#activeMenuButton.remove();
278
+ this.#activeMenuButton = null;
279
+ return;
280
+ }
281
+
282
+ // If clicking on the menu button itself, keep it
283
+ if (target.closest?.(`a.${this.#menuButtonCssClass}`))
284
+ return;
285
+
286
+ const clickedCell = target.closest?.('td,th');
287
+
288
+ // Clicked inside the same table -> keep button
289
+ if (clickedCell)
290
+ {
291
+ const currentWrapper = this.#activeMenuButton.closest(`figure.${this.#wrapperCssClass}`);
292
+ const clickedWrapper = clickedCell.closest(`figure.${this.#wrapperCssClass}`);
293
+
294
+ if (currentWrapper === clickedWrapper)
295
+ return;
296
+ }
297
+
298
+ // Otherwise remove
299
+ this.#activeMenuButton.remove();
300
+ this.#activeMenuButton = null;
301
+ }
302
+
303
+ // ── Operations ───────────────────────────────────────────────────────────
304
+
305
+ insertTable(rows = 3, cols = 3)
306
+ {
307
+ const colgroup = $lib.element({ tag: 'colgroup' }),
308
+ tbody = $lib.element({ tag: 'tbody' }),
309
+ table = $lib.element({ tag: 'table', content: [colgroup, tbody] });
310
+
311
+ table.style.borderCollapse = 'collapse';
312
+ table.style.width = '100%';
313
+
314
+ for (let colIndex = 0; colIndex < cols; colIndex++)
315
+ colgroup.appendChild($lib.element({ tag: 'col' }));
316
+
317
+ for (let rowIndex = 0; rowIndex < rows; rowIndex++)
318
+ {
319
+ const tr = $lib.element({ container: tbody, tag: 'tr' });
320
+ for (let colIndex = 0; colIndex < cols; colIndex++)
321
+ tr.appendChild(this.#createCell());
322
+ }
323
+
324
+ const wrapper = $lib.element({ tag: 'figure', content: [table], props: { className: this.#wrapperCssClass } });
325
+
326
+ this.editor.contentManager.insertHTML({
327
+ node: wrapper,
328
+ isPhrasingContent: false
329
+ });
330
+
331
+ // Set px col widths after layout, then build handles
332
+ this.editor.getDoc().defaultView.requestAnimationFrame(() =>
333
+ {
334
+ this.editor.activateDocument();
335
+ this.#rebuildColgroup(table);
336
+ this.#buildHandles(table);
337
+ const firstCell = table.querySelector('td,th');
338
+ this.editor.bindNodeEvents(wrapper);
339
+ this.#finalize(firstCell);
340
+ });
341
+ }
342
+
343
+ addRow(above = false)
344
+ {
345
+ const { cell, table } = this.#context();
346
+ if (!cell)
347
+ return;
348
+
349
+ const row = cell.parentElement,
350
+ colIndex = this.#cellColIndex(row, cell),
351
+ colCount = this.#rowColCount(table.rows[0]),
352
+ newRow = $lib.element({ tag: 'tr' });
353
+
354
+ for (let index = 0; index < colCount; index++)
355
+ newRow.appendChild(this.#createCell());
356
+
357
+ if (above)
358
+ row.before(newRow);
359
+ else
360
+ row.after(newRow);
361
+
362
+ this.#finalize(newRow.cells[colIndex]);
363
+ this.menu.collapseAll(true);
364
+ }
365
+
366
+ removeRow()
367
+ {
368
+ const { cell, table } = this.#context();
369
+ if (!table || table.rows.length <= 1)
370
+ return;
371
+
372
+ const rows = this.#getSelectedRows(table),
373
+ firstIndex = Math.min(...rows.map(r => this.#rowIndex(table, r))),
374
+ colIndex = this.#cellColIndex(cell.parentElement, cell);
375
+
376
+ rows.forEach(r => r.remove());
377
+
378
+ const targetRow = table.rows[Math.min(firstIndex, table.rows.length - 1)];
379
+ this.#finalize(targetRow.cells[colIndex]);
380
+ this.menu.collapseAll(true);
381
+ }
382
+
383
+ addColumn(left = false)
384
+ {
385
+ const { cell, table } = this.#context();
386
+ if (!cell) return;
387
+
388
+ const row = cell.parentElement,
389
+ colIndex = this.#columnIndexOfCell(row, cell),
390
+ rowIndex = this.#rowIndex(table, row);
391
+
392
+ Array.from(table.rows).forEach(r =>
393
+ {
394
+ const c = this.#cellAtColumnIndex(r, colIndex);
395
+ if (c)
396
+ left ? c.before(this.#createCell()) : c.after(this.#createCell());
397
+ });
398
+
399
+ this.#rebuildColgroup(table, colIndex);
400
+
401
+ const newColIndex = left ? colIndex : colIndex + 1,
402
+ newCell = this.#cellAtColumnIndex(table.rows[rowIndex], newColIndex);
403
+
404
+ this.#buildHandles(table);
405
+ this.#finalize(newCell);
406
+ this.menu.collapseAll(true);
407
+ }
408
+
409
+ removeColumn()
410
+ {
411
+ const { cell, table } = this.#context();
412
+
413
+ if (!cell || this.#rowColCount(table.rows[0]) <= 1)
414
+ return;
415
+
416
+ const row = cell.parentElement,
417
+ colIndex = this.#columnIndexOfCell(row, cell),
418
+ rowIndex = this.#rowIndex(table, row);
419
+
420
+ Array.from(table.rows).forEach(r =>
421
+ {
422
+ const t = this.#cellAtColumnIndex(r, colIndex);
423
+ if (!t) return;
424
+
425
+ const cellStart = this.#columnIndexOfCell(r, t),
426
+ span = t.colSpan || 1;
427
+
428
+ if (cellStart === colIndex) span === 1 ? t.remove() : t.colSpan--;
429
+ else if (cellStart < colIndex) t.colSpan--;
430
+ });
431
+
432
+ this.#rebuildColgroup(table);
433
+
434
+ const targetRow = table.rows[rowIndex],
435
+ newCell = this.#cellAtColumnIndex(targetRow, colIndex);
436
+
437
+ this.#buildHandles(table);
438
+ this.#finalize(newCell);
439
+ this.menu.collapseAll(true);
440
+ }
441
+
442
+ // ── Merge / Split ─────────────────────────────────────────────────────────────
443
+
444
+ canMerge()
445
+ {
446
+ const cells = this.#getSelectedCells(this.#context().table);
447
+ return cells.length > 1;
448
+ }
449
+
450
+ canSplit()
451
+ {
452
+ return this.#getSelectedCells(this.#context().table)
453
+ .some(c => (c.colSpan || 1) > 1 || (c.rowSpan || 1) > 1);
454
+ }
455
+
456
+ canMergeRight()
457
+ {
458
+ const { cell, table } = this.#context();
459
+ if (!cell) return false;
460
+ if (this.#getSelectedCells(table).length > 1) return false;
461
+
462
+ const grid = this.#buildGridMap(table);
463
+ const origin = this.#cellOrigin(grid, cell);
464
+ if (!origin) return false;
465
+
466
+ const next = grid[origin.r]?.[origin.c + (cell.colSpan || 1)];
467
+ if (!next) return false;
468
+
469
+ // shapes must align exactly — same rowSpan and same starting row
470
+ const nextOrigin = this.#cellOrigin(grid, next);
471
+ return (next.rowSpan || 1) === (cell.rowSpan || 1) && nextOrigin.r === origin.r;
472
+ }
473
+
474
+ canMergeDown()
475
+ {
476
+ const { cell, table } = this.#context();
477
+ if (!cell) return false;
478
+ if (this.#getSelectedCells(table).length > 1) return false;
479
+
480
+ const grid = this.#buildGridMap(table);
481
+ const origin = this.#cellOrigin(grid, cell);
482
+ if (!origin) return false;
483
+
484
+ const below = grid[origin.r + (cell.rowSpan || 1)]?.[origin.c];
485
+ if (!below) return false;
486
+
487
+ // shapes must align exactly — same colSpan and same starting column
488
+ const belowOrigin = this.#cellOrigin(grid, below);
489
+ return (below.colSpan || 1) === (cell.colSpan || 1) && belowOrigin.c === origin.c;
490
+ }
491
+
492
+ merge(cells)
493
+ {
494
+ cells = cells || this.#getSelectedCells(this.#context().table);
495
+ if (!cells || cells.length < 2) return;
496
+
497
+ const { table } = this.#context();
498
+ const grid = this.#buildGridMap(table);
499
+
500
+ // find bounding rectangle
501
+ let minRow = Infinity, maxRow = -Infinity,
502
+ minCol = Infinity, maxCol = -Infinity;
503
+
504
+ cells.forEach(cell =>
505
+ {
506
+ const origin = this.#cellOrigin(grid, cell);
507
+ if (!origin) return;
508
+ minRow = Math.min(minRow, origin.r);
509
+ maxRow = Math.max(maxRow, origin.r + (cell.rowSpan || 1) - 1);
510
+ minCol = Math.min(minCol, origin.c);
511
+ maxCol = Math.max(maxCol, origin.c + (cell.colSpan || 1) - 1);
512
+ });
513
+
514
+ // collect ALL cells in the rectangle, not just selected ones
515
+ const allCells = new Set();
516
+ for (let r = minRow; r <= maxRow; r++)
517
+ for (let c = minCol; c <= maxCol; c++)
518
+ if (grid[r]?.[c]) allCells.add(grid[r][c]);
519
+
520
+ const [first, ...rest] = allCells;
521
+ first.colSpan = maxCol - minCol + 1;
522
+ first.rowSpan = maxRow - minRow + 1;
523
+
524
+ // Collect all block content from all cells into first cell
525
+ const fragment = document.createDocumentFragment();
526
+ this.#appendCellContent(fragment, first);
527
+ rest.forEach(cell => this.#appendCellContent(fragment, cell, true));
528
+ first.innerHTML = '';
529
+ first.appendChild(fragment);
530
+
531
+ rest.forEach(c => c.remove());
532
+
533
+ this.#finalize(first);
534
+ this.menu.collapseAll(true);
535
+ }
536
+
537
+ mergeRight()
538
+ {
539
+ const { cell, table } = this.#context();
540
+ if (!cell) return;
541
+
542
+ const grid = this.#buildGridMap(table);
543
+ const origin = this.#cellOrigin(grid, cell);
544
+ if (!origin) return;
545
+
546
+ const next = grid[origin.r]?.[origin.c + (cell.colSpan || 1)];
547
+ if (!next) return;
548
+
549
+ const fragment = document.createDocumentFragment();
550
+ this.#appendCellContent(fragment, cell);
551
+ this.#appendCellContent(fragment, next, true);
552
+
553
+ cell.colSpan = (cell.colSpan || 1) + (next.colSpan || 1);
554
+ cell.innerHTML = '';
555
+ cell.appendChild(fragment);
556
+ next.remove();
557
+
558
+ this.#finalize(cell);
559
+ this.menu.collapseAll(true);
560
+ }
561
+
562
+ mergeDown()
563
+ {
564
+ const { cell, table } = this.#context();
565
+ if (!cell) return;
566
+
567
+ const grid = this.#buildGridMap(table);
568
+ const origin = this.#cellOrigin(grid, cell);
569
+ if (!origin) return;
570
+
571
+ const below = grid[origin.r + (cell.rowSpan || 1)]?.[origin.c];
572
+ if (!below) return;
573
+
574
+ const fragment = document.createDocumentFragment();
575
+ this.#appendCellContent(fragment, cell);
576
+ this.#appendCellContent(fragment, below, true);
577
+
578
+ cell.rowSpan = (cell.rowSpan || 1) + (below.rowSpan || 1);
579
+ cell.innerHTML = '';
580
+ cell.appendChild(fragment);
581
+ below.remove();
582
+
583
+ this.#finalize(cell);
584
+ this.menu.collapseAll(true);
585
+ }
586
+
587
+ split(cells)
588
+ {
589
+ cells = cells || this.#getSelectedCells(this.#context().table);
590
+ if (!cells || !cells.length) return;
591
+
592
+ const { table } = this.#context();
593
+ const grid = this.#buildGridMap(table); // one snapshot, valid throughout
594
+ const firstCell = cells[0];
595
+
596
+ cells.forEach(cell =>
597
+ {
598
+ this.#splitCell(table, cell, grid);
599
+ });
600
+
601
+ this.#finalize(firstCell);
602
+ this.menu.collapseAll(true);
603
+ }
604
+
605
+ #appendCellContent(fragment, cell, skipPlaceholder = false)
606
+ {
607
+ const children = Array.from(cell.childNodes);
608
+
609
+ if (!children.length)
610
+ {
611
+ if (!skipPlaceholder)
612
+ {
613
+ const p = $lib.element({ tag: 'p' });
614
+ $lib.element({ container: p, tag: 'br' });
615
+ fragment.appendChild(p);
616
+ }
617
+ return;
618
+ }
619
+
620
+ if (skipPlaceholder
621
+ && children.length === 1
622
+ && children[0].nodeName === 'P'
623
+ && children[0].childNodes.length === 1
624
+ && children[0].firstChild?.nodeName === 'BR')
625
+ return;
626
+
627
+ children.forEach(n => fragment.appendChild(n.cloneNode(true)));
628
+ }
629
+
630
+ #splitCell(table, cell, grid)
631
+ {
632
+ const colSpan = cell.colSpan || 1,
633
+ rowSpan = cell.rowSpan || 1;
634
+
635
+ grid = grid || this.#buildGridMap(table);
636
+
637
+ if (colSpan > 1)
638
+ {
639
+ cell.colSpan = 1;
640
+ for (let i = 1; i < colSpan; i++)
641
+ cell.parentElement.insertBefore(this.#createCell(), cell.nextElementSibling);
642
+ }
643
+
644
+ if (rowSpan > 1)
645
+ {
646
+ const origin = this.#cellOrigin(grid, cell);
647
+ if (!origin)
648
+ return;
649
+
650
+ cell.rowSpan = 1;
651
+
652
+ for (let rowIndex = 1; rowIndex < rowSpan; rowIndex++)
653
+ {
654
+ const targetRow = table.rows[origin.r + rowIndex];
655
+ if (!targetRow)
656
+ continue;
657
+
658
+ const siblingCell = grid[origin.r + rowIndex]?.[origin.c + colSpan];
659
+
660
+ for (let colIndex = 0; colIndex < colSpan; colIndex++)
661
+ {
662
+ const newCell = this.#createCell();
663
+ targetRow.insertBefore(newCell, siblingCell);
664
+ }
665
+ }
666
+ }
667
+ }
668
+
669
+ // ── Event handlers ───────────────────────────────────────────────────────
670
+
671
+ #cut(e)
672
+ {
673
+ const { cell, table } = this.#context();
674
+
675
+ if (!table)
676
+ return;
677
+
678
+ const data = this.#serializeSelection();
679
+ if (!data)
680
+ return;
681
+
682
+ e.clipboardData.setData('text/html', data.html);
683
+ e.clipboardData.setData('text/plain', data.text);
684
+ e.preventDefault();
685
+ this.#clearCells(this.#getSelectedCells(table));
686
+ this.#finalize(cell);
687
+ }
688
+
689
+ #copy(e)
690
+ {
691
+ const data = this.#serializeSelection();
692
+ if (!data)
693
+ return;
694
+
695
+ e.clipboardData.setData('text/html', data.html);
696
+ e.clipboardData.setData('text/plain', data.text);
697
+ e.preventDefault();
698
+ }
699
+
700
+ #paste(e)
701
+ {
702
+ const { cell, table } = this.#context();
703
+ if (!cell || !table)
704
+ return;
705
+
706
+ const html = e.clipboardData.getData('text/html');
707
+
708
+ if (html)
709
+ {
710
+ const temp = $lib.element({ content: [html] }),
711
+ sourceTable = temp.querySelector(':scope > table');
712
+
713
+ if (!sourceTable)
714
+ return;
715
+
716
+ sourceTable.remove();
717
+
718
+ if (temp.textContent.trim())
719
+ return;
720
+
721
+ e.preventDefault();
722
+ this.#applyPaste(sourceTable, table, cell);
723
+ }
724
+ else
725
+ {
726
+ const text = e.clipboardData.getData('text/plain');
727
+ if (text)
728
+ {
729
+ e.preventDefault();
730
+ this.#insertPlainText(text);
731
+ }
732
+ }
733
+
734
+ this.#finalize(cell);
735
+ }
736
+
737
+ #pointerDown(e)
738
+ {
739
+ if (e.button !== 0)
740
+ return;
741
+
742
+ this.#contextCell = null;
743
+
744
+ const cell = e.target.closest('td,th');
745
+
746
+ if (!cell)
747
+ return;
748
+
749
+ const differentTable = this.#anchorCell && !this.#isSameTable(cell, this.#anchorCell);
750
+
751
+ if (differentTable && this.#isSelecting)
752
+ return;
753
+
754
+ if (differentTable)
755
+ this.clearSelection();
756
+
757
+ this.#isSelecting = true;
758
+ this.#clearHighlights();
759
+ this.#ensureCellParagraph(cell);
760
+
761
+ if (e.shiftKey && this.#anchorCell)
762
+ {
763
+ e.preventDefault();
764
+ this.#selectRange(this.#anchorCell, cell);
765
+ }
766
+ else if (e.ctrlKey || e.metaKey)
767
+ {
768
+ e.preventDefault();
769
+ this.#selectedCells.has(cell) ? this.#selectedCells.delete(cell) : this.#selectedCells.add(cell);
770
+ this.#anchorCell = cell;
771
+ }
772
+ else
773
+ {
774
+ this.#selectedCells = new Set([cell]);
775
+ this.#anchorCell = cell;
776
+ }
777
+
778
+ this.#applyHighlights();
779
+ this.showMenuButton(cell);
780
+ this.#buildHandles(cell.closest('table'));
781
+ }
782
+
783
+ #pointerOver(e)
784
+ {
785
+ if (!this.#isSelecting)
786
+ return;
787
+
788
+ const cell = e.target.closest('td,th');
789
+
790
+ if (!cell || !this.#anchorCell)
791
+ return;
792
+
793
+ if (!this.#isSameTable(cell, this.#anchorCell))
794
+ return;
795
+
796
+ this.#clearHighlights();
797
+ this.#selectRange(this.#anchorCell, cell);
798
+ this.#applyHighlights();
799
+ }
800
+
801
+ #pointerMove(e)
802
+ {
803
+ this.#collapseMultiCellSelection();
804
+ }
805
+
806
+ #pointerUp(e)
807
+ {
808
+ const cell = e.target.closest('td,th');
809
+
810
+ this.#isSelecting = false;
811
+
812
+ if (cell)
813
+ cell.closest('table').style.removeProperty('user-select');
814
+ }
815
+
816
+ #keyDown(e)
817
+ {
818
+ this.#handleCaretOutsideCell(e);
819
+
820
+ const { cell } = this.#context();
821
+
822
+ if (!cell || !this.editor.getEditorElement().contains(cell))
823
+ return;
824
+
825
+ this.#ensureCellParagraph(cell);
826
+
827
+ if (e.shiftKey && ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key))
828
+ this.#keySelection(e, cell);
829
+ else
830
+ this.#keyNavigation(e, cell);
831
+ }
832
+
833
+ #contextMenu(e)
834
+ {
835
+ const cell = e.target.closest('td,th');
836
+ if (!cell) return;
837
+
838
+ // single cell with text selection — let browser handle
839
+ if (this.#selectedCells.size <= 1)
840
+ {
841
+ const range = this.editor.selectionRange.getRange();
842
+ if (!range.collapsed)
843
+ return;
844
+ }
845
+
846
+ this.#contextCell = cell; // capture before focus can shift
847
+ e.preventDefault();
848
+ this.#showMenu();
849
+ }
850
+
851
+ #selectionChange(e)
852
+ {
853
+ const { cell, table } = this.#context();
854
+
855
+ if (!cell)
856
+ return;
857
+
858
+ this.showMenuButton(cell);
859
+ }
860
+
861
+ // ── Menu ─────────────────────────────────────────────────────────────────
862
+
863
+ #showMenu()
864
+ {
865
+ if (this.#activeMenuButton)
866
+ this.hideMenuButton();
867
+
868
+ if (!this.menu)
869
+ this.#createMenu();
870
+ else if (this.menu.renderState == $base.static.RenderState.RENDERED)
871
+ {
872
+ this.#updateMenuState();
873
+ this.menu.expand('root');
874
+ }
875
+ }
876
+
877
+ #createMenu()
878
+ {
879
+ const Item = componyx.UI.Menu.Item,
880
+ labels = this.editor.labels;
881
+
882
+ this.menu = this.cf.createMenu('TableMenu', this.editor.tableMenuId, this.editor.element);
883
+
884
+ const list = this.menu.itemList;
885
+
886
+ const selectable = false;
887
+
888
+ list.push(new Item({
889
+ id: 'Cut',
890
+ text: labels.tableMenuCut,
891
+ hasIcon: true,
892
+ selectable,
893
+ cssClassIcon: 'icon ico-editor-cut',
894
+ command: () => this.#actionCut()
895
+ }));
896
+
897
+ list.push(new Item({
898
+ id: 'Copy',
899
+ text: labels.tableMenuCopy,
900
+ hasIcon: true,
901
+ selectable,
902
+ cssClassIcon: 'icon ico-copy',
903
+ command: () => this.#actionCopy()
904
+ }));
905
+
906
+ list.push(new Item({
907
+ id: 'Paste',
908
+ text: labels.tableMenuPaste,
909
+ hasIcon: true,
910
+ selectable,
911
+ cssClassIcon: 'icon ico-editor-clipboard',
912
+ command: () => this.#actionPaste()
913
+ }));
914
+
915
+ list.push(new Item({
916
+ id: 'PastePlainText',
917
+ text: labels.tableMenuPastePlainText,
918
+ hasIcon: true,
919
+ selectable,
920
+ cssClassIcon: 'icon ico-editor-clipboard-text',
921
+ command: () => this.#actionPasteText()
922
+ }));
923
+
924
+ const rowItem = new Item({
925
+ id: 'Row',
926
+ text: labels.tableMenuRow,
927
+ hasIcon: true,
928
+ selectable,
929
+ cssClassIcon: 'icon ico-editor-row',
930
+ });
931
+
932
+ rowItem.itemList.push(new Item({
933
+ id: 'AddRowAbove',
934
+ text: labels.tableMenuAddRowAbove,
935
+ hasIcon: true,
936
+ selectable,
937
+ cssClassIcon: 'icon ico-editor-add-above',
938
+ command: () => this.addRow(true)
939
+ }));
940
+
941
+ rowItem.itemList.push(new Item({
942
+ id: 'AddRowBelow',
943
+ text: labels.tableMenuAddRowBelow,
944
+ hasIcon: true,
945
+ selectable,
946
+ cssClassIcon: 'icon ico-editor-add-below',
947
+ command: () => this.addRow()
948
+ }));
949
+
950
+ rowItem.itemList.push(new Item({
951
+ id: 'RemoveRow',
952
+ text: labels.tableMenuRemoveRow,
953
+ hasIcon: true,
954
+ selectable,
955
+ cssClassIcon: 'icon ico-editor-remove-row',
956
+ command: () => this.removeRow()
957
+ }));
958
+
959
+ list.push(rowItem);
960
+
961
+ const colItem = new Item({
962
+ id: 'Col',
963
+ text: labels.tableMenuColumn,
964
+ hasIcon: true,
965
+ cssClassIcon: 'icon ico-editor-col',
966
+ selectable,
967
+ });
968
+
969
+ colItem.itemList.push(new Item({
970
+ id: 'AddColLeft',
971
+ text: labels.tableMenuAddColumnLeft,
972
+ hasIcon: true,
973
+ selectable,
974
+ cssClassIcon: 'icon ico-editor-add-left',
975
+ command: () => this.addColumn(true)
976
+ }));
977
+
978
+ colItem.itemList.push(new Item({
979
+ id: 'AddColRight',
980
+ text: labels.tableMenuAddColumnRight,
981
+ hasIcon: true,
982
+ selectable,
983
+ cssClassIcon: 'icon ico-editor-add-right',
984
+ command: () => this.addColumn()
985
+ }));
986
+
987
+ colItem.itemList.push(new Item({
988
+ id: 'RemoveCol',
989
+ text: labels.tableMenuRemoveColumn,
990
+ hasIcon: true,
991
+ selectable,
992
+ cssClassIcon: 'icon ico-editor-remove-col',
993
+ command: () => this.removeColumn()
994
+ }));
995
+
996
+ list.push(colItem);
997
+
998
+ const mergeItem = new Item({
999
+ id: 'Merge',
1000
+ text: labels.tableMenuMerge,
1001
+ hasIcon: true,
1002
+ selectable,
1003
+ cssClassIcon: 'icon ico-editor-merge-cells',
1004
+ });
1005
+
1006
+ mergeItem.itemList.push(new Item({
1007
+ id: 'MergeSelection',
1008
+ selectable,
1009
+ text: labels.tableMenuMergeSelection,
1010
+ command: () => this.merge()
1011
+ }));
1012
+
1013
+ mergeItem.itemList.push(new Item({
1014
+ id: 'MergeRight',
1015
+ selectable,
1016
+ text: labels.tableMenuMergeRight,
1017
+ command: () => this.mergeRight()
1018
+ }));
1019
+
1020
+ mergeItem.itemList.push(new Item({
1021
+ id: 'MergeDown',
1022
+ selectable,
1023
+ text: labels.tableMenuMergeDown,
1024
+ command: () => this.mergeDown()
1025
+ }));
1026
+
1027
+ list.push(mergeItem);
1028
+
1029
+ list.push(new Item({
1030
+ id: 'Split',
1031
+ selectable,
1032
+ text: labels.tableMenuSplit,
1033
+ hasIcon: true,
1034
+ cssClassIcon: 'icon ico-editor-split-cells',
1035
+ command: () => this.split()
1036
+ }));
1037
+
1038
+ list.push(new Item({
1039
+ id: 'Settings',
1040
+ selectable,
1041
+ text: labels.tableMenuSettings,
1042
+ hasIcon: true,
1043
+ cssClassIcon: 'icon ico-cog',
1044
+ command: () => this.#showSettings()
1045
+ }));
1046
+
1047
+ list.push(new Item({
1048
+ id: 'DeleteTable',
1049
+ text: labels.tableMenuDelete,
1050
+ hasIcon: true,
1051
+ selectable,
1052
+ cssClassIcon: 'icon ico-bin',
1053
+ command: () => this.#deleteTable()
1054
+ }));
1055
+
1056
+ this.menu.events.onPostRender.priorityAdd(() =>
1057
+ {
1058
+ this.#updateMenuState();
1059
+ this.menu.expand('root');
1060
+ });
1061
+
1062
+ this.menu.expandOnClick = componyx.UI.Menu.ExpandOnClickOption.ROOT;
1063
+ this.menu.visibleRoot = false;
1064
+ this.menu.showType = this.menu.hideType = 0; // Disable animations for context menu
1065
+ this.menu.show();
1066
+ }
1067
+
1068
+ #updateMenuState()
1069
+ {
1070
+ const { table } = this.#context();
1071
+
1072
+ table && table.rows.length > 1 ? this.menu.enableItem('RemoveRow') : this.menu.disableItem('RemoveRow');
1073
+ table && this.#rowColCount(table.rows[0]) > 1 ? this.menu.enableItem('RemoveCol') : this.menu.disableItem('RemoveCol');
1074
+
1075
+ // Merge / Split
1076
+ this.canMerge() ? this.menu.enableItem('MergeSelection') : this.menu.disableItem('MergeSelection');
1077
+ this.canMergeRight() ? this.menu.enableItem('MergeRight') : this.menu.disableItem('MergeRight');
1078
+ this.canMergeDown() ? this.menu.enableItem('MergeDown') : this.menu.disableItem('MergeDown');
1079
+ this.canSplit() ? this.menu.enableItem('Split') : this.menu.disableItem('Split');
1080
+ }
1081
+
1082
+ #deleteTable()
1083
+ {
1084
+ const { table } = this.#context();
1085
+ if (!table)
1086
+ return;
1087
+
1088
+ const wrapper = table.closest(`figure.${this.#wrapperCssClass}`);
1089
+ (wrapper || table).remove();
1090
+
1091
+ this.#removeHandles(table);
1092
+ this.#finalize();
1093
+ this.menu.collapseAll(true);
1094
+ }
1095
+
1096
+ #showSettings()
1097
+ {
1098
+ this.menu.collapseAll(true);
1099
+ this.editor.dialogManager.tableDialog.show(this.#context());
1100
+ }
1101
+
1102
+ async #actionCut()
1103
+ {
1104
+ const { cell, table } = this.#context();
1105
+ if (!table)
1106
+ return;
1107
+
1108
+ await this.#writeToClipboard();
1109
+ this.#clearCells(this.#getSelectedCells(table));
1110
+ this.#finalize(cell);
1111
+ this.menu.collapseAll(true);
1112
+ }
1113
+
1114
+ async #actionCopy()
1115
+ {
1116
+ await this.#writeToClipboard();
1117
+ this.menu.collapseAll(true);
1118
+ }
1119
+
1120
+ async #actionPaste()
1121
+ {
1122
+ const { cell } = this.#context();
1123
+ if (!cell) return;
1124
+
1125
+ this.#selectCell(cell);
1126
+
1127
+ const clipboardItems = await navigator.clipboard.read(),
1128
+ item = clipboardItems[0];
1129
+
1130
+ if (!item.types.includes('text/html')) return;
1131
+
1132
+ const blob = await item.getType('text/html'),
1133
+ html = await blob.text(),
1134
+ text = item.types.includes('text/plain') ? await (await item.getType('text/plain')).text() : '';
1135
+
1136
+ const dt = new DataTransfer();
1137
+ dt.setData('text/html', html);
1138
+
1139
+ if (text)
1140
+ dt.setData('text/plain', text);
1141
+
1142
+ this.editor.getEditorElement().dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true })); // fire paste event (global event handler will sanitize html)
1143
+ this.menu.collapseAll(true);
1144
+ }
1145
+
1146
+ async #actionPasteText()
1147
+ {
1148
+ const { cell, table } = this.#context();
1149
+ if (!cell || !table)
1150
+ return;
1151
+
1152
+ const text = await navigator.clipboard.readText();
1153
+ this.#insertPlainText(text);
1154
+ this.#finalize(cell);
1155
+ this.menu.collapseAll(true);
1156
+ }
1157
+
1158
+ async #insertPlainText(text)
1159
+ {
1160
+ const { cell } = this.#context();
1161
+ if (!cell)
1162
+ return;
1163
+
1164
+ const range = this.editor.selectionRange.getRange();
1165
+
1166
+ if (!cell.contains(range.startContainer))
1167
+ return;
1168
+
1169
+ range.deleteContents();
1170
+ range.insertNode(document.createTextNode(text));
1171
+ range.collapse(false);
1172
+ this.editor.selectionRange.ensureTextRange(range);
1173
+ }
1174
+
1175
+
1176
+ // ── Keyboard Navigation ──────────────────────────────────────────────────
1177
+
1178
+ #keyNavigation(e, cell)
1179
+ {
1180
+ const table = cell.closest('table'),
1181
+ grid = this.#buildGridMap(table),
1182
+ origin = this.#cellOrigin(grid, cell),
1183
+ key = e.key;
1184
+
1185
+ if (!origin) return;
1186
+
1187
+ const { r, c } = origin,
1188
+ rowSpan = cell.rowSpan || 1,
1189
+ colSpan = cell.colSpan || 1;
1190
+
1191
+ let target = null;
1192
+
1193
+ if (key === 'Tab')
1194
+ {
1195
+ const allCells = Array.from(table.querySelectorAll('td,th')),
1196
+ domIndex = allCells.indexOf(cell);
1197
+
1198
+ if (e.shiftKey)
1199
+ target = allCells[domIndex - 1] ?? null;
1200
+ else if (domIndex < allCells.length - 1)
1201
+ target = allCells[domIndex + 1];
1202
+ else
1203
+ {
1204
+ this.addRow();
1205
+ target = table.rows[table.rows.length - 1].cells[0];
1206
+ }
1207
+ }
1208
+ else if (key === 'ArrowLeft')
1209
+ {
1210
+ if (!this.#atCellStart(cell)) return;
1211
+ target = grid[r]?.[c - 1] ?? null;
1212
+ if (!target) { e.preventDefault(); e.stopPropagation(); return; }
1213
+ }
1214
+ else if (key === 'ArrowRight')
1215
+ {
1216
+ if (!this.#atCellEnd(cell)) return;
1217
+ target = grid[r]?.[c + colSpan] ?? null;
1218
+ if (!target) { e.preventDefault(); e.stopPropagation(); return; }
1219
+ }
1220
+ else if (key === 'ArrowUp')
1221
+ {
1222
+ if (!this.#onFirstLine(cell)) return;
1223
+ target = grid[r - 1]?.[c] ?? null;
1224
+ if (!target) { e.preventDefault(); e.stopPropagation(); return; }
1225
+ }
1226
+ else if (key === 'ArrowDown')
1227
+ {
1228
+ if (!this.#onLastLine(cell)) return;
1229
+ target = grid[r + rowSpan]?.[c] ?? null;
1230
+ if (!target) { e.preventDefault(); e.stopPropagation(); return; }
1231
+ }
1232
+
1233
+ if (target)
1234
+ {
1235
+ e.preventDefault();
1236
+ e.stopPropagation();
1237
+ this.#selectCell(target, false);
1238
+ this.#placeCaret(target, key === 'ArrowRight' || key === 'ArrowDown' || key === 'Tab');
1239
+ }
1240
+ }
1241
+
1242
+ #keySelection(e, cell)
1243
+ {
1244
+ const key = e.key,
1245
+ table = cell.closest('table'),
1246
+ grid = this.#buildGridMap(table);
1247
+
1248
+ // Only expand selection when caret is at the boundary in the arrow direction
1249
+ // This allows normal text selection within a cell first
1250
+ if (this.#selectedCells.size <= 1)
1251
+ {
1252
+ if (key === 'ArrowRight' && !this.#atCellEnd(cell, false)) return;
1253
+ if (key === 'ArrowLeft' && !this.#atCellStart(cell, false)) return;
1254
+ if (key === 'ArrowDown' && !this.#onLastLine(cell)) return;
1255
+ if (key === 'ArrowUp' && !this.#onFirstLine(cell)) return;
1256
+ }
1257
+
1258
+ if (!this.#anchorCell)
1259
+ this.#anchorCell = cell;
1260
+
1261
+ const anchorOrigin = this.#cellOrigin(grid, this.#anchorCell);
1262
+
1263
+ let minRow = anchorOrigin.r,
1264
+ maxRow = anchorOrigin.r + (this.#anchorCell.rowSpan || 1) - 1,
1265
+ minCol = anchorOrigin.c,
1266
+ maxCol = anchorOrigin.c + (this.#anchorCell.colSpan || 1) - 1;
1267
+
1268
+ this.#selectedCells.forEach(c =>
1269
+ {
1270
+ const origin = this.#cellOrigin(grid, c);
1271
+ if (!origin) return;
1272
+ minRow = Math.min(minRow, origin.r);
1273
+ maxRow = Math.max(maxRow, origin.r + (c.rowSpan || 1) - 1);
1274
+ minCol = Math.min(minCol, origin.c);
1275
+ maxCol = Math.max(maxCol, origin.c + (c.colSpan || 1) - 1);
1276
+ });
1277
+
1278
+ // Focus is whichever corner is opposite the anchor
1279
+ const focusRow = anchorOrigin.r <= (minRow + maxRow) / 2 ? maxRow : minRow;
1280
+ const focusCol = anchorOrigin.c <= (minCol + maxCol) / 2 ? maxCol : minCol;
1281
+
1282
+ // Step focus one unit in the arrow direction
1283
+ let newFocusRow = focusRow,
1284
+ newFocusCol = focusCol;
1285
+
1286
+ if (key === 'ArrowRight') newFocusCol = focusCol + 1;
1287
+ else if (key === 'ArrowLeft') newFocusCol = focusCol - 1;
1288
+ else if (key === 'ArrowDown') newFocusRow = focusRow + 1;
1289
+ else if (key === 'ArrowUp') newFocusRow = focusRow - 1;
1290
+
1291
+ const nextFocus = grid[newFocusRow]?.[newFocusCol] ?? null;
1292
+ if (!nextFocus)
1293
+ return;
1294
+
1295
+ e.preventDefault();
1296
+ e.stopPropagation();
1297
+ this.#clearHighlights();
1298
+ this.#selectRange(this.#anchorCell, nextFocus);
1299
+ this.#applyHighlights();
1300
+
1301
+ this.editor.getDoc().defaultView.requestAnimationFrame(() =>
1302
+ {
1303
+ this.editor.activateDocument();
1304
+ this.#placeCaret(this.#anchorCell, true);
1305
+ });
1306
+ }
1307
+
1308
+ #atCellStart(cell, requireCollapsed = true)
1309
+ {
1310
+ const range = this.editor.selectionRange.getRange();
1311
+ if (requireCollapsed && !range.collapsed)
1312
+ return false;
1313
+
1314
+ if (!cell.contains(range.startContainer))
1315
+ return false;
1316
+
1317
+ if (range.startOffset !== 0)
1318
+ return false;
1319
+
1320
+ let node = range.startContainer;
1321
+ while (node && node !== cell)
1322
+ {
1323
+ if (node.previousSibling)
1324
+ return false;
1325
+
1326
+ node = node.parentNode;
1327
+ }
1328
+ return true;
1329
+ }
1330
+
1331
+ #atCellEnd(cell, requireCollapsed = true)
1332
+ {
1333
+ const range = this.editor.selectionRange.getRange();
1334
+ if (requireCollapsed && !range.collapsed)
1335
+ return false;
1336
+
1337
+ const container = range.endContainer,
1338
+ maxOffset = container.nodeType === Node.TEXT_NODE ? container.length : container.childNodes.length;
1339
+
1340
+ if (!cell.contains(container))
1341
+ return false;
1342
+
1343
+ if (range.endOffset !== maxOffset)
1344
+ return false;
1345
+
1346
+ let node = container;
1347
+ while (node && node !== cell)
1348
+ {
1349
+ if (node.nextSibling)
1350
+ return false;
1351
+
1352
+ node = node.parentNode;
1353
+ }
1354
+ return true;
1355
+ }
1356
+
1357
+ #onFirstLine(cell)
1358
+ {
1359
+ const range = this.editor.selectionRange.getRange(),
1360
+ style = getComputedStyle(cell),
1361
+ lineHeight = parseFloat(style.lineHeight) || 16,
1362
+ paddingTop = parseFloat(style.paddingTop) || 0;
1363
+
1364
+ let rangeRect = range.getBoundingClientRect();
1365
+
1366
+ if (rangeRect.top === 0)
1367
+ {
1368
+ const node = range.startContainer;
1369
+ const target = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
1370
+ rangeRect = target.getBoundingClientRect();
1371
+ }
1372
+
1373
+ const cellRect = cell.getBoundingClientRect();
1374
+
1375
+ return rangeRect.top - (cellRect.top + paddingTop) < lineHeight / 2;
1376
+ }
1377
+
1378
+ #onLastLine(cell)
1379
+ {
1380
+ const range = this.editor.selectionRange.getRange(),
1381
+ style = getComputedStyle(cell),
1382
+ lineHeight = parseFloat(style.lineHeight) || 16,
1383
+ paddingBottom = parseFloat(style.paddingBottom) || 0;
1384
+
1385
+ let rangeRect = range.getBoundingClientRect();
1386
+
1387
+ if (rangeRect.top === 0)
1388
+ {
1389
+ const node = range.startContainer;
1390
+ const target = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
1391
+ rangeRect = target.getBoundingClientRect();
1392
+ }
1393
+
1394
+ const cellRect = cell.getBoundingClientRect();
1395
+
1396
+ return rangeRect.bottom - (cellRect.bottom - paddingBottom) < lineHeight / 2;
1397
+ }
1398
+
1399
+ #placeCaret(cell, toStart)
1400
+ {
1401
+ this.#ensureCellParagraph(cell);
1402
+
1403
+ const blocks = Array.from(cell.children).filter(n => this.editor.nodeManager.isBlock(n)),
1404
+ target = !toStart ? blocks[blocks.length - 1] : blocks[0],
1405
+ range = this.editor.selectionRange.getRange();
1406
+
1407
+ range.selectNodeContents(target);
1408
+ this.editor.selectionRange.ensureTextRange(range); // resolves to text node first
1409
+ range.collapse(toStart); // then collapse on text node
1410
+ this.editor.selectionRange.restoreRange(range);
1411
+ }
1412
+
1413
+ // ── Resize handles ───────────────────────────────────────────────────────
1414
+
1415
+ #buildHandles(table)
1416
+ {
1417
+ const wrapperEl = table.closest(`figure.${this.#wrapperCssClass}`);
1418
+ if (!wrapperEl)
1419
+ return;
1420
+
1421
+ this.#removeHandles(table);
1422
+
1423
+ const cols = this.#cols(table);
1424
+
1425
+ cols.forEach(col =>
1426
+ {
1427
+ const handle = $lib.element({ container: wrapperEl, props: { className: this.#colHandleCssClass } });
1428
+ this.#positionHandle(handle, col, table);
1429
+ $lib.on(handle, 'pointerdown', this.#resizeStart, [col, table], this);
1430
+ });
1431
+
1432
+ // Observe table for size changes
1433
+ if (!this.#tableObservers.has(table))
1434
+ {
1435
+ const observer = new ResizeObserver(() =>
1436
+ {
1437
+ if (!table.isConnected)
1438
+ {
1439
+ observer.disconnect();
1440
+ this.#tableObservers.delete(table);
1441
+ return;
1442
+ }
1443
+ this.#repositionHandles(table);
1444
+ });
1445
+ observer.observe(table);
1446
+ this.#tableObservers.set(table, observer);
1447
+ }
1448
+ }
1449
+
1450
+ #repositionHandles(table)
1451
+ {
1452
+ if (!table?.isConnected)
1453
+ return;
1454
+
1455
+ const cols = this.#cols(table),
1456
+ wrapperEl = table.closest(`figure.${this.#wrapperCssClass}`);
1457
+
1458
+ wrapperEl.querySelectorAll(`:scope > .${this.#colHandleCssClass}`)
1459
+ .forEach((handle, index) => this.#positionHandle(handle, cols[index], table));
1460
+ }
1461
+
1462
+ #positionHandle(handle, col, table)
1463
+ {
1464
+ const wrapperEl = table.closest(`figure.${this.#wrapperCssClass}`),
1465
+ wrapperRect = wrapperEl.getBoundingClientRect(),
1466
+ tableRect = table.getBoundingClientRect();
1467
+
1468
+ handle.style.left = Math.round((tableRect.left - wrapperRect.left) + col.offsetLeft + col.offsetWidth - (handle.offsetWidth / 2)) + 'px';
1469
+ handle.style.top = Math.round(tableRect.top - wrapperRect.top) + 'px';
1470
+ handle.style.height = table.offsetHeight + 'px';
1471
+ }
1472
+
1473
+ #removeHandles(table = null)
1474
+ {
1475
+ if (table)
1476
+ {
1477
+ this.#tableObservers.get(table)?.disconnect();
1478
+ this.#tableObservers.delete(table);
1479
+
1480
+ const wrapperEl = table.closest(`figure.${this.#wrapperCssClass}`);
1481
+ wrapperEl?.querySelectorAll(`:scope > .${this.#colHandleCssClass}`).forEach(h => h.remove());
1482
+ }
1483
+ else
1484
+ {
1485
+ const root = this.editor.getEditorElement();
1486
+ root.querySelectorAll(`.${this.#colHandleCssClass}`).forEach(h => h.remove());
1487
+ this.#tableObservers = new WeakMap();
1488
+ }
1489
+ }
1490
+
1491
+ // ── Resizing ─────────────────────────────────────────────────────────────
1492
+
1493
+ #resizeStart(col, table, e)
1494
+ {
1495
+ if (e.button !== 0)
1496
+ return;
1497
+
1498
+ e.preventDefault(); // prevents drag initiation and text selection
1499
+ e.stopPropagation(); // we own this event, don't bubble up
1500
+ this.hideMenuButton();
1501
+
1502
+ const handle = e.target,
1503
+ cols = this.#cols(table),
1504
+ colIndex = cols.findIndex(c => c === col),
1505
+ isLast = cols[cols.length - 1] === col,
1506
+ nextCol = col.nextElementSibling,
1507
+ prevCol = col.previousElementSibling,
1508
+ usePercent = this.#usesPercentages(table),
1509
+ tableRect = table.getBoundingClientRect(),
1510
+ widthOfPrevCols = cols.slice(0, colIndex).reduce((sum, c) => sum + c.offsetWidth, 0),
1511
+ neighbourCol = (isLast && usePercent) ? prevCol : nextCol;
1512
+
1513
+ handle.classList.add(this.#colHandleActiveCssClass);
1514
+
1515
+ this.#dragging = {
1516
+ col,
1517
+ table,
1518
+ isLast,
1519
+ usePercent,
1520
+ totalWidth: table.offsetWidth,
1521
+ startColWidth: col.offsetWidth,
1522
+ startNextColWidth: neighbourCol ? neighbourCol.offsetWidth : 0,
1523
+ nextCol: neighbourCol,
1524
+ startX: tableRect.left + widthOfPrevCols + col.offsetWidth,
1525
+ };
1526
+ }
1527
+
1528
+ #scheduleResize(e)
1529
+ {
1530
+ if (!this.#dragging)
1531
+ return;
1532
+
1533
+ this.#lastEvent = e;
1534
+ if (!this.#rafId)
1535
+ {
1536
+ this.#rafId = this.editor.getDoc().defaultView.requestAnimationFrame(() =>
1537
+ {
1538
+ this.editor.activateDocument();
1539
+
1540
+ if (this.#lastEvent)
1541
+ this.#resize(this.#lastEvent);
1542
+
1543
+ this.#rafId = null;
1544
+ this.#lastEvent = null;
1545
+ });
1546
+ }
1547
+ }
1548
+
1549
+ #resize(e)
1550
+ {
1551
+ const { col, table, isLast, usePercent, totalWidth, startColWidth, startX } = this.#dragging;
1552
+
1553
+ const delta = e.clientX - startX,
1554
+ newPx = Math.max(30, startColWidth + delta);
1555
+
1556
+ if (isLast && !usePercent)
1557
+ {
1558
+ const newTableWidth = Math.max(totalWidth - startColWidth + 30, totalWidth + delta);
1559
+ table.style.width = newTableWidth.toFixed(0) + 'px';
1560
+ col.style.width = newPx.toFixed(0) + 'px';
1561
+ }
1562
+ else
1563
+ {
1564
+ const nextCol = this.#dragging.nextCol,
1565
+ nextColWidth = startColWidth + this.#dragging.startNextColWidth - newPx;
1566
+
1567
+ if (!nextCol || nextColWidth < 30)
1568
+ return;
1569
+
1570
+ if (usePercent)
1571
+ {
1572
+ col.style.width = (newPx / totalWidth * 100).toFixed(2) + '%';
1573
+ nextCol.style.width = (nextColWidth / totalWidth * 100).toFixed(2) + '%';
1574
+ }
1575
+ else
1576
+ {
1577
+ col.style.width = newPx.toFixed(0) + 'px';
1578
+ nextCol.style.width = nextColWidth.toFixed(0) + 'px';
1579
+ }
1580
+ }
1581
+
1582
+ this.#repositionHandles(table);
1583
+ }
1584
+
1585
+ #resizeEnd()
1586
+ {
1587
+ if (!this.#dragging)
1588
+ return;
1589
+
1590
+ const { table } = this.#dragging;
1591
+ if (this.#rafId) { cancelAnimationFrame(this.#rafId); this.#rafId = null; }
1592
+ this.#dragging = null;
1593
+ this.#lastEvent = null;
1594
+
1595
+ const wrapperEl = table.closest(`figure.${this.#wrapperCssClass}`);
1596
+ wrapperEl.querySelectorAll(`:scope > .${this.#colHandleCssClass}`).forEach(h => h.classList.remove(this.#colHandleActiveCssClass));
1597
+
1598
+ const { cell } = this.#context();
1599
+ this.#finalize(cell);
1600
+
1601
+ if (cell && this.#activeMenuButton)
1602
+ this.positionMenuButton(cell);
1603
+ }
1604
+
1605
+ // ── Copying/Pasting ──────────────────────────────────────────────────────
1606
+
1607
+ /**
1608
+ * Writes the table html data to the clipboard.
1609
+ */
1610
+ async #writeToClipboard()
1611
+ {
1612
+ const data = this.#serializeSelection();
1613
+ if (!data)
1614
+ return;
1615
+
1616
+ await navigator.clipboard.write([
1617
+ new ClipboardItem({
1618
+ 'text/html': new Blob([data.html], { type: 'text/html' }),
1619
+ 'text/plain': new Blob([data.text], { type: 'text/plain' })
1620
+ })
1621
+ ]);
1622
+ }
1623
+
1624
+ /**
1625
+ * Builds the html data to copy.
1626
+ * @ignore
1627
+ * @returns {Object} The html and text data.
1628
+ */
1629
+ #serializeSelection()
1630
+ {
1631
+ const { table } = this.#context();
1632
+ if (!table || this.#selectedCells.size === 0)
1633
+ return null;
1634
+
1635
+ const rowMap = new Map();
1636
+
1637
+ this.#selectedCells.forEach(cell =>
1638
+ {
1639
+ const row = cell.parentElement;
1640
+
1641
+ if (!rowMap.has(row))
1642
+ rowMap.set(row, []);
1643
+
1644
+ rowMap.get(row).push(cell);
1645
+ });
1646
+
1647
+ const tableEl = $lib.element({ tag: 'table' });
1648
+
1649
+ rowMap.forEach(cells =>
1650
+ {
1651
+ const tr = $lib.element({ container: tableEl, tag: 'tr' });
1652
+
1653
+ cells.forEach(cell =>
1654
+ {
1655
+ $lib.element({
1656
+ container: tr,
1657
+ tag: 'td',
1658
+ content: [cell.innerHTML],
1659
+ props:
1660
+ {
1661
+ colSpan: cell.colSpan || undefined,
1662
+ rowSpan: cell.rowSpan || undefined
1663
+ },
1664
+ attrs: { style: cell.getAttribute?.('style') }
1665
+ });
1666
+ });
1667
+ });
1668
+
1669
+ return {
1670
+ html: tableEl.outerHTML,
1671
+ text: tableEl.innerText
1672
+ };
1673
+ }
1674
+
1675
+ /**
1676
+ * Pastes the source table cells into the target table at the current selection.
1677
+ * @param {any} sourceTable
1678
+ * @param {any} table
1679
+ * @param {any} cell
1680
+ * @ignore
1681
+ */
1682
+ #applyPaste(sourceTable, table, cell)
1683
+ {
1684
+ const startRowIndex = this.#rowIndex(table, cell.parentElement),
1685
+ startColIndex = this.#cellColIndex(cell.parentElement, cell),
1686
+ tableIdAttr = this.utility.tableIdAttr,
1687
+ tempId = $lib.guid();
1688
+
1689
+ table.setAttribute(tableIdAttr, tempId);
1690
+
1691
+ this.#preparePasteArea(sourceTable, table, cell, startRowIndex, startColIndex);
1692
+ this.#copyCells(sourceTable, table, startRowIndex, startColIndex);
1693
+
1694
+ setTimeout(() =>
1695
+ {
1696
+ const restoredTable = this.editor.getEditorElement().querySelector(`[${tableIdAttr}="${tempId}"]`);
1697
+
1698
+ if (!restoredTable)
1699
+ return;
1700
+
1701
+ restoredTable.removeAttribute(tableIdAttr);
1702
+
1703
+ const restoredCell = this.#cellAtColumnIndex(restoredTable.rows[startRowIndex], startColIndex);
1704
+ this.#rebuildColgroup(restoredTable);
1705
+ this.#finalize(restoredCell);
1706
+ }, 0);
1707
+ }
1708
+
1709
+ /**
1710
+ * Expands the table if needed and normalizes the paste region by unmerging conflicting spans.
1711
+ * @param {any} sourceTable
1712
+ * @param {any} table
1713
+ * @param {any} cell
1714
+ * @param {any} startRowIndex
1715
+ * @param {any} startColIndex
1716
+ * @ignore
1717
+ */
1718
+ #preparePasteArea(sourceTable, table, cell, startRowIndex, startColIndex)
1719
+ {
1720
+ const srcMaxCols = this.#maxColCount(sourceTable),
1721
+ srcRows = sourceTable.rows.length,
1722
+ section = cell.closest('tbody') || cell.closest('thead') || cell.closest('tfoot') || table;
1723
+
1724
+ // Add rows to current table
1725
+ while (table.rows.length < startRowIndex + srcRows)
1726
+ {
1727
+ const newRow = $lib.element({ container: section, tag: 'tr' });
1728
+
1729
+ for (let index = 0; index < this.#rowColCount(table.rows[0]); index++)
1730
+ newRow.appendChild(this.#createCell());
1731
+ }
1732
+
1733
+ const requiredCols = startColIndex + srcMaxCols,
1734
+ currentCols = this.#rowColCount(table.rows[0]),
1735
+ diff = requiredCols - currentCols;
1736
+
1737
+ if (diff > 0) // Expand all rows to new column width
1738
+ {
1739
+ for (const row of table.rows)
1740
+ {
1741
+ for (let index = 0; index < diff; index++)
1742
+ row.appendChild(this.#createCell());
1743
+ }
1744
+ }
1745
+
1746
+ // Unmerge all cells in the paste rectangle so we have a clean flat grid
1747
+ for (let r = startRowIndex; r < startRowIndex + srcRows; r++)
1748
+ {
1749
+ for (let c = startColIndex; c < startColIndex + srcMaxCols; c++)
1750
+ {
1751
+ const existing = this.#cellAtColumnIndex(table.rows[r], c);
1752
+ if (existing && ((existing.colSpan || 1) > 1 || (existing.rowSpan || 1) > 1))
1753
+ this.#splitCell(table, existing);
1754
+ }
1755
+ }
1756
+ }
1757
+
1758
+ /**
1759
+ * Copies cell content and spanning info from source to target table.
1760
+ * @param {any} sourceTable
1761
+ * @param {any} table
1762
+ * @param {any} startRowIndex
1763
+ * @param {any} startColIndex
1764
+ * @ignore
1765
+ */
1766
+ #copyCells(sourceTable, table, startRowIndex, startColIndex)
1767
+ {
1768
+ Array.from(sourceTable.rows).forEach((sourceRow, rowOffset) =>
1769
+ {
1770
+ let colOffset = 0;
1771
+ Array.from(sourceRow.cells).forEach(sourceCell =>
1772
+ {
1773
+ const targetRow = table.rows[startRowIndex + rowOffset],
1774
+ targetCol = this.#cellAtColumnIndex(targetRow, startColIndex + colOffset);
1775
+ if (!targetCol)
1776
+ return;
1777
+
1778
+ targetCol.innerHTML = sourceCell.innerHTML;
1779
+ targetCol.colSpan = sourceCell.colSpan || 1;
1780
+ targetCol.rowSpan = sourceCell.rowSpan || 1;
1781
+
1782
+ if (sourceCell.hasAttribute('style'))
1783
+ targetCol.setAttribute('style', sourceCell.getAttribute('style'));
1784
+ else
1785
+ targetCol.removeAttribute('style');
1786
+
1787
+ // Remove cells now covered by this spanning cell
1788
+ for (let rowIndex = 1; rowIndex < (targetCol.rowSpan || 1); rowIndex++)
1789
+ for (let colIndex = 0; colIndex < (targetCol.colSpan || 1); colIndex++)
1790
+ {
1791
+ const covered = this.#cellAtColumnIndex(table.rows[startRowIndex + rowOffset + rowIndex], startColIndex + colOffset + colIndex);
1792
+ if (covered)
1793
+ covered.remove();
1794
+ }
1795
+
1796
+ colOffset += sourceCell.colSpan || 1;
1797
+ });
1798
+ });
1799
+ }
1800
+
1801
+ // ── Selection ────────────────────────────────────────────────────────────
1802
+
1803
+ /**
1804
+ * Selects a rectangular range of cells, expanding to fully include
1805
+ * any partially covered rowspan/colspan cells.
1806
+ * @ignore
1807
+ */
1808
+ #selectRange(start, end)
1809
+ {
1810
+ const table = start.closest('table');
1811
+
1812
+ // STEP 1: Build visual grid (rowspan/colspan resolved)
1813
+ const grid = this.#buildGridMap(table);
1814
+
1815
+ // STEP 2: Get cell positions in grid
1816
+ const startPos = this.#cellOrigin(grid, start);
1817
+ const endPos = this.#cellOrigin(grid, end);
1818
+
1819
+ if (!startPos || !endPos) return;
1820
+
1821
+ // STEP 3: Initial rectangle
1822
+ let minRow = Math.min(startPos.r, endPos.r);
1823
+ let maxRow = Math.max(startPos.r, endPos.r);
1824
+ let minCol = Math.min(startPos.c, endPos.c);
1825
+ let maxCol = Math.max(startPos.c, endPos.c);
1826
+
1827
+ // STEP 4: Expand until stable (respect full spans)
1828
+ let expanded = true;
1829
+
1830
+ while (expanded)
1831
+ {
1832
+ expanded = false;
1833
+
1834
+ for (let r = minRow; r <= maxRow; r++)
1835
+ {
1836
+ for (let c = minCol; c <= maxCol; c++)
1837
+ {
1838
+ const cell = grid[r]?.[c];
1839
+ if (!cell) continue;
1840
+
1841
+ const bounds = this.#getCellBounds(cell, grid);
1842
+
1843
+ if (bounds.minRow < minRow)
1844
+ {
1845
+ minRow = bounds.minRow;
1846
+ expanded = true;
1847
+ }
1848
+
1849
+ if (bounds.minCol < minCol)
1850
+ {
1851
+ minCol = bounds.minCol;
1852
+ expanded = true;
1853
+ }
1854
+
1855
+ if (bounds.maxRow > maxRow)
1856
+ {
1857
+ maxRow = bounds.maxRow;
1858
+ expanded = true;
1859
+ }
1860
+
1861
+ if (bounds.maxCol > maxCol)
1862
+ {
1863
+ maxCol = bounds.maxCol;
1864
+ expanded = true;
1865
+ }
1866
+ }
1867
+ }
1868
+ }
1869
+
1870
+ // STEP 5: Collect final selection
1871
+ this.#selectedCells = new Set();
1872
+
1873
+ for (let r = minRow; r <= maxRow; r++)
1874
+ {
1875
+ for (let c = minCol; c <= maxCol; c++)
1876
+ {
1877
+ const cell = grid[r]?.[c];
1878
+ if (cell)
1879
+ {
1880
+ this.#selectedCells.add(cell);
1881
+ }
1882
+ }
1883
+ }
1884
+ }
1885
+
1886
+ /**
1887
+ * Builds a visual grid map of the table where each (row, col) position points to the actual DOM cell.
1888
+ * Handles rowspan/colspan by assigning spanning cells to all occupied visual slots.
1889
+ * This grid represents visual layout, not DOM structure.
1890
+ * @ignore
1891
+ */
1892
+ #buildGridMap(table)
1893
+ {
1894
+ const map = [],
1895
+ rows = Array.from(table.rows);
1896
+
1897
+ rows.forEach((row, rowIndex) =>
1898
+ {
1899
+ if (!map[rowIndex]) map[rowIndex] = [];
1900
+
1901
+ let col = 0;
1902
+ Array.from(row.children).forEach(cell =>
1903
+ {
1904
+ // Skip slots already claimed by a rowspanning cell from above
1905
+ while (map[rowIndex][col]) col++;
1906
+
1907
+ const rowSpan = cell.rowSpan || 1,
1908
+ colSpan = cell.colSpan || 1;
1909
+
1910
+ for (let r = 0; r < rowSpan; r++)
1911
+ {
1912
+ for (let c = 0; c < colSpan; c++)
1913
+ {
1914
+ if (!map[rowIndex + r])
1915
+ map[rowIndex + r] = [];
1916
+
1917
+ map[rowIndex + r][col + c] = cell; // same cell DOM reference used for all covered visual slots
1918
+ }
1919
+ }
1920
+
1921
+ col += colSpan;
1922
+ });
1923
+ });
1924
+
1925
+ return map;
1926
+ }
1927
+
1928
+ /**
1929
+ * Returns the top-left grid position of a cell within the table grid.
1930
+ * @ignore
1931
+ */
1932
+ #cellOrigin(grid, cell)
1933
+ {
1934
+ for (let r = 0; r < grid.length; r++)
1935
+ {
1936
+ for (let c = 0; c < (grid[r]?.length || 0); c++)
1937
+ {
1938
+ if (grid[r][c] === cell)
1939
+ {
1940
+ return { r, c };
1941
+ }
1942
+ }
1943
+ }
1944
+ return null;
1945
+ }
1946
+
1947
+ /**
1948
+ * Returns the full grid bounds occupied by a cell (including spans).
1949
+ * @ignore
1950
+ */
1951
+ #getCellBounds(cell, grid)
1952
+ {
1953
+ const origin = this.#cellOrigin(grid, cell),
1954
+ rowSpan = cell.rowSpan || 1,
1955
+ colSpan = cell.colSpan || 1;
1956
+
1957
+ return {
1958
+ minRow: origin.r,
1959
+ maxRow: origin.r + rowSpan - 1,
1960
+ minCol: origin.c,
1961
+ maxCol: origin.c + colSpan - 1
1962
+ };
1963
+ }
1964
+
1965
+ #selectCell(cell, placeCaret = true)
1966
+ {
1967
+ if (!cell)
1968
+ return;
1969
+
1970
+ this.#clearHighlights();
1971
+ this.#selectedCells = new Set([cell]);
1972
+ this.#anchorCell = cell;
1973
+ this.#applyHighlights();
1974
+
1975
+ if (!placeCaret)
1976
+ return;
1977
+
1978
+ const range = this.editor.selectionRange.getRange();
1979
+ if (!cell.contains(range.startContainer) || !range.collapsed)
1980
+ this.#placeCaret(cell, true);
1981
+ }
1982
+
1983
+ #clearHighlights()
1984
+ {
1985
+ this.#selectedCells.forEach(c =>
1986
+ {
1987
+ c.classList.remove(this.#selectedCssClass, this.#anchorCssClass);
1988
+ if (!c.className)
1989
+ c.removeAttribute('class');
1990
+ });
1991
+
1992
+ if (this.#anchorCell)
1993
+ {
1994
+ this.#anchorCell.classList.remove(this.#anchorCssClass);
1995
+ if (!this.#anchorCell.className)
1996
+ this.#anchorCell.removeAttribute('class');
1997
+ }
1998
+ }
1999
+
2000
+ #applyHighlights()
2001
+ {
2002
+ if (this.#selectedCells.size > 1)
2003
+ this.#selectedCells.forEach(c => c.classList.add(this.#selectedCssClass));
2004
+
2005
+ if (this.#anchorCell)
2006
+ this.#anchorCell.classList.add(this.#anchorCssClass);
2007
+ }
2008
+
2009
+ #getSelectedCells(table)
2010
+ {
2011
+ return Array.from(this.#selectedCells);
2012
+ }
2013
+
2014
+ #getSelectedRows(table) { return [...new Set(this.#getSelectedCells(table).map(c => c.parentElement))]; }
2015
+
2016
+
2017
+ // ── Helpers ──────────────────────────────────────────────────────────────
2018
+
2019
+ #context()
2020
+ {
2021
+ const selection = this.editor.selectionRange,
2022
+ range = selection.getRange(),
2023
+ nodes = selection.getNodesInRange(range),
2024
+ rootNodes = selection.getRootNodesInRange(range, nodes),
2025
+ rootNode = rootNodes[0];
2026
+
2027
+ let cell = rootNode?.closest('td,th') || this.#contextCell || this.#firstSelectedCell();
2028
+
2029
+ if (!cell?.isConnected)
2030
+ cell = null;
2031
+
2032
+ return { cell, table: cell?.closest('table') };
2033
+ }
2034
+
2035
+ #handleCaretOutsideCell(e)
2036
+ {
2037
+ const range = this.editor.selectionRange.getRange(),
2038
+ startContainer = range.startContainer,
2039
+ el = startContainer.nodeType === Node.TEXT_NODE ? startContainer.parentElement : startContainer,
2040
+ figure = el.closest(`figure.${this.#wrapperCssClass}`),
2041
+ isPrintable = this.utility.isPrintableChar(e);
2042
+
2043
+ let firstCell = el.closest('td,th');
2044
+
2045
+ if (figure && !firstCell) // Caret is between figure and table element - do not allow typing here, move to first cell
2046
+ {
2047
+ if (range.collapsed)
2048
+ {
2049
+ firstCell = figure.querySelector('td,th');
2050
+ if (firstCell)
2051
+ this.#placeCaret(firstCell, true);
2052
+ }
2053
+ else if (isPrintable || e.key === 'Delete' || e.key === 'Backspace')
2054
+ {
2055
+ // Clamp start of range to the first cell's content
2056
+ firstCell = figure.querySelector('td,th');
2057
+ if (firstCell)
2058
+ {
2059
+ range.setStart(firstCell.firstElementChild, 0);
2060
+ this.editor.selectionRange.ensureTextRange(range);
2061
+ this.editor.selectionRange.restoreRange(range);
2062
+ }
2063
+ }
2064
+ }
2065
+ }
2066
+
2067
+ #collapseMultiCellSelection()
2068
+ {
2069
+ const range = this.editor.selectionRange.getRange();
2070
+
2071
+ if (range.collapsed)
2072
+ return;
2073
+
2074
+ const startContainer = range.startContainer,
2075
+ endContainer = range.endContainer,
2076
+ firstEl = startContainer.nodeType === Node.TEXT_NODE ? startContainer.parentElement : startContainer,
2077
+ lastEl = endContainer.nodeType === Node.TEXT_NODE ? endContainer.parentElement : endContainer,
2078
+ firstCell = firstEl.closest('td, th'),
2079
+ lastCell = lastEl.closest('td, th');
2080
+
2081
+ if (firstCell && firstCell !== lastCell) // selection spawns multiple cells
2082
+ {
2083
+ const grid = this.#buildGridMap(firstCell.closest('table')),
2084
+ anchorPos = this.#cellOrigin(grid, firstCell),
2085
+ cellPos = (lastCell) ? this.#cellOrigin(grid, lastCell) : null;
2086
+ (lastCell) ? cellPos.r > anchorPos.r || (cellPos.r === anchorPos.r && cellPos.c > anchorPos.c) : true;
2087
+
2088
+ this.editor.getDoc().defaultView.requestAnimationFrame(() =>
2089
+ {
2090
+ this.#placeCaret(firstCell, true);
2091
+ });
2092
+ }
2093
+ }
2094
+
2095
+ #ensureCellParagraph(cell)
2096
+ {
2097
+ if (!cell)
2098
+ return;
2099
+
2100
+ const selection = this.editor.selectionRange,
2101
+ range = selection.getRange();
2102
+
2103
+ // Wrap any stray inline/text nodes directly in cell into a <p>
2104
+ const strayNodes = Array.from(cell.childNodes).filter(n => n.nodeType === Node.TEXT_NODE || (n.nodeType === Node.ELEMENT_NODE && !this.editor.nodeManager.isBlock(n)));
2105
+
2106
+ if (strayNodes.length)
2107
+ {
2108
+ const marker = selection.createRangeMarker(range);
2109
+ const p = $lib.element({ tag: 'p' });
2110
+ cell.insertBefore(p, strayNodes[0]);
2111
+ strayNodes.forEach(n => p.appendChild(n));
2112
+
2113
+ if (!p.firstChild || p.firstChild.nodeName === 'BR')
2114
+ {
2115
+ p.innerHTML = '';
2116
+ $lib.element({ container: p, tag: 'br' });
2117
+ }
2118
+
2119
+ selection.restoreRangeToMarker(marker, range);
2120
+ return;
2121
+ }
2122
+
2123
+ // If cell has no block at all, insert <p><br>
2124
+ const hasBlock = Array.from(cell.children).some(n => this.editor.nodeManager.isBlock(n));
2125
+
2126
+ if (!hasBlock)
2127
+ {
2128
+ const p = $lib.element({ tag: 'p' }),
2129
+ br = $lib.element({ container: p, tag: 'br' });
2130
+ selection.insertIntoRange(range, p, br);
2131
+ selection.restoreRange(range);
2132
+ return;
2133
+ }
2134
+
2135
+ // If caret is at cell level, move it into the appropriate child
2136
+ if (range.startContainer === cell)
2137
+ {
2138
+ const target = cell.children[range.startOffset] || cell.lastElementChild;
2139
+ range.selectNodeContents(target);
2140
+ range.collapse(true);
2141
+ selection.ensureTextRange(range);
2142
+ selection.restoreRange(range);
2143
+ }
2144
+
2145
+ Array.from(cell.children).filter(n => this.editor.nodeManager.isBlock(n) && !n.firstChild).forEach(n => $lib.element({ container: n, tag: 'br' })); // Make sure empty block has at least <br> tag, otherwise caret can't be set.
2146
+ }
2147
+
2148
+ #firstSelectedCell()
2149
+ {
2150
+ return this.#selectedCells.values().next().value ?? null;
2151
+ }
2152
+
2153
+ #usesPercentages(table)
2154
+ {
2155
+ const w = table.style.width || '';
2156
+ return w === '' || w === '100%' || w.endsWith('%');
2157
+ }
2158
+
2159
+ #createCell(container = null)
2160
+ {
2161
+ const p = $lib.element({ tag: 'p' });
2162
+ $lib.element({ container: p, tag: 'br' });
2163
+
2164
+ return $lib.element({ tag: 'td', container, content: p });
2165
+ }
2166
+
2167
+ #rebuildColgroup(table, splitAt = -1)
2168
+ {
2169
+ const cg = table.querySelector('colgroup');
2170
+ if (!cg) return;
2171
+
2172
+ const totalCols = this.#rowColCount(table.rows[0]),
2173
+ usePercent = this.#usesPercentages(table),
2174
+ totalWidth = table.offsetWidth,
2175
+ currentWidths = Array.from(cg.children).map(col => col.offsetWidth);
2176
+
2177
+ currentWidths.splice(splitAt + 1, 0, 0); // insert placeholder for new col
2178
+
2179
+ cg.innerHTML = '';
2180
+
2181
+ for (let index = 0; index < totalCols; index++)
2182
+ {
2183
+ const col = $lib.element({ container: cg, tag: 'col' });
2184
+ let px;
2185
+
2186
+ if (splitAt >= 0 && (index === splitAt || index === splitAt + 1))
2187
+ {
2188
+ const halfSize = Math.floor(currentWidths[splitAt] / 2);
2189
+
2190
+ if (index === splitAt)
2191
+ px = halfSize;
2192
+ else // index === splitAt + 1
2193
+ px = currentWidths[splitAt] - halfSize; // remainder avoids losing px to rounding
2194
+ }
2195
+ else
2196
+ px = currentWidths[index] || (totalWidth / totalCols);
2197
+
2198
+ col.style.width = usePercent ? (px / totalWidth * 100).toFixed(2) + '%' : px.toFixed(0) + 'px';
2199
+ }
2200
+ }
2201
+
2202
+ #ensureSection(table, tag)
2203
+ {
2204
+ let section = table.querySelector(tag);
2205
+ if (!section)
2206
+ {
2207
+ const tfoot = table.querySelector('tfoot');
2208
+ section = $lib.element({ container: table, tag, before: tag === 'tbody' && tfoot ? tfoot : null });
2209
+ }
2210
+ return section;
2211
+ }
2212
+
2213
+ #maxColCount(table)
2214
+ {
2215
+ let max = 0;
2216
+
2217
+ for (const row of table.rows)
2218
+ max = Math.max(max, this.#rowColCount(row));
2219
+
2220
+ return max;
2221
+ }
2222
+
2223
+ /**
2224
+ * Returns the column count for the given row. We only support rectangular table's so any row should return correct max column count.
2225
+ * @param {any} row
2226
+ * @returns {number} The column count.
2227
+ * @ignore
2228
+ */
2229
+ #rowColCount(row)
2230
+ {
2231
+ let colCount = 0;
2232
+
2233
+ for (const c of row.cells)
2234
+ colCount += c.colSpan || 1;
2235
+
2236
+ return colCount;
2237
+ }
2238
+
2239
+ #cellColIndex(row, cell) { return Array.from(row.cells).indexOf(cell); }
2240
+
2241
+ #rowIndex(table, row) { return Array.from(table.rows).indexOf(row); }
2242
+
2243
+ /**
2244
+ * Resolves the column index of the given cell considering spans.
2245
+ * @param {any} row
2246
+ * @param {any} cell
2247
+ * @ignore
2248
+ * @returns {number} The column index of the cell.
2249
+ */
2250
+ #columnIndexOfCell(row, cell)
2251
+ {
2252
+ let index = 0;
2253
+ for (const c of row.cells)
2254
+ {
2255
+ if (c === cell)
2256
+ return index;
2257
+
2258
+ index += c.colSpan || 1;
2259
+ }
2260
+ return -1;
2261
+ }
2262
+
2263
+ /**
2264
+ * Resolves the cell for the given column index.
2265
+ * @param {any} row The row.
2266
+ * @param {any} colIndex The real spanned column index.
2267
+ * @ignore
2268
+ * @returns {HTMLTableCellElement|null} The resolved cell.
2269
+ */
2270
+ #cellAtColumnIndex(row, colIndex)
2271
+ {
2272
+ let colCount = 0;
2273
+ for (const cell of row.cells)
2274
+ {
2275
+ const span = cell.colSpan || 1;
2276
+ if (colCount <= colIndex && colIndex < colCount + span)
2277
+ return cell;
2278
+
2279
+ colCount += span;
2280
+ }
2281
+ return null;
2282
+ }
2283
+
2284
+ #cols(table)
2285
+ {
2286
+ return Array.from(table.querySelectorAll(':scope > colgroup > col'));
2287
+ }
2288
+
2289
+ #clearCells(cells)
2290
+ {
2291
+ cells.forEach(cell =>
2292
+ {
2293
+ const p = $lib.element({ tag: 'p' });
2294
+ $lib.element({ container: p, tag: 'br' });
2295
+ cell.innerHTML = '';
2296
+ cell.appendChild(p);
2297
+ });
2298
+ }
2299
+
2300
+ #isSameTable(cellA, cellB)
2301
+ {
2302
+ return cellA?.closest('table') === cellB?.closest('table');
2303
+ }
2304
+
2305
+ #finalize(cell)
2306
+ {
2307
+ const table = cell?.closest('table');
2308
+
2309
+ this.clearSelection();
2310
+ this.removeMenuButton();
2311
+
2312
+ if (table)
2313
+ this.#removeHandles(table);
2314
+
2315
+ this.editor.history.addItem();
2316
+
2317
+ if (table)
2318
+ this.#buildHandles(table);
2319
+
2320
+ if (cell)
2321
+ this.#selectCell(cell);
2322
+ }
2323
+ };
2324
+
2325
+ var Table = componyx.UI.editor_modules.Table;
2326
+
2327
+ export { Table as default };