ep_data_tables 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4441 @@
1
+ /* ep_data_tables – attribute‑based tables (line‑class + PostWrite renderer)
2
+ * -----------------------------------------------------------------
3
+ * Strategy
4
+ * • One line attribute tbljson = JSON({tblId,row,cells:[{txt:"…"},…]})
5
+ * • One char‑range attr td = column‑index (string)
6
+ * • `aceLineAttribsToClasses` puts class `tbl-line` on the **line div** so
7
+ * we can catch it once per line in `acePostWriteDomLineHTML`.
8
+ * • Renderer accumulates rows that share the same tblId in a buffer on
9
+ * innerdocbody, flushes to a single <table> when the run ends.
10
+ * • No raw JSON text is ever visible to the user.
11
+ */
12
+
13
+ /* eslint-env browser */
14
+
15
+ // ────────────────────────────── constants ──────────────────────────────
16
+ const ATTR_TABLE_JSON = 'tbljson';
17
+ const ATTR_CELL = 'td';
18
+ const ATTR_CLASS_PREFIX = 'tbljson-'; // For finding the class in DOM
19
+ const log = (...m) => console.debug('[ep_data_tables:client_hooks]', ...m);
20
+ const DELIMITER = '\u241F'; // Internal column delimiter (␟)
21
+ // Use the same rare character inside the hidden span so acePostWriteDomLineHTML can
22
+ // still find delimiters when it splits node.innerHTML.
23
+ // Users never see this because the span is contenteditable=false and styled away.
24
+ const HIDDEN_DELIM = DELIMITER;
25
+
26
+ // helper for stable random ids
27
+ const rand = () => Math.random().toString(36).slice(2, 8);
28
+
29
+ // encode/decode so JSON can survive as a CSS class token if ever needed
30
+ const enc = s => btoa(s).replace(/\+/g, '-').replace(/\//g, '_');
31
+ const dec = s => {
32
+ // Revert to simpler decode, assuming enc provides valid padding
33
+ const str = s.replace(/-/g, '+').replace(/_/g, '/');
34
+ try {
35
+ if (typeof atob === 'function') {
36
+ return atob(str); // Browser environment
37
+ } else if (typeof Buffer === 'function') {
38
+ // Node.js environment
39
+ return Buffer.from(str, 'base64').toString('utf8');
40
+ } else {
41
+ console.error('[ep_data_tables] Base64 decoding function (atob or Buffer) not found.');
42
+ return null;
43
+ }
44
+ } catch (e) {
45
+ console.error('[ep_data_tables] Error decoding base64 string:', s, e);
46
+ return null;
47
+ }
48
+ };
49
+
50
+ // NEW: Module-level state for last clicked cell
51
+ let lastClickedCellInfo = null; // { lineNum: number, cellIndex: number, tblId: string }
52
+
53
+ // NEW: Module-level state for column resizing (similar to images plugin)
54
+ let isResizing = false;
55
+ let resizeStartX = 0;
56
+ let resizeCurrentX = 0; // Track current mouse position
57
+ let resizeTargetTable = null;
58
+ let resizeTargetColumn = -1;
59
+ let resizeOriginalWidths = [];
60
+ let resizeTableMetadata = null;
61
+ let resizeLineNum = -1;
62
+ let resizeOverlay = null; // Visual overlay element
63
+
64
+ // ─────────────────── Reusable Helper Functions ───────────────────
65
+
66
+ /**
67
+ * Recursively search for an element with a 'tbljson-' class inside a given element.
68
+ * This is used to find the metadata carrier when it's nested inside block elements.
69
+ * @param {HTMLElement} element - The root element to start searching from.
70
+ * @returns {HTMLElement|null} - The found element or null.
71
+ */
72
+ function findTbljsonElement(element) {
73
+ if (!element) return null;
74
+ // Check if this element has the tbljson class
75
+ if (element.classList) {
76
+ for (const cls of element.classList) {
77
+ if (cls.startsWith(ATTR_CLASS_PREFIX)) {
78
+ return element;
79
+ }
80
+ }
81
+ }
82
+ // Recursively check children
83
+ if (element.children) {
84
+ for (const child of element.children) {
85
+ const found = findTbljsonElement(child);
86
+ if (found) return found;
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Gets the table metadata for a given line, falling back to a DOM search if the
94
+ * line attribute is not present (e.g., for block-styled lines).
95
+ * @param {number} lineNum - The line number.
96
+ * @param {object} editorInfo - The editor instance.
97
+ * @param {object} docManager - The document attribute manager.
98
+ * @returns {object|null} - The parsed metadata object or null.
99
+ */
100
+ function getTableLineMetadata(lineNum, editorInfo, docManager) {
101
+ const funcName = 'getTableLineMetadata';
102
+ try {
103
+ // First, try the fast path: getting the attribute directly from the line.
104
+ const attribs = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
105
+ if (attribs) {
106
+ try {
107
+ const metadata = JSON.parse(attribs);
108
+ if (metadata && metadata.tblId) {
109
+ // log(`${funcName}: Found metadata via attribute for line ${lineNum}`);
110
+ return metadata;
111
+ }
112
+ } catch (e) {
113
+ // log(`${funcName}: Invalid JSON in tbljson attribute on line ${lineNum}:`, e.message);
114
+ }
115
+ }
116
+
117
+ // Fallback for block-styled lines.
118
+ // log(`${funcName}: No valid attribute on line ${lineNum}, checking DOM.`);
119
+ const rep = editorInfo.ace_getRep();
120
+
121
+ // This is the fix: Get the lineNode directly from the rep. It's more reliable
122
+ // than querying the DOM and avoids the ace_getOuterDoc() call which was failing.
123
+ const lineEntry = rep.lines.atIndex(lineNum);
124
+ const lineNode = lineEntry?.lineNode;
125
+
126
+ if (!lineNode) {
127
+ // log(`${funcName}: Could not find line node in rep for line ${lineNum}`);
128
+ return null;
129
+ }
130
+
131
+ const tbljsonElement = findTbljsonElement(lineNode);
132
+ if (tbljsonElement) {
133
+ for (const className of tbljsonElement.classList) {
134
+ if (className.startsWith(ATTR_CLASS_PREFIX)) {
135
+ const encodedData = className.substring(ATTR_CLASS_PREFIX.length);
136
+ try {
137
+ const decodedString = atob(encodedData);
138
+ const metadata = JSON.parse(decodedString);
139
+ // log(`${funcName}: Reconstructed metadata from DOM for line ${lineNum}:`, metadata);
140
+ return metadata;
141
+ } catch (e) {
142
+ console.error(`${funcName}: Failed to decode/parse tbljson class on line ${lineNum}:`, e);
143
+ return null;
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ // log(`${funcName}: Could not find table metadata for line ${lineNum} in DOM.`);
150
+ return null;
151
+ } catch (e) {
152
+ console.error(`[ep_data_tables] ${funcName}: Error getting metadata for line ${lineNum}:`, e);
153
+ return null;
154
+ }
155
+ }
156
+
157
+ // ─────────────────── Cell Navigation Helper Functions ───────────────────
158
+ /**
159
+ * Navigate to the next cell in the table (Tab key behavior)
160
+ * @param {number} currentLineNum - Current line number
161
+ * @param {number} currentCellIndex - Current cell index (0-based)
162
+ * @param {object} tableMetadata - Table metadata object
163
+ * @param {boolean} shiftKey - Whether Shift was held (for reverse navigation)
164
+ * @param {object} editorInfo - Editor instance
165
+ * @param {object} docManager - Document attribute manager
166
+ * @returns {boolean} - Success of navigation
167
+ */
168
+ function navigateToNextCell(currentLineNum, currentCellIndex, tableMetadata, shiftKey, editorInfo, docManager) {
169
+ const funcName = 'navigateToNextCell';
170
+ // log(`${funcName}: START - Current: Line=${currentLineNum}, Cell=${currentCellIndex}, Shift=${shiftKey}`);
171
+
172
+ try {
173
+ let targetRow = tableMetadata.row;
174
+ let targetCol = currentCellIndex;
175
+
176
+ if (shiftKey) {
177
+ // Shift+Tab: Move to previous cell
178
+ targetCol--;
179
+ if (targetCol < 0) {
180
+ // Move to last cell of previous row
181
+ targetRow--;
182
+ targetCol = tableMetadata.cols - 1;
183
+ }
184
+ } else {
185
+ // Tab: Move to next cell
186
+ targetCol++;
187
+ if (targetCol >= tableMetadata.cols) {
188
+ // Move to first cell of next row
189
+ targetRow++;
190
+ targetCol = 0;
191
+ }
192
+ }
193
+
194
+ // log(`${funcName}: Target coordinates - Row=${targetRow}, Col=${targetCol}`);
195
+
196
+ // Find the line number for the target row
197
+ const targetLineNum = findLineForTableRow(tableMetadata.tblId, targetRow, editorInfo, docManager);
198
+ if (targetLineNum === -1) {
199
+ // log(`${funcName}: Could not find line for target row ${targetRow}`);
200
+ return false;
201
+ }
202
+
203
+ // Navigate to the target cell
204
+ return navigateToCell(targetLineNum, targetCol, editorInfo, docManager);
205
+
206
+ } catch (e) {
207
+ console.error(`[ep_data_tables] ${funcName}: Error during navigation:`, e);
208
+ return false;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Navigate to the cell below in the same column (Enter key behavior)
214
+ * @param {number} currentLineNum - Current line number
215
+ * @param {number} currentCellIndex - Current cell index (0-based)
216
+ * @param {object} tableMetadata - Table metadata object
217
+ * @param {object} editorInfo - Editor instance
218
+ * @param {object} docManager - Document attribute manager
219
+ * @returns {boolean} - Success of navigation
220
+ */
221
+ function navigateToCellBelow(currentLineNum, currentCellIndex, tableMetadata, editorInfo, docManager) {
222
+ const funcName = 'navigateToCellBelow';
223
+ // log(`${funcName}: START - Current: Line=${currentLineNum}, Cell=${currentCellIndex}`);
224
+
225
+ try {
226
+ const targetRow = tableMetadata.row + 1;
227
+ const targetCol = currentCellIndex;
228
+
229
+ // log(`${funcName}: Target coordinates - Row=${targetRow}, Col=${targetCol}`);
230
+
231
+ // Find the line number for the target row
232
+ const targetLineNum = findLineForTableRow(tableMetadata.tblId, targetRow, editorInfo, docManager);
233
+
234
+ if (targetLineNum !== -1) {
235
+ // Found the row below, navigate to it.
236
+ // log(`${funcName}: Found line for target row ${targetRow}, navigating.`);
237
+ return navigateToCell(targetLineNum, targetCol, editorInfo, docManager);
238
+ } else {
239
+ // Could not find the row below, we must be on the last line.
240
+ // Create a new, empty line after the table.
241
+ // log(`${funcName}: Could not find next row. Creating new line after table.`);
242
+ const rep = editorInfo.ace_getRep();
243
+ const lineTextLength = rep.lines.atIndex(currentLineNum).text.length;
244
+ const endOfLinePos = [currentLineNum, lineTextLength];
245
+
246
+ // Move caret to end of the current line...
247
+ editorInfo.ace_performSelectionChange(endOfLinePos, endOfLinePos, false);
248
+ // ...and insert a newline character. This creates a new line below.
249
+ editorInfo.ace_performDocumentReplaceRange(endOfLinePos, endOfLinePos, '\n');
250
+
251
+ // The caret is automatically moved to the new line by the operation above,
252
+ // but we ensure the visual selection is synced and the editor is focused.
253
+ editorInfo.ace_updateBrowserSelectionFromRep();
254
+ editorInfo.ace_focus();
255
+
256
+ // We've now exited the table, so clear the last-clicked state.
257
+ const editor = editorInfo.editor;
258
+ if (editor) editor.ep_data_tables_last_clicked = null;
259
+ // log(`${funcName}: Cleared last click info as we have exited the table.`);
260
+
261
+ return true; // We handled it.
262
+ }
263
+ } catch (e) {
264
+ console.error(`[ep_data_tables] ${funcName}: Error during navigation:`, e);
265
+ return false;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Find the line number for a specific table row
271
+ * @param {string} tblId - Table ID
272
+ * @param {number} targetRow - Target row index
273
+ * @param {object} editorInfo - Editor instance
274
+ * @param {object} docManager - Document attribute manager
275
+ * @returns {number} - Line number (-1 if not found)
276
+ */
277
+ function findLineForTableRow(tblId, targetRow, editorInfo, docManager) {
278
+ const funcName = 'findLineForTableRow';
279
+ // log(`${funcName}: Searching for tblId=${tblId}, row=${targetRow}`);
280
+
281
+ try {
282
+ const rep = editorInfo.ace_getRep();
283
+ if (!rep || !rep.lines) {
284
+ // log(`${funcName}: Could not get rep or rep.lines`);
285
+ return -1;
286
+ }
287
+
288
+ const totalLines = rep.lines.length();
289
+ for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
290
+ try {
291
+ let lineAttrString = docManager.getAttributeOnLine(lineIndex, ATTR_TABLE_JSON);
292
+
293
+ // If no attribute found directly, check DOM (same logic as acePostWriteDomLineHTML)
294
+ if (!lineAttrString) {
295
+ const lineEntry = rep.lines.atIndex(lineIndex);
296
+ if (lineEntry && lineEntry.lineNode) {
297
+ const tableInDOM = lineEntry.lineNode.querySelector('table.dataTable[data-tblId]');
298
+ if (tableInDOM) {
299
+ const domTblId = tableInDOM.getAttribute('data-tblId');
300
+ const domRow = tableInDOM.getAttribute('data-row');
301
+ if (domTblId === tblId && domRow !== null && parseInt(domRow, 10) === targetRow) {
302
+ // log(`${funcName}: Found target via DOM: line ${lineIndex}`);
303
+ return lineIndex;
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ if (lineAttrString) {
310
+ const lineMetadata = JSON.parse(lineAttrString);
311
+ if (lineMetadata.tblId === tblId && lineMetadata.row === targetRow) {
312
+ // log(`${funcName}: Found target via attribute: line ${lineIndex}`);
313
+ return lineIndex;
314
+ }
315
+ }
316
+ } catch (e) {
317
+ continue; // Skip lines with invalid metadata
318
+ }
319
+ }
320
+
321
+ // log(`${funcName}: Target row not found`);
322
+ return -1;
323
+ } catch (e) {
324
+ console.error(`[ep_data_tables] ${funcName}: Error searching for line:`, e);
325
+ return -1;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Navigate to a specific cell and position caret at the end of its text
331
+ * @param {number} targetLineNum - Target line number
332
+ * @param {number} targetCellIndex - Target cell index (0-based)
333
+ * @param {object} editorInfo - Editor instance
334
+ * @param {object} docManager - Document attribute manager
335
+ * @returns {boolean} - Success of navigation
336
+ */
337
+ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager) {
338
+ const funcName = 'navigateToCell';
339
+ // log(`${funcName}: START - Target: Line=${targetLineNum}, Cell=${targetCellIndex}`);
340
+ let targetPos;
341
+
342
+ try {
343
+ const rep = editorInfo.ace_getRep();
344
+ if (!rep || !rep.lines) {
345
+ // log(`${funcName}: Could not get rep or rep.lines`);
346
+ return false;
347
+ }
348
+
349
+ const lineEntry = rep.lines.atIndex(targetLineNum);
350
+ if (!lineEntry) {
351
+ // log(`${funcName}: Could not get line entry for line ${targetLineNum}`);
352
+ return false;
353
+ }
354
+
355
+ const lineText = lineEntry.text || '';
356
+ const cells = lineText.split(DELIMITER);
357
+
358
+ if (targetCellIndex >= cells.length) {
359
+ // log(`${funcName}: Target cell ${targetCellIndex} doesn't exist (only ${cells.length} cells)`);
360
+ return false;
361
+ }
362
+
363
+ let targetCol = 0;
364
+ for (let i = 0; i < targetCellIndex; i++) {
365
+ targetCol += (cells[i]?.length ?? 0) + DELIMITER.length;
366
+ }
367
+ const targetCellContent = cells[targetCellIndex] || '';
368
+ targetCol += targetCellContent.length;
369
+
370
+ const clampedTargetCol = Math.min(targetCol, lineText.length);
371
+ targetPos = [targetLineNum, clampedTargetCol];
372
+
373
+ // --- NEW: Update plugin state BEFORE performing the UI action ---
374
+ try {
375
+ const editor = editorInfo.ep_data_tables_editor;
376
+ // Use the new robust helper to get metadata, which handles block-styled lines.
377
+ const tableMetadata = getTableLineMetadata(targetLineNum, editorInfo, docManager);
378
+
379
+ if (editor && tableMetadata) {
380
+ editor.ep_data_tables_last_clicked = {
381
+ lineNum: targetLineNum,
382
+ tblId: tableMetadata.tblId,
383
+ cellIndex: targetCellIndex,
384
+ relativePos: targetCellContent.length,
385
+ };
386
+ // log(`${funcName}: Pre-emptively updated stored click info:`, editor.ep_data_tables_last_clicked);
387
+ } else {
388
+ // log(`${funcName}: Could not get table metadata for target line ${targetLineNum}, cannot update click info.`);
389
+ }
390
+ } catch (e) {
391
+ // log(`${funcName}: Could not update stored click info before navigation:`, e.message);
392
+ }
393
+
394
+ // The previous attempts involving wrappers and poking the renderer have all
395
+ // proven to be unstable. The correct approach is to directly update the
396
+ // internal model and then tell the browser to sync its visual selection to
397
+ // that model.
398
+ try {
399
+ // 1. Update the internal representation of the selection.
400
+ editorInfo.ace_performSelectionChange(targetPos, targetPos, false);
401
+ // log(`${funcName}: Updated internal selection to [${targetPos}]`);
402
+
403
+ // 2. Explicitly tell the editor to update the browser's visual selection
404
+ // to match the new internal representation. This is the correct way to
405
+ // make the caret appear in the new location without causing a race condition.
406
+ editorInfo.ace_updateBrowserSelectionFromRep();
407
+ // log(`${funcName}: Called updateBrowserSelectionFromRep to sync visual caret.`);
408
+
409
+ // 3. Ensure the editor has focus.
410
+ editorInfo.ace_focus();
411
+ // log(`${funcName}: Editor focused.`);
412
+
413
+ } catch(e) {
414
+ console.error(`[ep_data_tables] ${funcName}: Error during direct navigation update:`, e);
415
+ return false;
416
+ }
417
+
418
+ } catch (e) {
419
+ // This synchronous catch is a fallback, though the error was happening asynchronously.
420
+ console.error(`[ep_data_tables] ${funcName}: Error during cell navigation:`, e);
421
+ return false;
422
+ }
423
+
424
+ // log(`${funcName}: Navigation considered successful.`);
425
+ return true;
426
+ }
427
+
428
+ // ────────────────────── collectContentPre (DOM → atext) ─────────────────────
429
+ exports.collectContentPre = (hook, ctx) => {
430
+ const funcName = 'collectContentPre';
431
+ const node = ctx.domNode; // Etherpad's collector uses ctx.domNode
432
+ const state = ctx.state;
433
+ const cc = ctx.cc; // ContentCollector instance
434
+
435
+ // log(`${funcName}: *** ENTRY POINT *** Hook: ${hook}, Node: ${node?.tagName}.${node?.className}`);
436
+
437
+ // ***** START Primary Path: Reconstruct from rendered table *****
438
+ if (node?.classList?.contains('ace-line')) {
439
+ const tableNode = node.querySelector('table.dataTable[data-tblId]');
440
+ if (tableNode) {
441
+ // log(`${funcName}: Found ace-line with rendered table. Attempting reconstruction from DOM.`);
442
+
443
+ const docManager = cc.documentAttributeManager;
444
+ const rep = cc.rep;
445
+ const lineNum = rep?.lines?.indexOfKey(node.id);
446
+
447
+ if (typeof lineNum === 'number' && lineNum >= 0 && docManager) {
448
+ // log(`${funcName}: Processing line ${lineNum} (NodeID: ${node.id}) for DOM reconstruction.`);
449
+ try {
450
+ const existingAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
451
+ // log(`${funcName}: Line ${lineNum} existing ${ATTR_TABLE_JSON} attribute: '${existingAttrString}'`);
452
+
453
+ if (existingAttrString) {
454
+ const existingMetadata = JSON.parse(existingAttrString);
455
+ if (existingMetadata && typeof existingMetadata.tblId !== 'undefined' &&
456
+ typeof existingMetadata.row !== 'undefined' && typeof existingMetadata.cols === 'number') {
457
+ // log(`${funcName}: Line ${lineNum} existing metadata is valid:`, existingMetadata);
458
+
459
+ const trNode = tableNode.querySelector('tbody > tr');
460
+ if (trNode) {
461
+ // log(`${funcName}: Line ${lineNum} found <tr> node for cell content extraction.`);
462
+ let cellHTMLSegments = Array.from(trNode.children).map((td, index) => {
463
+ let segmentHTML = td.innerHTML || '';
464
+ // log(`${funcName}: Line ${lineNum} TD[${index}] raw innerHTML (first 100): "${segmentHTML.substring(0,100)}"`);
465
+
466
+ const resizeHandleRegex = /<div class="ep-data_tables-resize-handle"[^>]*><\/div>/ig;
467
+ segmentHTML = segmentHTML.replace(resizeHandleRegex, '');
468
+ // NEW: Also remove any previously injected hidden delimiter span so we do
469
+ // not serialise it back into the atext. Leaving it in would duplicate the
470
+ // hidden span on every save-reload cycle and, more importantly, confuse the
471
+ // later HTML-to-table reconstruction because the delimiter that lives *inside*
472
+ // the span would be mistaken for a real cell boundary.
473
+ const hiddenDelimRegexPrimary = /<span class="ep-data_tables-delim"[^>]*>.*?<\/span>/ig;
474
+ segmentHTML = segmentHTML.replace(hiddenDelimRegexPrimary, '');
475
+
476
+ const hidden = index === 0 ? '' :
477
+ /* keep the char in the DOM but make it visually disappear and non-editable */
478
+ `<span class="ep-data_tables-delim" contenteditable="false">${HIDDEN_DELIM}</span>`;
479
+ // log(`${funcName}: Line ${lineNum} TD[${index}] cleaned innerHTML (first 100): "${segmentHTML.substring(0,100)}"`);
480
+ return segmentHTML;
481
+ });
482
+
483
+ if (cellHTMLSegments.length !== existingMetadata.cols) {
484
+ // log(`${funcName}: WARNING Line ${lineNum}: Reconstructed cell count (${cellHTMLSegments.length}) does not match metadata cols (${existingMetadata.cols}). Padding/truncating.`);
485
+ while (cellHTMLSegments.length < existingMetadata.cols) cellHTMLSegments.push('&nbsp;');
486
+ if (cellHTMLSegments.length > existingMetadata.cols) cellHTMLSegments.length = existingMetadata.cols;
487
+ }
488
+
489
+ const canonicalLineText = cellHTMLSegments.join(DELIMITER);
490
+ state.line = canonicalLineText;
491
+ // log(`${funcName}: Line ${lineNum} successfully reconstructed ctx.state.line: "${canonicalLineText.substring(0, 200)}..."`);
492
+
493
+ state.lineAttributes = state.lineAttributes || [];
494
+ state.lineAttributes = state.lineAttributes.filter(attr => attr[0] !== ATTR_TABLE_JSON);
495
+ state.lineAttributes.push([ATTR_TABLE_JSON, existingAttrString]);
496
+ // log(`${funcName}: Line ${lineNum} ensured ${ATTR_TABLE_JSON} attribute is in state.lineAttributes.`);
497
+
498
+ // log(`${funcName}: Line ${lineNum} reconstruction complete. Returning undefined to prevent default DOM collection.`);
499
+ return undefined;
500
+ } else {
501
+ // log(`${funcName}: ERROR Line ${lineNum}: Could not find tbody > tr in rendered table for reconstruction.`);
502
+ }
503
+ } else {
504
+ // log(`${funcName}: ERROR Line ${lineNum}: Invalid or incomplete existing metadata from line attribute:`, existingMetadata);
505
+ }
506
+ } else {
507
+ // log(`${funcName}: WARNING Line ${lineNum}: No existing ${ATTR_TABLE_JSON} attribute found for reconstruction, despite table DOM presence. Table may be malformed or attribute lost.`);
508
+ const domTblId = tableNode.getAttribute('data-tblId');
509
+ const domRow = tableNode.getAttribute('data-row');
510
+ const trNode = tableNode.querySelector('tbody > tr');
511
+ if (domTblId && domRow !== null && trNode && trNode.children.length > 0) {
512
+ // log(`${funcName}: Line ${lineNum} FALLBACK: Attempting reconstruction using table DOM attributes as ${ATTR_TABLE_JSON} was missing.`);
513
+ const domCols = trNode.children.length;
514
+ const tempMetadata = {tblId: domTblId, row: parseInt(domRow, 10), cols: domCols};
515
+ const tempAttrString = JSON.stringify(tempMetadata);
516
+ // log(`${funcName}: Line ${lineNum} FALLBACK: Constructed temporary metadata: ${tempAttrString}`);
517
+
518
+ let cellHTMLSegments = Array.from(trNode.children).map((td, index) => {
519
+ let segmentHTML = td.innerHTML || '';
520
+ const resizeHandleRegex = /<div class="ep-data_tables-resize-handle"[^>]*><\/div>/ig;
521
+ segmentHTML = segmentHTML.replace(resizeHandleRegex, '');
522
+ if (index > 0) {
523
+ const hiddenDelimRegex = new RegExp(`^<span class="ep-data_tables-delim" contenteditable="false">${DELIMITER}(<\\/span>)?<\\/span>`, 'i');
524
+ segmentHTML = segmentHTML.replace(hiddenDelimRegex, '');
525
+ }
526
+ return segmentHTML;
527
+ });
528
+
529
+ if (cellHTMLSegments.length !== domCols) {
530
+ // log(`${funcName}: WARNING Line ${lineNum} (Fallback): Reconstructed cell count (${cellHTMLSegments.length}) does not match DOM cols (${domCols}).`);
531
+ while(cellHTMLSegments.length < domCols) cellHTMLSegments.push('&nbsp;');
532
+ if(cellHTMLSegments.length > domCols) cellHTMLSegments.length = domCols;
533
+ }
534
+
535
+ const canonicalLineText = cellHTMLSegments.join(DELIMITER);
536
+ state.line = canonicalLineText;
537
+ state.lineAttributes = state.lineAttributes || [];
538
+ state.lineAttributes = state.lineAttributes.filter(attr => attr[0] !== ATTR_TABLE_JSON);
539
+ state.lineAttributes.push([ATTR_TABLE_JSON, tempAttrString]);
540
+ // log(`${funcName}: Line ${lineNum} FALLBACK: Successfully reconstructed line using DOM attributes. Returning undefined.`);
541
+ return undefined;
542
+ } else {
543
+ // log(`${funcName}: Line ${lineNum} FALLBACK: Could not reconstruct from DOM attributes due to missing info.`);
544
+ }
545
+ }
546
+ } catch (e) {
547
+ console.error(`[ep_data_tables] ${funcName}: Line ${lineNum} error during DOM reconstruction:`, e);
548
+ // log(`${funcName}: Line ${lineNum} Exception details:`, { message: e.message, stack: e.stack });
549
+ }
550
+ } else {
551
+ // log(`${funcName}: Could not get valid line number (${lineNum}), rep, or docManager for DOM reconstruction of ace-line.`);
552
+ }
553
+ } else {
554
+ // log(`${funcName}: Node is ace-line but no rendered table.dataTable[data-tblId] found. Allowing normal processing for: ${node?.className}`);
555
+ }
556
+ } else {
557
+ // log(`${funcName}: Node is not an ace-line (or node is null). Node: ${node?.tagName}.${node?.className}. Allowing normal processing.`);
558
+ }
559
+ // ***** END Primary Path *****
560
+
561
+
562
+ // ***** Secondary Path: Apply attributes from tbljson-* class on spans (for initial creation/pasting) *****
563
+ const classes = ctx.cls ? ctx.cls.split(' ') : [];
564
+ let appliedAttribFromClass = false;
565
+ if (classes.length > 0) {
566
+ // log(`${funcName}: Secondary path - Checking classes on node ${node?.tagName}.${node?.className}: [${classes.join(', ')}]`);
567
+ for (const cls of classes) {
568
+ if (cls.startsWith('tbljson-')) {
569
+ // log(`${funcName}: Secondary path - Found tbljson class: ${cls} on node ${node?.tagName}.${node?.className}`);
570
+ const encodedMetadata = cls.substring(8);
571
+ try {
572
+ const decodedMetadata = dec(encodedMetadata);
573
+ if (decodedMetadata) {
574
+ cc.doAttrib(state, `${ATTR_TABLE_JSON}::${decodedMetadata}`);
575
+ appliedAttribFromClass = true;
576
+ // log(`${funcName}: Secondary path - Applied attribute to OP via cc.doAttrib for class ${cls.substring(0, 20)}... on ${node?.tagName}`);
577
+ } else {
578
+ // log(`${funcName}: Secondary path - ERROR - Decoded metadata is null or empty for class ${cls}`);
579
+ }
580
+ } catch (e) {
581
+ console.error(`[ep_data_tables] ${funcName}: Secondary path - Error processing tbljson class ${cls} on ${node?.tagName}:`, e);
582
+ }
583
+ break;
584
+ }
585
+ }
586
+ if (!appliedAttribFromClass && classes.some(c => c.startsWith('tbljson-'))) {
587
+ // log(`${funcName}: Secondary path - Found tbljson- class but failed to apply attribute.`);
588
+ } else if (!classes.some(c => c.startsWith('tbljson-'))) {
589
+ // log(`${funcName}: Secondary path - No tbljson- class found on this node.`);
590
+ }
591
+ } else {
592
+ // log(`${funcName}: Secondary path - Node ${node?.tagName}.${node?.className} has no ctx.cls or classes array is empty.`);
593
+ }
594
+
595
+ // log(`${funcName}: *** EXIT POINT *** For Node: ${node?.tagName}.${node?.className}. Applied from class: ${appliedAttribFromClass}`);
596
+ };
597
+
598
+ // ───────────── attribute → span‑class mapping (linestylefilter hook) ─────────
599
+ exports.aceAttribsToClasses = (hook, ctx) => {
600
+ const funcName = 'aceAttribsToClasses';
601
+ // log(`>>>> ${funcName}: Called with key: ${ctx.key}`); // log entry
602
+ if (ctx.key === ATTR_TABLE_JSON) {
603
+ // log(`${funcName}: Processing ATTR_TABLE_JSON.`);
604
+ // ctx.value is the raw JSON string from Etherpad's attribute pool
605
+ const rawJsonValue = ctx.value;
606
+ // log(`${funcName}: Received raw attribute value (ctx.value):`, rawJsonValue);
607
+
608
+ // Attempt to parse for logging purposes
609
+ let parsedMetadataForLog = '[JSON Parse Error]';
610
+ try {
611
+ parsedMetadataForLog = JSON.parse(rawJsonValue);
612
+ // log(`${funcName}: Value parsed for logging:`, parsedMetadataForLog);
613
+ } catch(e) {
614
+ // log(`${funcName}: Error parsing raw JSON value for logging:`, e);
615
+ // Continue anyway, enc() might still work if it's just a string
616
+ }
617
+
618
+ // Generate the class name by base64 encoding the raw JSON string.
619
+ // This ensures acePostWriteDomLineHTML receives the expected encoded format.
620
+ const className = `tbljson-${enc(rawJsonValue)}`;
621
+ // log(`${funcName}: Generated class name by encoding raw JSON: ${className}`);
622
+ return [className];
623
+ }
624
+ if (ctx.key === ATTR_CELL) {
625
+ // Keep this in case we want cell-specific styling later
626
+ // // log(`${funcName}: Processing ATTR_CELL: ${ctx.value}`); // Optional: Uncomment if needed
627
+ return [`tblCell-${ctx.value}`];
628
+ }
629
+ // // log(`${funcName}: Processing other key: ${ctx.key}`); // Optional: Uncomment if needed
630
+ return [];
631
+ };
632
+
633
+ // ───────────── line‑class mapping (REMOVE - superseded by aceAttribsToClasses) ─────────
634
+ // exports.aceLineAttribsToClasses = ... (Removed as aceAttribsToClasses adds table-line-data now)
635
+
636
+ // ─────────────────── Create Initial DOM Structure ────────────────────
637
+ // REMOVED - This hook doesn't reliably trigger on attribute changes during creation.
638
+ // exports.aceCreateDomLine = (hookName, args, cb) => { ... };
639
+
640
+ // Helper function to escape HTML (Keep this helper)
641
+ function escapeHtml(text = '') {
642
+ const strText = String(text);
643
+ var map = {
644
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'
645
+ };
646
+ return strText.replace(/[&<>"'']/g, function(m) { return map[m]; });
647
+ }
648
+
649
+ // NEW Helper function to build table HTML from pre-rendered delimited content with resize handles
650
+ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
651
+ const funcName = 'buildTableFromDelimitedHTML';
652
+ // log(`${funcName}: START`, { metadata, innerHTMLSegments });
653
+
654
+ if (!metadata || typeof metadata.tblId === 'undefined' || typeof metadata.row === 'undefined') {
655
+ console.error(`[ep_data_tables] ${funcName}: Invalid or missing metadata. Aborting.`);
656
+ // log(`${funcName}: END - Error`);
657
+ return '<table class="dataTable dataTable-error"><tbody><tr><td>Error: Missing table metadata</td></tr></tbody></table>'; // Return error table
658
+ }
659
+
660
+ // Get column widths from metadata, or use equal distribution if not set
661
+ const numCols = innerHTMLSegments.length;
662
+ const columnWidths = metadata.columnWidths || Array(numCols).fill(100 / numCols);
663
+
664
+ // Ensure we have the right number of column widths
665
+ while (columnWidths.length < numCols) {
666
+ columnWidths.push(100 / numCols);
667
+ }
668
+ if (columnWidths.length > numCols) {
669
+ columnWidths.splice(numCols);
670
+ }
671
+
672
+ // Basic styling - can be moved to CSS later
673
+ const tdStyle = `padding: 5px 7px; word-wrap:break-word; vertical-align: top; border: 1px solid #000; position: relative;`; // Added position: relative
674
+
675
+ // Map the HTML segments directly into TD elements with column widths
676
+ const cellsHtml = innerHTMLSegments.map((segment, index) => {
677
+ // Build the hidden delimiter *inside* the first author span so the caret
678
+ // cannot sit between delimiter and text.
679
+ let modifiedSegment = segment || '&nbsp;';
680
+ if (index > 0) {
681
+ const delimSpan = `<span class="ep-data_tables-delim" contenteditable="false">${HIDDEN_DELIM}</span>`;
682
+ // If the rendered segment already starts with a <span …> (which will be
683
+ // the usual author-colour wrapper) inject the delimiter right after that
684
+ // opening tag; otherwise just prefix it.
685
+ modifiedSegment = modifiedSegment.replace(/^(<span[^>]*>)/i, `$1${delimSpan}`);
686
+ if (modifiedSegment === segment) modifiedSegment = `${delimSpan}${modifiedSegment}`;
687
+ }
688
+
689
+ // --- NEW: Always embed the invisible caret-anchor as *last* child *within* the first author span ---
690
+ const caretAnchorSpan = '<span class="ep-data_tables-caret-anchor" contenteditable="false"></span>';
691
+ const anchorInjected = modifiedSegment.replace(/<\/span>\s*$/i, `${caretAnchorSpan}</span>`);
692
+ modifiedSegment = (anchorInjected !== modifiedSegment) ? anchorInjected : `${modifiedSegment}${caretAnchorSpan}`;
693
+
694
+ // Width & other decorations remain unchanged
695
+ const widthPercent = columnWidths[index] || (100 / numCols);
696
+ const cellStyle = `${tdStyle} width: ${widthPercent}%;`;
697
+
698
+ const isLastColumn = index === innerHTMLSegments.length - 1;
699
+ const resizeHandle = !isLastColumn ?
700
+ `<div class="ep-data_tables-resize-handle" data-column="${index}" style="position: absolute; top: 0; right: -2px; width: 4px; height: 100%; cursor: col-resize; background: transparent; z-index: 10;"></div>` : '';
701
+
702
+ const tdContent = `<td style="${cellStyle}" data-column="${index}" draggable="false">${modifiedSegment}${resizeHandle}</td>`;
703
+ return tdContent;
704
+ }).join('');
705
+ // log(`${funcName}: Joined all cellsHtml:`, cellsHtml);
706
+
707
+ // Add 'dataTable-first-row' class if it's the logical first row (row index 0)
708
+ const firstRowClass = metadata.row === 0 ? ' dataTable-first-row' : '';
709
+ // log(`${funcName}: First row class applied: '${firstRowClass}'`);
710
+
711
+ // Construct the final table HTML
712
+ // Rely on CSS for border-collapse, width etc. Add data attributes from metadata.
713
+ const tableHtml = `<table class="dataTable${firstRowClass}" data-tblId="${metadata.tblId}" data-row="${metadata.row}" style="width:100%; border-collapse: collapse; table-layout: fixed;" draggable="false"><tbody><tr>${cellsHtml}</tr></tbody></table>`;
714
+ // log(`${funcName}: Generated final table HTML:`, tableHtml);
715
+ // log(`${funcName}: END - Success`);
716
+ return tableHtml;
717
+ }
718
+
719
+ // ───────────────── Populate Table Cells / Render (PostWrite) ──────────────────
720
+ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
721
+ const funcName = 'acePostWriteDomLineHTML';
722
+ const node = args?.node;
723
+ const nodeId = node?.id;
724
+ const lineNum = args?.lineNumber; // Etherpad >= 1.9 provides lineNumber
725
+ const logPrefix = '[ep_data_tables:acePostWriteDomLineHTML]'; // Consistent prefix
726
+
727
+ // *** STARTUP LOGGING ***
728
+ // log(`${logPrefix} ----- START ----- NodeID: ${nodeId} LineNum: ${lineNum}`);
729
+ if (!node || !nodeId) {
730
+ // log(`${logPrefix} ERROR - Received invalid node or node without ID. Aborting.`);
731
+ console.error(`[ep_data_tables] ${funcName}: Received invalid node or node without ID.`);
732
+ return cb();
733
+ }
734
+
735
+ // *** ENHANCED DEBUG: Log complete DOM state ***
736
+ // log(`${logPrefix} NodeID#${nodeId}: COMPLETE DOM STRUCTURE DEBUG:`);
737
+ // log(`${logPrefix} NodeID#${nodeId}: Node tagName: ${node.tagName}`);
738
+ // log(`${logPrefix} NodeID#${nodeId}: Node className: ${node.className}`);
739
+ // log(`${logPrefix} NodeID#${nodeId}: Node innerHTML length: ${node.innerHTML?.length || 0}`);
740
+ // log(`${logPrefix} NodeID#${nodeId}: Node innerHTML (first 500 chars): "${(node.innerHTML || '').substring(0, 500)}"`);
741
+ // log(`${logPrefix} NodeID#${nodeId}: Node children count: ${node.children?.length || 0}`);
742
+
743
+ // log all child elements and their classes
744
+ if (node.children) {
745
+ for (let i = 0; i < Math.min(node.children.length, 10); i++) {
746
+ const child = node.children[i];
747
+ // log(`${logPrefix} NodeID#${nodeId}: Child[${i}] tagName: ${child.tagName}, className: "${child.className}", innerHTML length: ${child.innerHTML?.length || 0}`);
748
+ if (child.className && child.className.includes('tbljson-')) {
749
+ // log(`${logPrefix} NodeID#${nodeId}: *** FOUND TBLJSON CLASS ON CHILD[${i}] ***`);
750
+ }
751
+ }
752
+ }
753
+
754
+ let rowMetadata = null;
755
+ let encodedJsonString = null;
756
+
757
+ // --- 1. Find and Parse Metadata Attribute ---
758
+ // log(`${logPrefix} NodeID#${nodeId}: Searching for tbljson-* class...`);
759
+
760
+ // ENHANCED Helper function to recursively search for tbljson class in all descendants
761
+ function findTbljsonClass(element, depth = 0, path = '') {
762
+ const indent = ' '.repeat(depth);
763
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Searching element: ${element.tagName || 'unknown'}, path: ${path}`);
764
+
765
+ // Check the element itself
766
+ if (element.classList) {
767
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has ${element.classList.length} classes: [${Array.from(element.classList).join(', ')}]`);
768
+ for (const cls of element.classList) {
769
+ if (cls.startsWith('tbljson-')) {
770
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}*** FOUND TBLJSON CLASS: ${cls.substring(8)} at depth ${depth}, path: ${path} ***`);
771
+ return cls.substring(8);
772
+ }
773
+ }
774
+ } else {
775
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has no classList`);
776
+ }
777
+
778
+ // Recursively check all descendants
779
+ if (element.children) {
780
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has ${element.children.length} children`);
781
+ for (let i = 0; i < element.children.length; i++) {
782
+ const child = element.children[i];
783
+ const childPath = `${path}>${child.tagName}[${i}]`;
784
+ const found = findTbljsonClass(child, depth + 1, childPath);
785
+ if (found) {
786
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Returning found result from child: ${found}`);
787
+ return found;
788
+ }
789
+ }
790
+ } else {
791
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has no children`);
792
+ }
793
+
794
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}No tbljson class found in this element or its children`);
795
+ return null;
796
+ }
797
+
798
+ // Search for tbljson class starting from the node
799
+ // log(`${logPrefix} NodeID#${nodeId}: Starting recursive search for tbljson class...`);
800
+ encodedJsonString = findTbljsonClass(node, 0, 'ROOT');
801
+
802
+ if (encodedJsonString) {
803
+ // log(`${logPrefix} NodeID#${nodeId}: *** SUCCESS: Found encoded tbljson class: ${encodedJsonString} ***`);
804
+ } else {
805
+ // log(`${logPrefix} NodeID#${nodeId}: *** NO TBLJSON CLASS FOUND ***`);
806
+ }
807
+
808
+ // If no attribute found, it's not a table line managed by us
809
+ if (!encodedJsonString) {
810
+ // log(`${logPrefix} NodeID#${nodeId}: No tbljson-* class found. Assuming not a table line. END.`);
811
+
812
+ // DEBUG: Add detailed logging to understand why tbljson class is missing
813
+ // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Node tag: ${node.tagName}, Node classes:`, Array.from(node.classList || []));
814
+ // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Node innerHTML (first 200 chars): "${(node.innerHTML || '').substring(0, 200)}"`);
815
+
816
+ // Check if there are any child elements with classes
817
+ if (node.children && node.children.length > 0) {
818
+ for (let i = 0; i < Math.min(node.children.length, 5); i++) {
819
+ const child = node.children[i];
820
+ // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Child ${i} tag: ${child.tagName}, classes:`, Array.from(child.classList || []));
821
+ }
822
+ }
823
+
824
+ // Check if there's already a table in this node (orphaned table)
825
+ const existingTable = node.querySelector('table.dataTable[data-tblId]');
826
+ if (existingTable) {
827
+ const existingTblId = existingTable.getAttribute('data-tblId');
828
+ const existingRow = existingTable.getAttribute('data-row');
829
+ // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Found orphaned table! TblId: ${existingTblId}, Row: ${existingRow}`);
830
+
831
+ // This suggests the table exists but the tbljson class was lost
832
+ // Check if we're in a post-resize situation
833
+ if (existingTblId && existingRow !== null) {
834
+ // log(`${logPrefix} NodeID#${nodeId}: POTENTIAL ISSUE - Table exists but no tbljson class. This may be a post-resize issue.`);
835
+
836
+ // Try to look up what the metadata should be based on the table attributes
837
+ const tableCells = existingTable.querySelectorAll('td');
838
+ // log(`${logPrefix} NodeID#${nodeId}: Table has ${tableCells.length} cells`);
839
+
840
+ // log the current line's attribute state if we can get line number
841
+ if (lineNum !== undefined && args?.documentAttributeManager) {
842
+ try {
843
+ const currentLineAttr = args.documentAttributeManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
844
+ // log(`${logPrefix} NodeID#${nodeId}: Current line ${lineNum} tbljson attribute: ${currentLineAttr || 'NULL'}`);
845
+ } catch (e) {
846
+ // log(`${logPrefix} NodeID#${nodeId}: Error getting line attribute:`, e);
847
+ }
848
+ }
849
+ }
850
+ }
851
+
852
+ return cb();
853
+ }
854
+
855
+ // *** NEW CHECK: If table already rendered, skip regeneration ***
856
+ const existingTable = node.querySelector('table.dataTable[data-tblId]');
857
+ if (existingTable) {
858
+ // log(`${logPrefix} NodeID#${nodeId}: Table already exists in DOM. Skipping innerHTML replacement.`);
859
+ // Optionally, verify tblId matches metadata? For now, assume it's correct.
860
+ // const existingTblId = existingTable.getAttribute('data-tblId');
861
+ // try {
862
+ // const decoded = dec(encodedJsonString);
863
+ // const currentMetadata = JSON.parse(decoded);
864
+ // if (existingTblId === currentMetadata?.tblId) { ... }
865
+ // } catch(e) { /* ignore validation error */ }
866
+ return cb(); // Do nothing further
867
+ }
868
+
869
+ // log(`${logPrefix} NodeID#${nodeId}: Decoding and parsing metadata...`);
870
+ try {
871
+ const decoded = dec(encodedJsonString);
872
+ // log(`${logPrefix} NodeID#${nodeId}: Decoded string: ${decoded}`);
873
+ if (!decoded) throw new Error('Decoded string is null or empty.');
874
+ rowMetadata = JSON.parse(decoded);
875
+ // log(`${logPrefix} NodeID#${nodeId}: Parsed rowMetadata:`, rowMetadata);
876
+
877
+ // Validate essential metadata
878
+ if (!rowMetadata || typeof rowMetadata.tblId === 'undefined' || typeof rowMetadata.row === 'undefined' || typeof rowMetadata.cols !== 'number') {
879
+ throw new Error('Invalid or incomplete metadata (missing tblId, row, or cols).');
880
+ }
881
+ // log(`${logPrefix} NodeID#${nodeId}: Metadata validated successfully.`);
882
+
883
+ } catch(e) {
884
+ // log(`${logPrefix} NodeID#${nodeId}: FATAL ERROR - Failed to decode/parse/validate tbljson metadata. Rendering cannot proceed.`, e);
885
+ console.error(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Failed to decode/parse/validate tbljson.`, encodedJsonString, e);
886
+ // Optionally render an error state in the node?
887
+ node.innerHTML = '<div style="color:red; border: 1px solid red; padding: 5px;">[ep_data_tables] Error: Invalid table metadata attribute found.</div>';
888
+ // log(`${logPrefix} NodeID#${nodeId}: Rendered error message in node. END.`);
889
+ return cb();
890
+ }
891
+ // --- End Metadata Parsing ---
892
+
893
+ // --- 2. Get and Parse Line Content ---
894
+ // ALWAYS get the innerHTML of the line div itself to preserve all styling spans and attributes.
895
+ // This innerHTML is set by Etherpad based on the line's current text in atext and includes
896
+ // all the span elements with author colors, bold, italic, and other styling.
897
+ // For an imported line's first render, atext is "Cell1|Cell2", so node.innerHTML will be "Cell1|Cell2".
898
+ // For a natively created line, node.innerHTML is also "Cell1|Cell2".
899
+ // After an edit, aceKeyEvent updates atext, and node.innerHTML reflects that new "EditedCell1|Cell2" string.
900
+ // When styling is applied, it will include spans like: <span class="author-xxx bold">Cell1</span>|<span class="author-yyy italic">Cell2</span>
901
+ const delimitedTextFromLine = node.innerHTML;
902
+ // log(`${logPrefix} NodeID#${nodeId}: Using node.innerHTML for delimited text to preserve styling.`);
903
+ // log(`${logPrefix} NodeID#${nodeId}: Raw innerHTML length: ${delimitedTextFromLine?.length || 0}`);
904
+ // log(`${logPrefix} NodeID#${nodeId}: Raw innerHTML (first 1000 chars): "${(delimitedTextFromLine || '').substring(0, 1000)}"`);
905
+
906
+ // *** ENHANCED DEBUG: Analyze delimiter presence ***
907
+ const delimiterCount = (delimitedTextFromLine || '').split(DELIMITER).length - 1;
908
+ // log(`${logPrefix} NodeID#${nodeId}: Delimiter '${DELIMITER}' count in innerHTML: ${delimiterCount}`);
909
+ // log(`${logPrefix} NodeID#${nodeId}: Expected delimiters for ${rowMetadata.cols} columns: ${rowMetadata.cols - 1}`);
910
+
911
+ // log all delimiter positions
912
+ let pos = -1;
913
+ const delimiterPositions = [];
914
+ while ((pos = delimitedTextFromLine.indexOf(DELIMITER, pos + 1)) !== -1) {
915
+ delimiterPositions.push(pos);
916
+ // log(`${logPrefix} NodeID#${nodeId}: Delimiter found at position ${pos}, context: "${delimitedTextFromLine.substring(Math.max(0, pos - 20), pos + 21)}"`);
917
+ }
918
+ // log(`${logPrefix} NodeID#${nodeId}: All delimiter positions: [${delimiterPositions.join(', ')}]`);
919
+
920
+ // The DELIMITER const is defined at the top of this file.
921
+ // NEW: Remove all hidden-delimiter <span> wrappers **before** we split so
922
+ // the embedded delimiter character they carry doesn't inflate or shrink
923
+ // the segment count.
924
+ const spanDelimRegex = new RegExp('<span class="ep-data_tables-delim"[^>]*>' + DELIMITER + '<\\/span>', 'ig');
925
+ const sanitizedHTMLForSplit = (delimitedTextFromLine || '').replace(spanDelimRegex, '');
926
+ const htmlSegments = sanitizedHTMLForSplit.split(DELIMITER);
927
+
928
+ // log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT ANALYSIS ***`);
929
+ // log(`${logPrefix} NodeID#${nodeId}: Split resulted in ${htmlSegments.length} segments`);
930
+ for (let i = 0; i < htmlSegments.length; i++) {
931
+ const segment = htmlSegments[i] || '';
932
+ // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] length: ${segment.length}`);
933
+ // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (first 200 chars): "${segment.substring(0, 200)}"`);
934
+ if (segment.length > 200) {
935
+ // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (chars 200-400): "${segment.substring(200, 400)}"`);
936
+ }
937
+ if (segment.length > 400) {
938
+ // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (chars 400-600): "${segment.substring(400, 600)}"`);
939
+ }
940
+ // Check if segment contains image-related content
941
+ if (segment.includes('image:') || segment.includes('image-placeholder') || segment.includes('currently-selected')) {
942
+ // log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT[${i}] CONTAINS IMAGE CONTENT ***`);
943
+ }
944
+ }
945
+
946
+ // log(`${logPrefix} NodeID#${nodeId}: Parsed HTML segments (${htmlSegments.length}):`, htmlSegments.map(s => (s || '').substring(0,50) + (s && s.length > 50 ? '...' : '')));
947
+
948
+ // --- Enhanced Validation with Automatic Structure Reconstruction ---
949
+ let finalHtmlSegments = htmlSegments;
950
+
951
+ if (htmlSegments.length !== rowMetadata.cols) {
952
+ // log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH DETECTED ***`);
953
+ // log(`${logPrefix} NodeID#${nodeId}: WARNING - Parsed segment count (${htmlSegments.length}) does not match metadata cols (${rowMetadata.cols}). Auto-reconstructing table structure.`);
954
+ console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Parsed segment count (${htmlSegments.length}) mismatch with metadata cols (${rowMetadata.cols}). Segments:`, htmlSegments);
955
+
956
+ // *** ENHANCED DEBUG: Analyze why we have a mismatch ***
957
+ // log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH ANALYSIS ***`);
958
+ // log(`${logPrefix} NodeID#${nodeId}: Expected columns: ${rowMetadata.cols}`);
959
+ // log(`${logPrefix} NodeID#${nodeId}: Actual segments: ${htmlSegments.length}`);
960
+ // log(`${logPrefix} NodeID#${nodeId}: Delimiter count found: ${delimiterCount}`);
961
+ // log(`${logPrefix} NodeID#${nodeId}: Expected delimiter count: ${rowMetadata.cols - 1}`);
962
+
963
+ // Check if this is an image selection issue
964
+ const hasImageSelected = delimitedTextFromLine.includes('currently-selected');
965
+ const hasImageContent = delimitedTextFromLine.includes('image:');
966
+ // log(`${logPrefix} NodeID#${nodeId}: Has selected image: ${hasImageSelected}`);
967
+ // log(`${logPrefix} NodeID#${nodeId}: Has image content: ${hasImageContent}`);
968
+
969
+ if (hasImageSelected) {
970
+ // log(`${logPrefix} NodeID#${nodeId}: *** POTENTIAL CAUSE: Image selection state may be affecting segment parsing ***`);
971
+ }
972
+
973
+ // ENHANCED: Always reconstruct the correct structure based on metadata
974
+ const reconstructedSegments = [];
975
+
976
+ if (htmlSegments.length === 1 && rowMetadata.cols > 1) {
977
+ // Single segment case - put all content in first column, empty remaining columns
978
+ // log(`${logPrefix} NodeID#${nodeId}: Single segment detected, distributing content to first column of ${rowMetadata.cols} columns.`);
979
+ reconstructedSegments.push(htmlSegments[0]);
980
+ for (let i = 1; i < rowMetadata.cols; i++) {
981
+ reconstructedSegments.push('&nbsp;');
982
+ }
983
+ } else if (htmlSegments.length > rowMetadata.cols) {
984
+ // Too many segments - merge excess into last column
985
+ // log(`${logPrefix} NodeID#${nodeId}: Too many segments (${htmlSegments.length}), merging excess into ${rowMetadata.cols} columns.`);
986
+ for (let i = 0; i < rowMetadata.cols - 1; i++) {
987
+ reconstructedSegments.push(htmlSegments[i] || '&nbsp;');
988
+ }
989
+ // Merge remaining segments into last column
990
+ const remainingSegments = htmlSegments.slice(rowMetadata.cols - 1);
991
+ reconstructedSegments.push(remainingSegments.join('|') || '&nbsp;');
992
+ } else {
993
+ // Too few segments - pad with empty columns
994
+ // log(`${logPrefix} NodeID#${nodeId}: Too few segments (${htmlSegments.length}), padding to ${rowMetadata.cols} columns.`);
995
+ for (let i = 0; i < rowMetadata.cols; i++) {
996
+ reconstructedSegments.push(htmlSegments[i] || '&nbsp;');
997
+ }
998
+ }
999
+
1000
+ // log(`${logPrefix} NodeID#${nodeId}: Reconstructed ${reconstructedSegments.length} segments to match expected ${rowMetadata.cols} columns.`);
1001
+ finalHtmlSegments = reconstructedSegments;
1002
+
1003
+ } else {
1004
+ // log(`${logPrefix} NodeID#${nodeId}: Segment count matches metadata cols (${rowMetadata.cols}). Using original segments.`);
1005
+ }
1006
+
1007
+ // --- 3. Build and Render Table ---
1008
+ // log(`${logPrefix} NodeID#${nodeId}: Calling buildTableFromDelimitedHTML...`);
1009
+ try {
1010
+ const newTableHTML = buildTableFromDelimitedHTML(rowMetadata, finalHtmlSegments);
1011
+ // log(`${logPrefix} NodeID#${nodeId}: Received new table HTML from helper. Replacing content.`);
1012
+
1013
+ // The old local findTbljsonElement is removed from here. We use the global one now.
1014
+ const tbljsonElement = findTbljsonElement(node);
1015
+
1016
+ // If we found a tbljson element and it's nested in a block element,
1017
+ // we need to preserve the block wrapper while replacing the content
1018
+ if (tbljsonElement && tbljsonElement.parentElement && tbljsonElement.parentElement !== node) {
1019
+ // Check if the parent is a block-level element that should be preserved
1020
+ const parentTag = tbljsonElement.parentElement.tagName.toLowerCase();
1021
+ const blockElements = ['center', 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'right', 'left', 'ul', 'ol', 'li', 'code'];
1022
+
1023
+ if (blockElements.includes(parentTag)) {
1024
+ // log(`${logPrefix} NodeID#${nodeId}: Preserving block element ${parentTag} and replacing its content with table.`);
1025
+ tbljsonElement.parentElement.innerHTML = newTableHTML;
1026
+ } else {
1027
+ // log(`${logPrefix} NodeID#${nodeId}: Parent element ${parentTag} is not a block element, replacing entire node content.`);
1028
+ node.innerHTML = newTableHTML;
1029
+ }
1030
+ } else {
1031
+ // Replace the node's content entirely with the generated table
1032
+ // log(`${logPrefix} NodeID#${nodeId}: No nested block element found, replacing entire node content.`);
1033
+ node.innerHTML = newTableHTML;
1034
+ }
1035
+
1036
+ // log(`${logPrefix} NodeID#${nodeId}: Successfully replaced content with new table structure.`);
1037
+ } catch (renderError) {
1038
+ // log(`${logPrefix} NodeID#${nodeId}: ERROR during table building or rendering.`, renderError);
1039
+ console.error(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Error building/rendering table.`, renderError);
1040
+ node.innerHTML = '<div style="color:red; border: 1px solid red; padding: 5px;">[ep_data_tables] Error: Failed to render table structure.</div>';
1041
+ // log(`${logPrefix} NodeID#${nodeId}: Rendered build/render error message in node. END.`);
1042
+ return cb();
1043
+ }
1044
+ // --- End Table Building ---
1045
+
1046
+ // *** REMOVED CACHING LOGIC ***
1047
+ // The old logic based on tableRowNodes cache is completely removed.
1048
+
1049
+ // log(`${logPrefix}: ----- END ----- NodeID: ${nodeId}`);
1050
+ return cb();
1051
+ };
1052
+
1053
+ // NEW: Helper function to get line number (adapted from ep_image_insert)
1054
+ // Ensure this is defined before it's used in postAceInit
1055
+ function _getLineNumberOfElement(element) {
1056
+ // Implementation similar to ep_image_insert
1057
+ let currentElement = element;
1058
+ let count = 0;
1059
+ while (currentElement = currentElement.previousElementSibling) {
1060
+ count++;
1061
+ }
1062
+ return count;
1063
+ }
1064
+
1065
+ // ───────────────────── Handle Key Events ─────────────────────
1066
+ exports.aceKeyEvent = (h, ctx) => {
1067
+ const funcName = 'aceKeyEvent';
1068
+ const evt = ctx.evt;
1069
+ const rep = ctx.rep;
1070
+ const editorInfo = ctx.editorInfo;
1071
+ const docManager = ctx.documentAttributeManager;
1072
+
1073
+ const startLogTime = Date.now();
1074
+ const logPrefix = '[ep_data_tables:aceKeyEvent]';
1075
+ // log(`${logPrefix} START Key='${evt?.key}' Code=${evt?.keyCode} Type=${evt?.type} Modifiers={ctrl:${evt?.ctrlKey},alt:${evt?.altKey},meta:${evt?.metaKey},shift:${evt?.shiftKey}}`, { selStart: rep?.selStart, selEnd: rep?.selEnd });
1076
+
1077
+ if (!rep || !rep.selStart || !editorInfo || !evt || !docManager) {
1078
+ // log(`${logPrefix} Skipping - Missing critical context.`);
1079
+ return false;
1080
+ }
1081
+
1082
+ // Get caret info from event context - may be stale
1083
+ const reportedLineNum = rep.selStart[0];
1084
+ const reportedCol = rep.selStart[1];
1085
+ // log(`${logPrefix} Reported caret from rep: Line=${reportedLineNum}, Col=${reportedCol}`);
1086
+
1087
+ // --- Get Table Metadata for the reported line ---
1088
+ let tableMetadata = null;
1089
+ let lineAttrString = null; // Store for potential use later
1090
+ try {
1091
+ // Add debugging to see what's happening with attribute retrieval
1092
+ // log(`${logPrefix} DEBUG: Attempting to get ${ATTR_TABLE_JSON} attribute from line ${reportedLineNum}`);
1093
+ lineAttrString = docManager.getAttributeOnLine(reportedLineNum, ATTR_TABLE_JSON);
1094
+ // log(`${logPrefix} DEBUG: getAttributeOnLine returned: ${lineAttrString ? `"${lineAttrString}"` : 'null/undefined'}`);
1095
+
1096
+ // Also check if there are any attributes on this line at all
1097
+ if (typeof docManager.getAttributesOnLine === 'function') {
1098
+ try {
1099
+ const allAttribs = docManager.getAttributesOnLine(reportedLineNum);
1100
+ // log(`${logPrefix} DEBUG: All attributes on line ${reportedLineNum}:`, allAttribs);
1101
+ } catch(e) {
1102
+ // log(`${logPrefix} DEBUG: Error getting all attributes:`, e);
1103
+ }
1104
+ }
1105
+
1106
+ // NEW: Check if there's a table in the DOM even though attribute is missing
1107
+ if (!lineAttrString) {
1108
+ try {
1109
+ const rep = editorInfo.ace_getRep();
1110
+ const lineEntry = rep.lines.atIndex(reportedLineNum);
1111
+ if (lineEntry && lineEntry.lineNode) {
1112
+ const tableInDOM = lineEntry.lineNode.querySelector('table.dataTable[data-tblId]');
1113
+ if (tableInDOM) {
1114
+ const domTblId = tableInDOM.getAttribute('data-tblId');
1115
+ const domRow = tableInDOM.getAttribute('data-row');
1116
+ // log(`${logPrefix} DEBUG: Found table in DOM without attribute! TblId=${domTblId}, Row=${domRow}`);
1117
+ // Try to reconstruct the metadata from DOM
1118
+ const domCells = tableInDOM.querySelectorAll('td');
1119
+ if (domTblId && domRow !== null && domCells.length > 0) {
1120
+ // log(`${logPrefix} DEBUG: Attempting to reconstruct metadata from DOM...`);
1121
+ const reconstructedMetadata = {
1122
+ tblId: domTblId,
1123
+ row: parseInt(domRow, 10),
1124
+ cols: domCells.length
1125
+ };
1126
+ lineAttrString = JSON.stringify(reconstructedMetadata);
1127
+ // log(`${logPrefix} DEBUG: Reconstructed metadata: ${lineAttrString}`);
1128
+ }
1129
+ }
1130
+ }
1131
+ } catch(e) {
1132
+ // log(`${logPrefix} DEBUG: Error checking DOM for table:`, e);
1133
+ }
1134
+ }
1135
+
1136
+ if (lineAttrString) {
1137
+ tableMetadata = JSON.parse(lineAttrString);
1138
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
1139
+ // log(`${logPrefix} Line ${reportedLineNum} has attribute, but metadata invalid/missing cols.`);
1140
+ tableMetadata = null; // Ensure it's null if invalid
1141
+ }
1142
+ } else {
1143
+ // log(`${logPrefix} DEBUG: No ${ATTR_TABLE_JSON} attribute found on line ${reportedLineNum}`);
1144
+ // Not a table line based on reported caret line
1145
+ }
1146
+ } catch(e) {
1147
+ console.error(`${logPrefix} Error checking/parsing line attribute for line ${reportedLineNum}.`, e);
1148
+ tableMetadata = null; // Ensure it's null on error
1149
+ }
1150
+
1151
+ // Get last known good state
1152
+ const editor = editorInfo.editor; // Get editor instance
1153
+ const lastClick = editor?.ep_data_tables_last_clicked; // Read shared state
1154
+ // log(`${logPrefix} Reading stored click/caret info:`, lastClick);
1155
+
1156
+ // --- Determine the TRUE target line, cell, and caret position ---
1157
+ let currentLineNum = -1;
1158
+ let targetCellIndex = -1;
1159
+ let relativeCaretPos = -1;
1160
+ let precedingCellsOffset = 0;
1161
+ let cellStartCol = 0;
1162
+ let lineText = '';
1163
+ let cellTexts = [];
1164
+ let metadataForTargetLine = null;
1165
+ let trustedLastClick = false; // Flag to indicate if we are using stored info
1166
+
1167
+ // ** Scenario 1: Try to trust lastClick info **
1168
+ if (lastClick) {
1169
+ // log(`${logPrefix} Attempting to validate stored click info for Line=${lastClick.lineNum}...`);
1170
+ let storedLineAttrString = null;
1171
+ let storedLineMetadata = null;
1172
+ try {
1173
+ // log(`${logPrefix} DEBUG: Getting ${ATTR_TABLE_JSON} attribute from stored line ${lastClick.lineNum}`);
1174
+ storedLineAttrString = docManager.getAttributeOnLine(lastClick.lineNum, ATTR_TABLE_JSON);
1175
+ // log(`${logPrefix} DEBUG: Stored line attribute result: ${storedLineAttrString ? `"${storedLineAttrString}"` : 'null/undefined'}`);
1176
+
1177
+ if (storedLineAttrString) {
1178
+ storedLineMetadata = JSON.parse(storedLineAttrString);
1179
+ // log(`${logPrefix} DEBUG: Parsed stored metadata:`, storedLineMetadata);
1180
+ }
1181
+
1182
+ // Check if metadata is valid and tblId matches
1183
+ if (storedLineMetadata && typeof storedLineMetadata.cols === 'number' && storedLineMetadata.tblId === lastClick.tblId) {
1184
+ // log(`${logPrefix} Stored click info VALIDATED (Metadata OK and tblId matches). Trusting stored state.`);
1185
+ trustedLastClick = true;
1186
+ currentLineNum = lastClick.lineNum;
1187
+ targetCellIndex = lastClick.cellIndex;
1188
+ metadataForTargetLine = storedLineMetadata;
1189
+ lineAttrString = storedLineAttrString; // Use the validated attr string
1190
+
1191
+ lineText = rep.lines.atIndex(currentLineNum)?.text || '';
1192
+ cellTexts = lineText.split(DELIMITER);
1193
+ // log(`${logPrefix} Using Line=${currentLineNum}, CellIndex=${targetCellIndex}. Text: "${lineText}"`);
1194
+
1195
+ if (cellTexts.length !== metadataForTargetLine.cols) {
1196
+ // log(`${logPrefix} WARNING: Stored cell count mismatch for trusted line ${currentLineNum}.`);
1197
+ }
1198
+
1199
+ cellStartCol = 0;
1200
+ for (let i = 0; i < targetCellIndex; i++) {
1201
+ cellStartCol += (cellTexts[i]?.length ?? 0) + DELIMITER.length;
1202
+ }
1203
+ precedingCellsOffset = cellStartCol;
1204
+ // log(`${logPrefix} Calculated cellStartCol=${cellStartCol} from trusted cellIndex=${targetCellIndex}.`);
1205
+
1206
+ if (typeof lastClick.relativePos === 'number' && lastClick.relativePos >= 0) {
1207
+ const currentCellTextLength = cellTexts[targetCellIndex]?.length ?? 0;
1208
+ relativeCaretPos = Math.max(0, Math.min(lastClick.relativePos, currentCellTextLength));
1209
+ // log(`${logPrefix} Using and validated stored relative position: ${relativeCaretPos}.`);
1210
+ } else {
1211
+ relativeCaretPos = reportedCol - cellStartCol; // Use reportedCol for initial calc if relative is missing
1212
+ const currentCellTextLength = cellTexts[targetCellIndex]?.length ?? 0;
1213
+ relativeCaretPos = Math.max(0, Math.min(relativeCaretPos, currentCellTextLength));
1214
+ // log(`${logPrefix} Stored relativePos missing, calculated from reportedCol (${reportedCol}): ${relativeCaretPos}`);
1215
+ }
1216
+ } else {
1217
+ // log(`${logPrefix} Stored click info INVALID (Metadata missing/invalid or tblId mismatch). Clearing stored state.`);
1218
+ if (editor) editor.ep_data_tables_last_clicked = null;
1219
+ }
1220
+ } catch (e) {
1221
+ console.error(`${logPrefix} Error validating stored click info for line ${lastClick.lineNum}.`, e);
1222
+ if (editor) editor.ep_data_tables_last_clicked = null; // Clear on error
1223
+ }
1224
+ }
1225
+
1226
+ // ** Scenario 2: Fallback - Use reported line/col ONLY if stored info wasn't trusted **
1227
+ if (!trustedLastClick) {
1228
+ // log(`${logPrefix} Fallback: Using reported caret position Line=${reportedLineNum}, Col=${reportedCol}.`);
1229
+ // Fetch metadata for the reported line again, in case it wasn't fetched or was invalid earlier
1230
+ try {
1231
+ lineAttrString = docManager.getAttributeOnLine(reportedLineNum, ATTR_TABLE_JSON);
1232
+ if (lineAttrString) tableMetadata = JSON.parse(lineAttrString);
1233
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number') tableMetadata = null;
1234
+
1235
+ // If no attribute found directly, check if there's a table in the DOM even though attribute is missing (block styles)
1236
+ if (!lineAttrString) {
1237
+ try {
1238
+ const rep = editorInfo.ace_getRep();
1239
+ const lineEntry = rep.lines.atIndex(reportedLineNum);
1240
+ if (lineEntry && lineEntry.lineNode) {
1241
+ const tableInDOM = lineEntry.lineNode.querySelector('table.dataTable[data-tblId]');
1242
+ if (tableInDOM) {
1243
+ const domTblId = tableInDOM.getAttribute('data-tblId');
1244
+ const domRow = tableInDOM.getAttribute('data-row');
1245
+ // log(`${logPrefix} Fallback: Found table in DOM without attribute! TblId=${domTblId}, Row=${domRow}`);
1246
+ // Try to reconstruct the metadata from DOM
1247
+ const domCells = tableInDOM.querySelectorAll('td');
1248
+ if (domTblId && domRow !== null && domCells.length > 0) {
1249
+ // log(`${logPrefix} Fallback: Attempting to reconstruct metadata from DOM...`);
1250
+ const reconstructedMetadata = {
1251
+ tblId: domTblId,
1252
+ row: parseInt(domRow, 10),
1253
+ cols: domCells.length
1254
+ };
1255
+ lineAttrString = JSON.stringify(reconstructedMetadata);
1256
+ tableMetadata = reconstructedMetadata;
1257
+ // log(`${logPrefix} Fallback: Reconstructed metadata: ${lineAttrString}`);
1258
+ }
1259
+ }
1260
+ }
1261
+ } catch(e) {
1262
+ // log(`${logPrefix} Fallback: Error checking DOM for table:`, e);
1263
+ }
1264
+ }
1265
+ } catch(e) { tableMetadata = null; } // Ignore errors here, handled below
1266
+
1267
+ if (!tableMetadata) {
1268
+ // log(`${logPrefix} Fallback: Reported line ${reportedLineNum} is not a valid table line. Allowing default.`);
1269
+ return false;
1270
+ }
1271
+
1272
+ currentLineNum = reportedLineNum;
1273
+ metadataForTargetLine = tableMetadata;
1274
+ // log(`${logPrefix} Fallback: Processing based on reported line ${currentLineNum}.`);
1275
+
1276
+ lineText = rep.lines.atIndex(currentLineNum)?.text || '';
1277
+ cellTexts = lineText.split(DELIMITER);
1278
+ // log(`${logPrefix} Fallback: Fetched text for reported line ${currentLineNum}: "${lineText}"`);
1279
+
1280
+ if (cellTexts.length !== metadataForTargetLine.cols) {
1281
+ // log(`${logPrefix} WARNING (Fallback): Cell count mismatch for reported line ${currentLineNum}.`);
1282
+ }
1283
+
1284
+ // Calculate target cell based on reportedCol
1285
+ let currentOffset = 0;
1286
+ let foundIndex = -1;
1287
+ for (let i = 0; i < cellTexts.length; i++) {
1288
+ const cellLength = cellTexts[i]?.length ?? 0;
1289
+ const cellEndCol = currentOffset + cellLength;
1290
+ if (reportedCol >= currentOffset && reportedCol <= cellEndCol) {
1291
+ foundIndex = i;
1292
+ relativeCaretPos = reportedCol - currentOffset;
1293
+ cellStartCol = currentOffset;
1294
+ precedingCellsOffset = cellStartCol;
1295
+ // log(`${logPrefix} --> (Fallback Calc) Found target cell ${foundIndex}. RelativePos: ${relativeCaretPos}.`);
1296
+ break;
1297
+ }
1298
+ if (i < cellTexts.length - 1 && reportedCol === cellEndCol + DELIMITER.length) {
1299
+ foundIndex = i + 1;
1300
+ relativeCaretPos = 0;
1301
+ cellStartCol = currentOffset + cellLength + DELIMITER.length;
1302
+ precedingCellsOffset = cellStartCol;
1303
+ // log(`${logPrefix} --> (Fallback Calc) Caret at delimiter AFTER cell ${i}. Treating as start of cell ${foundIndex}.`);
1304
+ break;
1305
+ }
1306
+ currentOffset += cellLength + DELIMITER.length;
1307
+ }
1308
+
1309
+ if (foundIndex === -1) {
1310
+ if (reportedCol === lineText.length && cellTexts.length > 0) {
1311
+ foundIndex = cellTexts.length - 1;
1312
+ cellStartCol = 0;
1313
+ for (let i = 0; i < foundIndex; i++) { cellStartCol += (cellTexts[i]?.length ?? 0) + DELIMITER.length; }
1314
+ precedingCellsOffset = cellStartCol;
1315
+ relativeCaretPos = cellTexts[foundIndex]?.length ?? 0;
1316
+ // log(`${logPrefix} --> (Fallback Calc) Caret detected at END of last cell (${foundIndex}).`);
1317
+ } else {
1318
+ // log(`${logPrefix} (Fallback Calc) FAILED to determine target cell for caret col ${reportedCol}. Allowing default handling.`);
1319
+ return false;
1320
+ }
1321
+ }
1322
+ targetCellIndex = foundIndex;
1323
+ }
1324
+
1325
+ // --- Final Validation ---
1326
+ if (currentLineNum < 0 || targetCellIndex < 0 || !metadataForTargetLine || targetCellIndex >= metadataForTargetLine.cols) {
1327
+ // log(`${logPrefix} FAILED final validation: Line=${currentLineNum}, Cell=${targetCellIndex}, Metadata=${!!metadataForTargetLine}. Allowing default.`);
1328
+ if (editor) editor.ep_data_tables_last_clicked = null;
1329
+ return false;
1330
+ }
1331
+
1332
+ // log(`${logPrefix} --> Final Target: Line=${currentLineNum}, CellIndex=${targetCellIndex}, RelativePos=${relativeCaretPos}`);
1333
+ // --- End Cell/Position Determination ---
1334
+
1335
+ // --- START NEW: Handle Highlight Deletion/Replacement ---
1336
+ const selStartActual = rep.selStart;
1337
+ const selEndActual = rep.selEnd;
1338
+ const hasSelection = selStartActual[0] !== selEndActual[0] || selStartActual[1] !== selEndActual[1];
1339
+
1340
+ if (hasSelection) {
1341
+ // log(`${logPrefix} [selection] Active selection detected. Start:[${selStartActual[0]},${selStartActual[1]}], End:[${selEndActual[0]},${selEndActual[1]}]`);
1342
+ // log(`${logPrefix} [caretTrace] [selection] Initial rep.selStart: Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
1343
+
1344
+ if (selStartActual[0] !== currentLineNum || selEndActual[0] !== currentLineNum) {
1345
+ // log(`${logPrefix} [selection] Selection spans multiple lines (${selStartActual[0]}-${selEndActual[0]}) or is not on the current focused table line (${currentLineNum}). Preventing default action.`);
1346
+ evt.preventDefault();
1347
+ return true;
1348
+ }
1349
+
1350
+ let selectionStartColInLine = selStartActual[1]; // may be clamped
1351
+ let selectionEndColInLine = selEndActual[1]; // may be clamped
1352
+
1353
+ const currentCellFullText = cellTexts[targetCellIndex] || '';
1354
+ // cellStartCol is already defined and calculated based on trustedLastClick or fallback
1355
+ const cellContentStartColInLine = cellStartCol;
1356
+ const cellContentEndColInLine = cellStartCol + currentCellFullText.length;
1357
+
1358
+ /* If the user selected the whole cell plus delimiter characters,
1359
+ * clamp the selection to just the cell content. */
1360
+ const hasTrailingDelim =
1361
+ targetCellIndex < metadataForTargetLine.cols - 1 &&
1362
+ selectionEndColInLine === cellContentEndColInLine + DELIMITER.length;
1363
+
1364
+ const hasLeadingDelim =
1365
+ targetCellIndex > 0 &&
1366
+ selectionStartColInLine === cellContentStartColInLine - DELIMITER.length;
1367
+
1368
+ console.log(`[ep_data_tables:highlight-deletion] Selection analysis:`, {
1369
+ targetCellIndex,
1370
+ totalCols: metadataForTargetLine.cols,
1371
+ selectionStartCol: selectionStartColInLine,
1372
+ selectionEndCol: selectionEndColInLine,
1373
+ cellContentStartCol: cellContentStartColInLine,
1374
+ cellContentEndCol: cellContentEndColInLine,
1375
+ delimiterLength: DELIMITER.length,
1376
+ expectedTrailingDelimiterPos: cellContentEndColInLine + DELIMITER.length,
1377
+ expectedLeadingDelimiterPos: cellContentStartColInLine - DELIMITER.length,
1378
+ hasTrailingDelim,
1379
+ hasLeadingDelim,
1380
+ cellText: currentCellFullText
1381
+ });
1382
+
1383
+ if (hasLeadingDelim) {
1384
+ console.log(`[ep_data_tables:highlight-deletion] CLAMPING selection start from ${selectionStartColInLine} to ${cellContentStartColInLine}`);
1385
+ selectionStartColInLine = cellContentStartColInLine;
1386
+ }
1387
+
1388
+ if (hasTrailingDelim) {
1389
+ console.log(`[ep_data_tables:highlight-deletion] CLAMPING selection end from ${selectionEndColInLine} to ${cellContentEndColInLine}`);
1390
+ selectionEndColInLine = cellContentEndColInLine;
1391
+ }
1392
+
1393
+ // log(`${logPrefix} [selection] Cell context for selection: targetCellIndex=${targetCellIndex}, cellStartColInLine=${cellContentStartColInLine}, cellEndColInLine=${cellContentEndColInLine}, currentCellFullText='${currentCellFullText}'`);
1394
+
1395
+ const isSelectionEntirelyWithinCell =
1396
+ selectionStartColInLine >= cellContentStartColInLine &&
1397
+ selectionEndColInLine <= cellContentEndColInLine;
1398
+
1399
+ // Allow selection even if it starts at the very first char, but be ready to restore
1400
+
1401
+ if (isSelectionEntirelyWithinCell) {
1402
+ // Pure selection (no key pressed yet) – allow browser shortcuts such as
1403
+ // Ctrl-C / Ctrl-X / Cmd-C / Cmd-X to work. We only take control for
1404
+ // real keydown events that would modify the cell (handled further below).
1405
+
1406
+ // 1. Non-keydown events → let them bubble (copy/cut command happens on
1407
+ // the subsequent "copy"/"cut" event).
1408
+ if (evt.type !== 'keydown') return false;
1409
+
1410
+ // 2. Keydown that involves modifiers (Ctrl/Cmd/Alt) → we are not going
1411
+ // to change the cell text, so let the browser handle it.
1412
+ if (evt.ctrlKey || evt.metaKey || evt.altKey) return false;
1413
+
1414
+ // 3. For destructive or printable keys we fall through so the specialised
1415
+ // highlight-deletion logic that follows can run.
1416
+ }
1417
+
1418
+ const isCurrentKeyDelete = evt.key === 'Delete' || evt.keyCode === 46;
1419
+ const isCurrentKeyBackspace = evt.key === 'Backspace' || evt.keyCode === 8;
1420
+ // Check if it's a printable character, not a modifier
1421
+ const isCurrentKeyTyping = evt.key && evt.key.length === 1 && !evt.ctrlKey && !evt.metaKey && !evt.altKey;
1422
+
1423
+
1424
+ if (isSelectionEntirelyWithinCell && (isCurrentKeyDelete || isCurrentKeyBackspace || isCurrentKeyTyping)) {
1425
+ // log(`${logPrefix} [selection] Handling key='${evt.key}' (Type: ${evt.type}) for valid intra-cell selection.`);
1426
+
1427
+ if (evt.type !== 'keydown') {
1428
+ // log(`${logPrefix} [selection] Ignoring non-keydown event type ('${evt.type}') for selection handling. Allowing default.`);
1429
+ return false;
1430
+ }
1431
+ evt.preventDefault();
1432
+
1433
+ const rangeStart = [currentLineNum, selectionStartColInLine];
1434
+ const rangeEnd = [currentLineNum, selectionEndColInLine];
1435
+ let replacementText = '';
1436
+ let newAbsoluteCaretCol = selectionStartColInLine;
1437
+ const repBeforeEdit = editorInfo.ace_getRep(); // Get rep before edit for attribute helper
1438
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart before ace_performDocumentReplaceRange: Line=${repBeforeEdit.selStart[0]}, Col=${repBeforeEdit.selStart[1]}`);
1439
+
1440
+ if (isCurrentKeyTyping) {
1441
+ replacementText = evt.key;
1442
+ newAbsoluteCaretCol = selectionStartColInLine + replacementText.length;
1443
+ // log(`${logPrefix} [selection] -> Replacing selected range [[${rangeStart[0]},${rangeStart[1]}],[${rangeEnd[0]},${rangeEnd[1]}]] with text '${replacementText}'`);
1444
+ } else { // Delete or Backspace
1445
+ // log(`${logPrefix} [selection] -> Deleting selected range [[${rangeStart[0]},${rangeStart[1]}],[${rangeEnd[0]},${rangeEnd[1]}]]`);
1446
+ // If whole cell is being wiped, keep a single space so cell isn't empty
1447
+ const isWholeCell = selectionStartColInLine <= cellContentStartColInLine && selectionEndColInLine >= cellContentEndColInLine;
1448
+ if (isWholeCell) {
1449
+ replacementText = ' ';
1450
+ newAbsoluteCaretCol = selectionStartColInLine + 1;
1451
+ // log(`${logPrefix} [selection] Whole cell cleared – inserting single space to preserve caret/author span.`);
1452
+ }
1453
+ }
1454
+
1455
+ try {
1456
+ // const repBeforeEdit = editorInfo.ace_getRep(); // Get rep before edit for attribute helper - MOVED UP
1457
+ editorInfo.ace_performDocumentReplaceRange(rangeStart, rangeEnd, replacementText);
1458
+
1459
+ // NEW: ensure the replacement text inherits the cell attribute so the
1460
+ // author-span (& tblCell-N) comes back immediately
1461
+ if (replacementText.length > 0) {
1462
+ const attrStart = [currentLineNum, selectionStartColInLine];
1463
+ const attrEnd = [currentLineNum, selectionStartColInLine + replacementText.length];
1464
+ console.log(`[ep_data_tables:highlight-deletion] Applying cell attribute to replacement text "${replacementText}" at range [${attrStart[0]},${attrStart[1]}] to [${attrEnd[0]},${attrEnd[1]}]`);
1465
+ editorInfo.ace_performDocumentApplyAttributesToRange(
1466
+ attrStart, attrEnd, [[ATTR_CELL, String(targetCellIndex)]],
1467
+ );
1468
+ }
1469
+ const repAfterReplace = editorInfo.ace_getRep();
1470
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performDocumentReplaceRange: Line=${repAfterReplace.selStart[0]}, Col=${repAfterReplace.selStart[1]}`);
1471
+
1472
+
1473
+ // log(`${logPrefix} [selection] -> Re-applying tbljson line attribute...`);
1474
+ const applyHelper = editorInfo.ep_data_tables_applyMeta;
1475
+ if (applyHelper && typeof applyHelper === 'function' && repBeforeEdit) {
1476
+ const attrStringToApply = (trustedLastClick || reportedLineNum === currentLineNum) ? lineAttrString : null;
1477
+ applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, repBeforeEdit, editorInfo, attrStringToApply, docManager);
1478
+ // log(`${logPrefix} [selection] -> tbljson line attribute re-applied (using rep before edit).`);
1479
+ } else {
1480
+ console.error(`${logPrefix} [selection] -> FAILED to re-apply tbljson attribute (helper or repBeforeEdit missing).`);
1481
+ const currentRepFallback = editorInfo.ace_getRep();
1482
+ if (applyHelper && typeof applyHelper === 'function' && currentRepFallback) {
1483
+ // log(`${logPrefix} [selection] -> Retrying attribute application with current rep...`);
1484
+ applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, currentRepFallback, editorInfo, null, docManager);
1485
+ // log(`${logPrefix} [selection] -> tbljson line attribute re-applied (using current rep fallback).`);
1486
+ } else {
1487
+ console.error(`${logPrefix} [selection] -> FAILED to re-apply tbljson attribute even with fallback rep.`);
1488
+ }
1489
+ }
1490
+
1491
+ // log(`${logPrefix} [selection] -> Setting selection/caret to: [${currentLineNum}, ${newAbsoluteCaretCol}]`);
1492
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart before ace_performSelectionChange: Line=${editorInfo.ace_getRep().selStart[0]}, Col=${editorInfo.ace_getRep().selStart[1]}`);
1493
+ editorInfo.ace_performSelectionChange([currentLineNum, newAbsoluteCaretCol], [currentLineNum, newAbsoluteCaretCol], false);
1494
+ const repAfterSelectionChange = editorInfo.ace_getRep();
1495
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performSelectionChange: Line=${repAfterSelectionChange.selStart[0]}, Col=${repAfterSelectionChange.selStart[1]}`);
1496
+
1497
+ // Add sync hint AFTER setting selection
1498
+ editorInfo.ace_fastIncorp(1);
1499
+ const repAfterFastIncorp = editorInfo.ace_getRep();
1500
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_fastIncorp: Line=${repAfterFastIncorp.selStart[0]}, Col=${repAfterFastIncorp.selStart[1]}`);
1501
+ // log(`${logPrefix} [selection] -> Requested sync hint (fastIncorp 1).`);
1502
+
1503
+ // --- Re-assert selection ---
1504
+ // log(`${logPrefix} [caretTrace] [selection] Attempting to re-assert selection post-fastIncorp to [${currentLineNum}, ${newAbsoluteCaretCol}]`);
1505
+ editorInfo.ace_performSelectionChange([currentLineNum, newAbsoluteCaretCol], [currentLineNum, newAbsoluteCaretCol], false);
1506
+ const repAfterReassert = editorInfo.ace_getRep();
1507
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after re-asserting selection: Line=${repAfterReassert.selStart[0]}, Col=${repAfterReassert.selStart[1]}`);
1508
+
1509
+ const newRelativePos = newAbsoluteCaretCol - cellStartCol;
1510
+ if (editor) {
1511
+ editor.ep_data_tables_last_clicked = {
1512
+ lineNum: currentLineNum,
1513
+ tblId: metadataForTargetLine.tblId,
1514
+ cellIndex: targetCellIndex,
1515
+ relativePos: newRelativePos < 0 ? 0 : newRelativePos
1516
+ };
1517
+ // log(`${logPrefix} [selection] -> Updated stored click/caret info:`, editor.ep_data_tables_last_clicked);
1518
+ } else {
1519
+ // log(`${logPrefix} [selection] -> Editor instance not found, cannot update ep_data_tables_last_clicked.`);
1520
+ }
1521
+
1522
+ // log(`${logPrefix} END [selection] (Handled highlight modification) Key='${evt.key}' Type='${evt.type}'. Duration: ${Date.now() - startLogTime}ms`);
1523
+ return true;
1524
+ } catch (error) {
1525
+ // log(`${logPrefix} [selection] ERROR during highlight modification:`, error);
1526
+ console.error('[ep_data_tables] Error processing highlight modification:', error);
1527
+ return true; // Still return true as we prevented default.
1528
+ }
1529
+ }
1530
+ }
1531
+ // --- END NEW: Handle Highlight Deletion/Replacement ---
1532
+
1533
+ // --- Check for Ctrl+X (Cut) key combination ---
1534
+ const isCutKey = (evt.ctrlKey || evt.metaKey) && (evt.key === 'x' || evt.key === 'X' || evt.keyCode === 88);
1535
+ if (isCutKey && hasSelection) {
1536
+ // log(`${logPrefix} Ctrl+X (Cut) detected with selection. Letting cut event handler manage this.`);
1537
+ // Let the cut event handler handle this - we don't need to preventDefault here
1538
+ // as the cut event will handle the operation and prevent default
1539
+ return false; // Allow the cut event to be triggered
1540
+ } else if (isCutKey && !hasSelection) {
1541
+ // log(`${logPrefix} Ctrl+X (Cut) detected but no selection. Allowing default.`);
1542
+ return false; // Allow default - nothing to cut
1543
+ }
1544
+
1545
+ // --- Define Key Types ---
1546
+ const isTypingKey = evt.key && evt.key.length === 1 && !evt.ctrlKey && !evt.metaKey && !evt.altKey;
1547
+ const isDeleteKey = evt.key === 'Delete' || evt.keyCode === 46;
1548
+ const isBackspaceKey = evt.key === 'Backspace' || evt.keyCode === 8;
1549
+ const isNavigationKey = [33, 34, 35, 36, 37, 38, 39, 40].includes(evt.keyCode);
1550
+ const isTabKey = evt.key === 'Tab';
1551
+ const isEnterKey = evt.key === 'Enter';
1552
+ // log(`${logPrefix} Key classification: Typing=${isTypingKey}, Backspace=${isBackspaceKey}, Delete=${isDeleteKey}, Nav=${isNavigationKey}, Tab=${isTabKey}, Enter=${isEnterKey}, Cut=${isCutKey}`);
1553
+
1554
+ /*
1555
+ * Prevent caret placement *after* the invisible caret-anchor.
1556
+ * – RIGHT (→) pressed at the end of a cell jumps to the next cell.
1557
+ * – LEFT (←) pressed at the start of a cell jumps to the previous cell.
1558
+ * This avoids the narrow dead-zone that lives between the anchor and the
1559
+ * resize handle where typing previously caused content to drift into the
1560
+ * neighbouring column.
1561
+ */
1562
+ const currentCellTextLengthEarly = cellTexts[targetCellIndex]?.length ?? 0;
1563
+
1564
+ if (evt.type === 'keydown' && !evt.ctrlKey && !evt.metaKey && !evt.altKey) {
1565
+ // Right-arrow – if at the very end of a cell, move to the next cell.
1566
+ if (evt.keyCode === 39 && relativeCaretPos >= currentCellTextLengthEarly && targetCellIndex < metadataForTargetLine.cols - 1) {
1567
+ // log(`${logPrefix} ArrowRight at cell boundary – navigating to next cell to avoid anchor zone.`);
1568
+ evt.preventDefault();
1569
+ navigateToNextCell(currentLineNum, targetCellIndex, metadataForTargetLine, false, editorInfo, docManager);
1570
+ return true;
1571
+ }
1572
+
1573
+ // Left-arrow – if at the very start of a cell, move to the previous cell.
1574
+ if (evt.keyCode === 37 && relativeCaretPos === 0 && targetCellIndex > 0) {
1575
+ // log(`${logPrefix} ArrowLeft at cell boundary – navigating to previous cell to avoid anchor zone.`);
1576
+ evt.preventDefault();
1577
+ navigateToNextCell(currentLineNum, targetCellIndex, metadataForTargetLine, true, editorInfo, docManager);
1578
+ return true;
1579
+ }
1580
+ }
1581
+
1582
+ // --- Handle Keys ---
1583
+
1584
+ // 1. Allow non-Tab navigation keys immediately
1585
+ if (isNavigationKey && !isTabKey) {
1586
+ // log(`${logPrefix} Allowing navigation key: ${evt.key}. Clearing click state.`);
1587
+ if (editor) editor.ep_data_tables_last_clicked = null; // Clear state on navigation
1588
+ return false;
1589
+ }
1590
+
1591
+ // 2. Handle Tab - Navigate to next cell (only on keydown to avoid double navigation)
1592
+ if (isTabKey) {
1593
+ // log(`${logPrefix} Tab key pressed. Event type: ${evt.type}`);
1594
+ evt.preventDefault();
1595
+
1596
+ // Only process keydown events for navigation to avoid double navigation
1597
+ if (evt.type !== 'keydown') {
1598
+ // log(`${logPrefix} Ignoring Tab ${evt.type} event to prevent double navigation.`);
1599
+ return true;
1600
+ }
1601
+
1602
+ // log(`${logPrefix} Processing Tab keydown - implementing cell navigation.`);
1603
+ const success = navigateToNextCell(currentLineNum, targetCellIndex, metadataForTargetLine, evt.shiftKey, editorInfo, docManager);
1604
+ if (!success) {
1605
+ // log(`${logPrefix} Tab navigation failed, cell navigation not possible.`);
1606
+ }
1607
+ return true;
1608
+ }
1609
+
1610
+ // 3. Handle Enter - Navigate to cell below (only on keydown to avoid double navigation)
1611
+ if (isEnterKey) {
1612
+ // log(`${logPrefix} Enter key pressed. Event type: ${evt.type}`);
1613
+ evt.preventDefault();
1614
+
1615
+ // Only process keydown events for navigation to avoid double navigation
1616
+ if (evt.type !== 'keydown') {
1617
+ // log(`${logPrefix} Ignoring Enter ${evt.type} event to prevent double navigation.`);
1618
+ return true;
1619
+ }
1620
+
1621
+ // log(`${logPrefix} Processing Enter keydown - implementing cell navigation.`);
1622
+ const success = navigateToCellBelow(currentLineNum, targetCellIndex, metadataForTargetLine, editorInfo, docManager);
1623
+ if (!success) {
1624
+ // log(`${logPrefix} Enter navigation failed, cell navigation not possible.`);
1625
+ }
1626
+ return true;
1627
+ }
1628
+
1629
+ // 4. Intercept destructive keys ONLY at cell boundaries to protect delimiters
1630
+ const currentCellTextLength = cellTexts[targetCellIndex]?.length ?? 0;
1631
+ // Backspace at the very beginning of cell > 0
1632
+ if (isBackspaceKey && relativeCaretPos === 0 && targetCellIndex > 0) {
1633
+ // log(`${logPrefix} Intercepted Backspace at start of cell ${targetCellIndex}. Preventing default.`);
1634
+ evt.preventDefault();
1635
+ return true;
1636
+ }
1637
+ // NEW: Backspace at very beginning of first cell – would merge with previous line
1638
+ if (isBackspaceKey && relativeCaretPos === 0 && targetCellIndex === 0) {
1639
+ // log(`${logPrefix} Intercepted Backspace at start of first cell (line boundary). Preventing merge.`);
1640
+ evt.preventDefault();
1641
+ return true;
1642
+ }
1643
+ // Delete at the very end of cell < last cell
1644
+ if (isDeleteKey && relativeCaretPos === currentCellTextLength && targetCellIndex < metadataForTargetLine.cols - 1) {
1645
+ // log(`${logPrefix} Intercepted Delete at end of cell ${targetCellIndex}. Preventing default.`);
1646
+ evt.preventDefault();
1647
+ return true;
1648
+ }
1649
+ // NEW: Delete at very end of last cell – would merge with next line
1650
+ if (isDeleteKey && relativeCaretPos === currentCellTextLength && targetCellIndex === metadataForTargetLine.cols - 1) {
1651
+ // log(`${logPrefix} Intercepted Delete at end of last cell (line boundary). Preventing merge.`);
1652
+ evt.preventDefault();
1653
+ return true;
1654
+ }
1655
+
1656
+ // 5. Handle Typing/Backspace/Delete WITHIN a cell via manual modification
1657
+ const isInternalBackspace = isBackspaceKey && relativeCaretPos > 0;
1658
+ const isInternalDelete = isDeleteKey && relativeCaretPos < currentCellTextLength;
1659
+
1660
+ // Guard: internal Backspace at relativePos 1 (would delete delimiter) & Delete at relativePos 0
1661
+ if ((isInternalBackspace && relativeCaretPos === 1 && targetCellIndex > 0) ||
1662
+ (isInternalDelete && relativeCaretPos === 0 && targetCellIndex > 0)) {
1663
+ // log(`${logPrefix} Attempt to erase protected delimiter – operation blocked.`);
1664
+ evt.preventDefault();
1665
+ return true;
1666
+ }
1667
+
1668
+ if (isTypingKey || isInternalBackspace || isInternalDelete) {
1669
+ // --- PREVENT TYPING DIRECTLY AFTER DELIMITER (relativeCaretPos===0) ---
1670
+ if (isTypingKey && relativeCaretPos === 0 && targetCellIndex > 0) {
1671
+ // log(`${logPrefix} Caret at forbidden position 0 (just after delimiter). Auto-advancing to position 1.`);
1672
+ const safePosAbs = cellStartCol + 1;
1673
+ editorInfo.ace_performSelectionChange([currentLineNum, safePosAbs], [currentLineNum, safePosAbs], false);
1674
+ editorInfo.ace_updateBrowserSelectionFromRep();
1675
+ relativeCaretPos = 1;
1676
+ // log(`${logPrefix} Caret moved to safe position. New relativeCaretPos=${relativeCaretPos}`);
1677
+ }
1678
+ // *** Use the validated currentLineNum and currentCol derived from relativeCaretPos ***
1679
+ const currentCol = cellStartCol + relativeCaretPos;
1680
+ // log(`${logPrefix} Handling INTERNAL key='${evt.key}' Type='${evt.type}' at Line=${currentLineNum}, Col=${currentCol} (CellIndex=${targetCellIndex}, RelativePos=${relativeCaretPos}).`);
1681
+ // log(`${logPrefix} [caretTrace] Initial rep.selStart for internal edit: Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
1682
+
1683
+ // Only process keydown events for modifications
1684
+ if (evt.type !== 'keydown') {
1685
+ // log(`${logPrefix} Ignoring non-keydown event type ('${evt.type}') for handled key.`);
1686
+ return false;
1687
+ }
1688
+
1689
+ // log(`${logPrefix} Preventing default browser action for keydown event.`);
1690
+ evt.preventDefault();
1691
+
1692
+ let newAbsoluteCaretCol = -1;
1693
+ let repBeforeEdit = null; // Store rep before edits for attribute helper
1694
+
1695
+ try {
1696
+ repBeforeEdit = editorInfo.ace_getRep(); // Get rep *before* making changes
1697
+ // log(`${logPrefix} [caretTrace] rep.selStart before ace_performDocumentReplaceRange: Line=${repBeforeEdit.selStart[0]}, Col=${repBeforeEdit.selStart[1]}`);
1698
+
1699
+ if (isTypingKey) {
1700
+ const insertPos = [currentLineNum, currentCol];
1701
+ // log(`${logPrefix} -> Inserting text '${evt.key}' at [${insertPos}]`);
1702
+ editorInfo.ace_performDocumentReplaceRange(insertPos, insertPos, evt.key);
1703
+ newAbsoluteCaretCol = currentCol + 1;
1704
+
1705
+ } else if (isInternalBackspace) {
1706
+ const delRangeStart = [currentLineNum, currentCol - 1];
1707
+ const delRangeEnd = [currentLineNum, currentCol];
1708
+ // log(`${logPrefix} -> Deleting (Backspace) range [${delRangeStart}]-[${delRangeEnd}]`);
1709
+ editorInfo.ace_performDocumentReplaceRange(delRangeStart, delRangeEnd, '');
1710
+ newAbsoluteCaretCol = currentCol - 1;
1711
+
1712
+ } else if (isInternalDelete) {
1713
+ const delRangeStart = [currentLineNum, currentCol];
1714
+ const delRangeEnd = [currentLineNum, currentCol + 1];
1715
+ // log(`${logPrefix} -> Deleting (Delete) range [${delRangeStart}]-[${delRangeEnd}]`);
1716
+ editorInfo.ace_performDocumentReplaceRange(delRangeStart, delRangeEnd, '');
1717
+ newAbsoluteCaretCol = currentCol; // Caret stays at the same column for delete
1718
+ }
1719
+ const repAfterReplace = editorInfo.ace_getRep();
1720
+ // log(`${logPrefix} [caretTrace] rep.selStart after ace_performDocumentReplaceRange: Line=${repAfterReplace.selStart[0]}, Col=${repAfterReplace.selStart[1]}`);
1721
+
1722
+
1723
+ // *** CRITICAL: Re-apply the line attribute after ANY modification ***
1724
+ // log(`${logPrefix} -> Re-applying tbljson line attribute...`);
1725
+
1726
+ // DEBUG: Log the values before calculating attrStringToApply
1727
+ // log(`${logPrefix} DEBUG: Before calculating attrStringToApply - trustedLastClick=${trustedLastClick}, reportedLineNum=${reportedLineNum}, currentLineNum=${currentLineNum}`);
1728
+ // log(`${logPrefix} DEBUG: lineAttrString value:`, lineAttrString ? `"${lineAttrString}"` : 'null/undefined');
1729
+
1730
+ const applyHelper = editorInfo.ep_data_tables_applyMeta;
1731
+ if (applyHelper && typeof applyHelper === 'function' && repBeforeEdit) {
1732
+ // Pass the original lineAttrString if available AND if it belongs to the currentLineNum
1733
+ const attrStringToApply = (trustedLastClick || reportedLineNum === currentLineNum) ? lineAttrString : null;
1734
+
1735
+ // log(`${logPrefix} DEBUG: Calculated attrStringToApply:`, attrStringToApply ? `"${attrStringToApply}"` : 'null/undefined');
1736
+ // log(`${logPrefix} DEBUG: Condition result: (${trustedLastClick} || ${reportedLineNum} === ${currentLineNum}) = ${trustedLastClick || reportedLineNum === currentLineNum}`);
1737
+
1738
+ applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, repBeforeEdit, editorInfo, attrStringToApply, docManager);
1739
+ // log(`${logPrefix} -> tbljson line attribute re-applied (using rep before edit).`);
1740
+ } else {
1741
+ console.error(`${logPrefix} -> FAILED to re-apply tbljson attribute (helper or repBeforeEdit missing).`);
1742
+ const currentRepFallback = editorInfo.ace_getRep();
1743
+ if (applyHelper && typeof applyHelper === 'function' && currentRepFallback) {
1744
+ // log(`${logPrefix} -> Retrying attribute application with current rep...`);
1745
+ applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, currentRepFallback, editorInfo, null, docManager); // Cannot guarantee old attr string is valid here
1746
+ // log(`${logPrefix} -> tbljson line attribute re-applied (using current rep fallback).`);
1747
+ } else {
1748
+ console.error(`${logPrefix} -> FAILED to re-apply tbljson attribute even with fallback rep.`);
1749
+ }
1750
+ }
1751
+
1752
+ // Set caret position immediately
1753
+ if (newAbsoluteCaretCol >= 0) {
1754
+ const newCaretPos = [currentLineNum, newAbsoluteCaretCol]; // Use the trusted currentLineNum
1755
+ // log(`${logPrefix} -> Setting selection immediately to:`, newCaretPos);
1756
+ // log(`${logPrefix} [caretTrace] rep.selStart before ace_performSelectionChange: Line=${editorInfo.ace_getRep().selStart[0]}, Col=${editorInfo.ace_getRep().selStart[1]}`);
1757
+ try {
1758
+ editorInfo.ace_performSelectionChange(newCaretPos, newCaretPos, false);
1759
+ const repAfterSelectionChange = editorInfo.ace_getRep();
1760
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performSelectionChange: Line=${repAfterSelectionChange.selStart[0]}, Col=${repAfterSelectionChange.selStart[1]}`);
1761
+ // log(`${logPrefix} -> Selection set immediately.`);
1762
+
1763
+ // Add sync hint AFTER setting selection
1764
+ editorInfo.ace_fastIncorp(1);
1765
+ const repAfterFastIncorp = editorInfo.ace_getRep();
1766
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_fastIncorp: Line=${repAfterFastIncorp.selStart[0]}, Col=${repAfterFastIncorp.selStart[1]}`);
1767
+ // log(`${logPrefix} -> Requested sync hint (fastIncorp 1).`);
1768
+
1769
+ // --- Re-assert selection ---
1770
+ const targetCaretPosForReassert = [currentLineNum, newAbsoluteCaretCol];
1771
+ // log(`${logPrefix} [caretTrace] Attempting to re-assert selection post-fastIncorp to [${targetCaretPosForReassert[0]}, ${targetCaretPosForReassert[1]}]`);
1772
+ editorInfo.ace_performSelectionChange(targetCaretPosForReassert, targetCaretPosForReassert, false);
1773
+ const repAfterReassert = editorInfo.ace_getRep();
1774
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after re-asserting selection: Line=${repAfterReassert.selStart[0]}, Col=${repAfterReassert.selStart[1]}`);
1775
+
1776
+ // Store the updated caret info for the next event
1777
+ const newRelativePos = newAbsoluteCaretCol - cellStartCol;
1778
+ editor.ep_data_tables_last_clicked = {
1779
+ lineNum: currentLineNum,
1780
+ tblId: metadataForTargetLine.tblId,
1781
+ cellIndex: targetCellIndex,
1782
+ relativePos: newRelativePos
1783
+ };
1784
+ // log(`${logPrefix} -> Updated stored click/caret info:`, editor.ep_data_tables_last_clicked);
1785
+ // log(`${logPrefix} [caretTrace] Updated ep_data_tables_last_clicked. Line=${editor.ep_data_tables_last_clicked.lineNum}, Cell=${editor.ep_data_tables_last_clicked.cellIndex}, RelPos=${editor.ep_data_tables_last_clicked.relativePos}`);
1786
+
1787
+
1788
+ } catch (selError) {
1789
+ console.error(`${logPrefix} -> ERROR setting selection immediately:`, selError);
1790
+ }
1791
+ } else {
1792
+ // log(`${logPrefix} -> Warning: newAbsoluteCaretCol not set, skipping selection update.`);
1793
+ }
1794
+
1795
+ } catch (error) {
1796
+ // log(`${logPrefix} ERROR during manual key handling:`, error);
1797
+ console.error('[ep_data_tables] Error processing key event update:', error);
1798
+ // Maybe return false to allow default as a fallback on error?
1799
+ // For now, return true as we prevented default.
1800
+ return true;
1801
+ }
1802
+
1803
+ const endLogTime = Date.now();
1804
+ // log(`${logPrefix} END (Handled Internal Edit Manually) Key='${evt.key}' Type='${evt.type}' -> Returned true. Duration: ${endLogTime - startLogTime}ms`);
1805
+ return true; // We handled the key event
1806
+
1807
+ } // End if(isTypingKey || isInternalBackspace || isInternalDelete)
1808
+
1809
+
1810
+ // Fallback for any other keys or edge cases not handled above
1811
+ const endLogTimeFinal = Date.now();
1812
+ // log(`${logPrefix} END (Fell Through / Unhandled Case) Key='${evt.key}' Type='${evt.type}'. Allowing default. Duration: ${endLogTimeFinal - startLogTime}ms`);
1813
+ // Clear click state if it wasn't handled?
1814
+ // if (editor?.ep_data_tables_last_clicked) editor.ep_data_tables_last_clicked = null;
1815
+ // log(`${logPrefix} [caretTrace] Final rep.selStart at end of aceKeyEvent (if unhandled): Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
1816
+ return false; // Allow default browser/ACE handling
1817
+ };
1818
+
1819
+ // ───────────────────── ace init + public helpers ─────────────────────
1820
+ exports.aceInitialized = (h, ctx) => {
1821
+ const logPrefix = '[ep_data_tables:aceInitialized]';
1822
+ // log(`${logPrefix} START`, { hook_name: h, context: ctx });
1823
+ const ed = ctx.editorInfo;
1824
+ const docManager = ctx.documentAttributeManager;
1825
+
1826
+ // log(`${logPrefix} Attaching ep_data_tables_applyMeta helper to editorInfo.`);
1827
+ ed.ep_data_tables_applyMeta = applyTableLineMetadataAttribute;
1828
+ // log(`${logPrefix}: Attached applyTableLineMetadataAttribute helper to ed.ep_data_tables_applyMeta successfully.`);
1829
+
1830
+ // Store the documentAttributeManager reference for later use
1831
+ // log(`${logPrefix} Storing documentAttributeManager reference on editorInfo.`);
1832
+ ed.ep_data_tables_docManager = docManager;
1833
+ // log(`${logPrefix}: Stored documentAttributeManager reference as ed.ep_data_tables_docManager.`);
1834
+
1835
+ // *** ENHANCED: Paste event listener + Column resize listeners ***
1836
+ // log(`${logPrefix} Preparing to attach paste and resize listeners via ace_callWithAce.`);
1837
+ ed.ace_callWithAce((ace) => {
1838
+ const callWithAceLogPrefix = '[ep_data_tables:aceInitialized:callWithAceForListeners]';
1839
+ // log(`${callWithAceLogPrefix} Entered ace_callWithAce callback for listeners.`);
1840
+
1841
+ if (!ace || !ace.editor) {
1842
+ console.error(`${callWithAceLogPrefix} ERROR: ace or ace.editor is not available. Cannot attach listeners.`);
1843
+ // log(`${callWithAceLogPrefix} Aborting listener attachment due to missing ace.editor.`);
1844
+ return;
1845
+ }
1846
+ const editor = ace.editor;
1847
+ // log(`${callWithAceLogPrefix} ace.editor obtained successfully.`);
1848
+
1849
+ // Store editor reference for later use in table operations
1850
+ // log(`${logPrefix} Storing editor reference on editorInfo.`);
1851
+ ed.ep_data_tables_editor = editor;
1852
+ // log(`${logPrefix}: Stored editor reference as ed.ep_data_tables_editor.`);
1853
+
1854
+ // Attempt to find the inner iframe body, similar to ep_image_insert
1855
+ let $inner;
1856
+ try {
1857
+ // log(`${callWithAceLogPrefix} Attempting to find inner iframe body for listener attachment.`);
1858
+ const $iframeOuter = $('iframe[name="ace_outer"]');
1859
+ if ($iframeOuter.length === 0) {
1860
+ console.error(`${callWithAceLogPrefix} ERROR: Could not find outer iframe (ace_outer).`);
1861
+ // log(`${callWithAceLogPrefix} Failed to find ace_outer.`);
1862
+ return;
1863
+ }
1864
+ // log(`${callWithAceLogPrefix} Found ace_outer:`, $iframeOuter);
1865
+
1866
+ const $iframeInner = $iframeOuter.contents().find('iframe[name="ace_inner"]');
1867
+ if ($iframeInner.length === 0) {
1868
+ console.error(`${callWithAceLogPrefix} ERROR: Could not find inner iframe (ace_inner).`);
1869
+ // log(`${callWithAceLogPrefix} Failed to find ace_inner within ace_outer.`);
1870
+ return;
1871
+ }
1872
+ // log(`${callWithAceLogPrefix} Found ace_inner:`, $iframeInner);
1873
+
1874
+ const innerDocBody = $iframeInner.contents().find('body');
1875
+ if (innerDocBody.length === 0) {
1876
+ console.error(`${callWithAceLogPrefix} ERROR: Could not find body element in inner iframe.`);
1877
+ // log(`${callWithAceLogPrefix} Failed to find body in ace_inner.`);
1878
+ return;
1879
+ }
1880
+ $inner = $(innerDocBody[0]); // Ensure it's a jQuery object of the body itself
1881
+ // log(`${callWithAceLogPrefix} Successfully found inner iframe body:`, $inner);
1882
+ } catch (e) {
1883
+ console.error(`${callWithAceLogPrefix} ERROR: Exception while trying to find inner iframe body:`, e);
1884
+ // log(`${callWithAceLogPrefix} Exception details:`, { message: e.message, stack: e.stack });
1885
+ return;
1886
+ }
1887
+
1888
+ if (!$inner || $inner.length === 0) {
1889
+ console.error(`${callWithAceLogPrefix} ERROR: $inner is not valid after attempting to find iframe body. Cannot attach listeners.`);
1890
+ // log(`${callWithAceLogPrefix} $inner is invalid. Aborting.`);
1891
+ return;
1892
+ }
1893
+
1894
+ // *** CUT EVENT LISTENER ***
1895
+ // log(`${callWithAceLogPrefix} Attaching cut event listener to $inner (inner iframe body).`);
1896
+ $inner.on('cut', (evt) => {
1897
+ const cutLogPrefix = '[ep_data_tables:cutHandler]';
1898
+ // log(`${cutLogPrefix} CUT EVENT TRIGGERED. Event object:`, evt);
1899
+
1900
+ // log(`${cutLogPrefix} Getting current editor representation (rep).`);
1901
+ const rep = ed.ace_getRep();
1902
+ if (!rep || !rep.selStart) {
1903
+ // log(`${cutLogPrefix} WARNING: Could not get representation or selection. Allowing default cut.`);
1904
+ console.warn(`${cutLogPrefix} Could not get rep or selStart.`);
1905
+ return; // Allow default
1906
+ }
1907
+ // log(`${cutLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
1908
+ const selStart = rep.selStart;
1909
+ const selEnd = rep.selEnd;
1910
+ const lineNum = selStart[0];
1911
+ // log(`${cutLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
1912
+
1913
+ // Check if there's actually a selection to cut
1914
+ if (selStart[0] === selEnd[0] && selStart[1] === selEnd[1]) {
1915
+ // log(`${cutLogPrefix} No selection to cut. Allowing default cut.`);
1916
+ return; // Allow default - nothing to cut
1917
+ }
1918
+
1919
+ // Check if selection spans multiple lines
1920
+ if (selStart[0] !== selEnd[0]) {
1921
+ // log(`${cutLogPrefix} WARNING: Selection spans multiple lines. Preventing cut to protect table structure.`);
1922
+ evt.preventDefault();
1923
+ return;
1924
+ }
1925
+
1926
+ // log(`${cutLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
1927
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
1928
+ let tableMetadata = null;
1929
+
1930
+ if (lineAttrString) {
1931
+ // Fast-path: attribute exists – parse it.
1932
+ try {
1933
+ tableMetadata = JSON.parse(lineAttrString);
1934
+ } catch {}
1935
+ }
1936
+
1937
+ if (!tableMetadata) {
1938
+ // Fallback for block-styled rows – reconstruct via DOM helper.
1939
+ tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
1940
+ }
1941
+
1942
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
1943
+ // log(`${cutLogPrefix} Line ${lineNum} is NOT a recognised table line. Allowing default cut.`);
1944
+ return; // Not a table line
1945
+ }
1946
+
1947
+ // log(`${cutLogPrefix} Line ${lineNum} IS a table line. Metadata:`, tableMetadata);
1948
+
1949
+ // Validate selection is within cell boundaries
1950
+ const lineText = rep.lines.atIndex(lineNum)?.text || '';
1951
+ const cells = lineText.split(DELIMITER);
1952
+ let currentOffset = 0;
1953
+ let targetCellIndex = -1;
1954
+ let cellStartCol = 0;
1955
+ let cellEndCol = 0;
1956
+
1957
+ for (let i = 0; i < cells.length; i++) {
1958
+ const cellLength = cells[i]?.length ?? 0;
1959
+ const cellEndColThisIteration = currentOffset + cellLength;
1960
+
1961
+ if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
1962
+ targetCellIndex = i;
1963
+ cellStartCol = currentOffset;
1964
+ cellEndCol = cellEndColThisIteration;
1965
+ break;
1966
+ }
1967
+ currentOffset += cellLength + DELIMITER.length;
1968
+ }
1969
+
1970
+ /* allow "…cell content + delimiter" selections */
1971
+ const wouldClampStart = targetCellIndex > 0 && selStart[1] === cellStartCol - DELIMITER.length;
1972
+ const wouldClampEnd = targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length;
1973
+
1974
+ console.log(`[ep_data_tables:cut-handler] Cut selection analysis:`, {
1975
+ targetCellIndex,
1976
+ selStartCol: selStart[1],
1977
+ selEndCol: selEnd[1],
1978
+ cellStartCol,
1979
+ cellEndCol,
1980
+ delimiterLength: DELIMITER.length,
1981
+ expectedLeadingDelimiterPos: cellStartCol - DELIMITER.length,
1982
+ expectedTrailingDelimiterPos: cellEndCol + DELIMITER.length,
1983
+ wouldClampStart,
1984
+ wouldClampEnd
1985
+ });
1986
+
1987
+ if (wouldClampStart) {
1988
+ console.log(`[ep_data_tables:cut-handler] CLAMPING cut selection start from ${selStart[1]} to ${cellStartCol}`);
1989
+ selStart[1] = cellStartCol; // clamp
1990
+ }
1991
+
1992
+ if (wouldClampEnd) {
1993
+ console.log(`[ep_data_tables:cut-handler] CLAMPING cut selection end from ${selEnd[1]} to ${cellEndCol}`);
1994
+ selEnd[1] = cellEndCol; // clamp
1995
+ }
1996
+ if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
1997
+ // log(`${cutLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing cut to protect table structure.`);
1998
+ evt.preventDefault();
1999
+ return;
2000
+ }
2001
+
2002
+ // If we reach here, the selection is entirely within a single cell - allow cut and preserve table structure
2003
+ // log(`${cutLogPrefix} Selection is entirely within cell ${targetCellIndex}. Intercepting cut to preserve table structure.`);
2004
+ evt.preventDefault();
2005
+
2006
+ try {
2007
+ // Get the selected text to copy to clipboard
2008
+ const selectedText = lineText.substring(selStart[1], selEnd[1]);
2009
+ // log(`${cutLogPrefix} Selected text to cut: "${selectedText}"`);
2010
+
2011
+ // Copy to clipboard manually
2012
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2013
+ navigator.clipboard.writeText(selectedText).then(() => {
2014
+ // log(`${cutLogPrefix} Successfully copied to clipboard via Navigator API.`);
2015
+ }).catch((err) => {
2016
+ console.warn(`${cutLogPrefix} Failed to copy to clipboard via Navigator API:`, err);
2017
+ });
2018
+ } else {
2019
+ // Fallback for older browsers
2020
+ // log(`${cutLogPrefix} Using fallback clipboard method.`);
2021
+ const textArea = document.createElement('textarea');
2022
+ textArea.value = selectedText;
2023
+ document.body.appendChild(textArea);
2024
+ textArea.select();
2025
+ try {
2026
+ document.execCommand('copy');
2027
+ // log(`${cutLogPrefix} Successfully copied to clipboard via execCommand fallback.`);
2028
+ } catch (err) {
2029
+ console.warn(`${cutLogPrefix} Failed to copy to clipboard via fallback:`, err);
2030
+ }
2031
+ document.body.removeChild(textArea);
2032
+ }
2033
+
2034
+ // Now perform the deletion within the cell using ace operations
2035
+ // log(`${cutLogPrefix} Performing deletion via ed.ace_callWithAce.`);
2036
+ ed.ace_callWithAce((aceInstance) => {
2037
+ const callAceLogPrefix = `${cutLogPrefix}[ace_callWithAceOps]`;
2038
+ // log(`${callAceLogPrefix} Entered ace_callWithAce for cut operations. selStart:`, selStart, `selEnd:`, selEnd);
2039
+
2040
+ // log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to delete selected text.`);
2041
+ aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, '');
2042
+ // log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
2043
+
2044
+ // --- Ensure cell is not left empty (zero-length) ---
2045
+ const repAfterDeletion = aceInstance.ace_getRep();
2046
+ const lineTextAfterDeletion = repAfterDeletion.lines.atIndex(lineNum).text;
2047
+ const cellsAfterDeletion = lineTextAfterDeletion.split(DELIMITER);
2048
+ const cellTextAfterDeletion = cellsAfterDeletion[targetCellIndex] || '';
2049
+
2050
+ if (cellTextAfterDeletion.length === 0) {
2051
+ // log(`${callAceLogPrefix} Cell ${targetCellIndex} became empty after cut – inserting single space to preserve structure.`);
2052
+ const insertPos = [lineNum, selStart[1]]; // Start of the now-empty cell
2053
+ aceInstance.ace_performDocumentReplaceRange(insertPos, insertPos, ' ');
2054
+
2055
+ // NEW – re-apply td attribute to the freshly inserted space
2056
+ const attrStart = insertPos;
2057
+ const attrEnd = [insertPos[0], insertPos[1] + 1];
2058
+ aceInstance.ace_performDocumentApplyAttributesToRange(
2059
+ attrStart, attrEnd, [[ATTR_CELL, String(targetCellIndex)]],
2060
+ );
2061
+ }
2062
+
2063
+ // log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
2064
+ const repAfterCut = aceInstance.ace_getRep();
2065
+ // log(`${callAceLogPrefix} Fetched rep after cut for applyMeta. Line ${lineNum} text now: "${repAfterCut.lines.atIndex(lineNum).text}"`);
2066
+
2067
+ ed.ep_data_tables_applyMeta(
2068
+ lineNum,
2069
+ tableMetadata.tblId,
2070
+ tableMetadata.row,
2071
+ tableMetadata.cols,
2072
+ repAfterCut,
2073
+ ed,
2074
+ null,
2075
+ docManager
2076
+ );
2077
+ // log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
2078
+
2079
+ const newCaretPos = [lineNum, selStart[1]];
2080
+ // log(`${callAceLogPrefix} Setting caret position to: [${newCaretPos}].`);
2081
+ aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2082
+ // log(`${callAceLogPrefix} Selection change successful.`);
2083
+
2084
+ // log(`${callAceLogPrefix} Cut operations within ace_callWithAce completed successfully.`);
2085
+ }, 'tableCutTextOperations', true);
2086
+
2087
+ // log(`${cutLogPrefix} Cut operation completed successfully.`);
2088
+ } catch (error) {
2089
+ console.error(`${cutLogPrefix} ERROR during cut operation:`, error);
2090
+ // log(`${cutLogPrefix} Cut operation failed. Error details:`, { message: error.message, stack: error.stack });
2091
+ }
2092
+ });
2093
+
2094
+ // *** BEFOREINPUT EVENT LISTENER FOR CONTEXT-MENU DELETE ***
2095
+ // log(`${callWithAceLogPrefix} Attaching beforeinput event listener to $inner (inner iframe body).`);
2096
+ $inner.on('beforeinput', (evt) => {
2097
+ const deleteLogPrefix = '[ep_data_tables:beforeinputDeleteHandler]';
2098
+ // log(`${deleteLogPrefix} BEFOREINPUT EVENT TRIGGERED. inputType: "${evt.originalEvent.inputType}", event object:`, evt);
2099
+
2100
+ // Only intercept deletion-related input events
2101
+ if (!evt.originalEvent.inputType || !evt.originalEvent.inputType.startsWith('delete')) {
2102
+ // log(`${deleteLogPrefix} Not a deletion event (inputType: "${evt.originalEvent.inputType}"). Allowing default.`);
2103
+ return; // Allow default for non-delete events
2104
+ }
2105
+
2106
+ // log(`${deleteLogPrefix} Getting current editor representation (rep).`);
2107
+ const rep = ed.ace_getRep();
2108
+ if (!rep || !rep.selStart) {
2109
+ // log(`${deleteLogPrefix} WARNING: Could not get representation or selection. Allowing default delete.`);
2110
+ console.warn(`${deleteLogPrefix} Could not get rep or selStart.`);
2111
+ return; // Allow default
2112
+ }
2113
+ // log(`${deleteLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
2114
+ const selStart = rep.selStart;
2115
+ const selEnd = rep.selEnd;
2116
+ const lineNum = selStart[0];
2117
+ // log(`${deleteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
2118
+
2119
+ // Check if there's actually a selection to delete
2120
+ if (selStart[0] === selEnd[0] && selStart[1] === selEnd[1]) {
2121
+ // log(`${deleteLogPrefix} No selection to delete. Allowing default delete.`);
2122
+ return; // Allow default - nothing to delete
2123
+ }
2124
+
2125
+ // Check if selection spans multiple lines
2126
+ if (selStart[0] !== selEnd[0]) {
2127
+ // log(`${deleteLogPrefix} WARNING: Selection spans multiple lines. Preventing delete to protect table structure.`);
2128
+ evt.preventDefault();
2129
+ return;
2130
+ }
2131
+
2132
+ // log(`${deleteLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
2133
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2134
+ let tableMetadata = null;
2135
+
2136
+ if (lineAttrString) {
2137
+ // Fast-path: attribute exists – parse it.
2138
+ try {
2139
+ tableMetadata = JSON.parse(lineAttrString);
2140
+ } catch {}
2141
+ }
2142
+
2143
+ if (!tableMetadata) {
2144
+ // Fallback for block-styled rows – reconstruct via DOM helper.
2145
+ tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
2146
+ }
2147
+
2148
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
2149
+ // log(`${deleteLogPrefix} Line ${lineNum} is NOT a recognised table line. Allowing default delete.`);
2150
+ return; // Not a table line
2151
+ }
2152
+
2153
+ // log(`${deleteLogPrefix} Line ${lineNum} IS a table line. Metadata:`, tableMetadata);
2154
+
2155
+ // Validate selection is within cell boundaries
2156
+ const lineText = rep.lines.atIndex(lineNum)?.text || '';
2157
+ const cells = lineText.split(DELIMITER);
2158
+ let currentOffset = 0;
2159
+ let targetCellIndex = -1;
2160
+ let cellStartCol = 0;
2161
+ let cellEndCol = 0;
2162
+
2163
+ for (let i = 0; i < cells.length; i++) {
2164
+ const cellLength = cells[i]?.length ?? 0;
2165
+ const cellEndColThisIteration = currentOffset + cellLength;
2166
+
2167
+ if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
2168
+ targetCellIndex = i;
2169
+ cellStartCol = currentOffset;
2170
+ cellEndCol = cellEndColThisIteration;
2171
+ break;
2172
+ }
2173
+ currentOffset += cellLength + DELIMITER.length;
2174
+ }
2175
+
2176
+ /* allow "…cell content + delimiter" selections */
2177
+ const wouldClampStart = targetCellIndex > 0 && selStart[1] === cellStartCol - DELIMITER.length;
2178
+ const wouldClampEnd = targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length;
2179
+
2180
+ console.log(`[ep_data_tables:beforeinput-delete] Delete selection analysis:`, {
2181
+ targetCellIndex,
2182
+ selStartCol: selStart[1],
2183
+ selEndCol: selEnd[1],
2184
+ cellStartCol,
2185
+ cellEndCol,
2186
+ delimiterLength: DELIMITER.length,
2187
+ expectedLeadingDelimiterPos: cellStartCol - DELIMITER.length,
2188
+ expectedTrailingDelimiterPos: cellEndCol + DELIMITER.length,
2189
+ wouldClampStart,
2190
+ wouldClampEnd
2191
+ });
2192
+
2193
+ if (wouldClampStart) {
2194
+ console.log(`[ep_data_tables:beforeinput-delete] CLAMPING delete selection start from ${selStart[1]} to ${cellStartCol}`);
2195
+ selStart[1] = cellStartCol; // clamp
2196
+ }
2197
+
2198
+ if (wouldClampEnd) {
2199
+ console.log(`[ep_data_tables:beforeinput-delete] CLAMPING delete selection end from ${selEnd[1]} to ${cellEndCol}`);
2200
+ selEnd[1] = cellEndCol; // clamp
2201
+ }
2202
+
2203
+ if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
2204
+ // log(`${deleteLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing delete to protect table structure.`);
2205
+ evt.preventDefault();
2206
+ return;
2207
+ }
2208
+
2209
+ // If we reach here, the selection is entirely within a single cell - intercept delete and preserve table structure
2210
+ // log(`${deleteLogPrefix} Selection is entirely within cell ${targetCellIndex}. Intercepting delete to preserve table structure.`);
2211
+ evt.preventDefault();
2212
+
2213
+ try {
2214
+ // No clipboard operations needed for delete - just perform the deletion within the cell using ace operations
2215
+ // log(`${deleteLogPrefix} Performing deletion via ed.ace_callWithAce.`);
2216
+ ed.ace_callWithAce((aceInstance) => {
2217
+ const callAceLogPrefix = `${deleteLogPrefix}[ace_callWithAceOps]`;
2218
+ // log(`${callAceLogPrefix} Entered ace_callWithAce for delete operations. selStart:`, selStart, `selEnd:`, selEnd);
2219
+
2220
+ // log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to delete selected text.`);
2221
+ aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, '');
2222
+ // log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
2223
+
2224
+ // --- Ensure cell is not left empty (zero-length) ---
2225
+ const repAfterDeletion = aceInstance.ace_getRep();
2226
+ const lineTextAfterDeletion = repAfterDeletion.lines.atIndex(lineNum).text;
2227
+ const cellsAfterDeletion = lineTextAfterDeletion.split(DELIMITER);
2228
+ const cellTextAfterDeletion = cellsAfterDeletion[targetCellIndex] || '';
2229
+
2230
+ if (cellTextAfterDeletion.length === 0) {
2231
+ // log(`${callAceLogPrefix} Cell ${targetCellIndex} became empty after delete – inserting single space to preserve structure.`);
2232
+ const insertPos = [lineNum, selStart[1]]; // Start of the now-empty cell
2233
+ aceInstance.ace_performDocumentReplaceRange(insertPos, insertPos, ' ');
2234
+
2235
+ // NEW – give the placeholder its cell attribute back
2236
+ const attrStart = insertPos;
2237
+ const attrEnd = [insertPos[0], insertPos[1] + 1];
2238
+ aceInstance.ace_performDocumentApplyAttributesToRange(
2239
+ attrStart, attrEnd, [[ATTR_CELL, String(targetCellIndex)]],
2240
+ );
2241
+ }
2242
+
2243
+ // log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
2244
+ const repAfterDelete = aceInstance.ace_getRep();
2245
+ // log(`${callAceLogPrefix} Fetched rep after delete for applyMeta. Line ${lineNum} text now: "${repAfterDelete.lines.atIndex(lineNum).text}"`);
2246
+
2247
+ ed.ep_data_tables_applyMeta(
2248
+ lineNum,
2249
+ tableMetadata.tblId,
2250
+ tableMetadata.row,
2251
+ tableMetadata.cols,
2252
+ repAfterDelete,
2253
+ ed,
2254
+ null,
2255
+ docManager
2256
+ );
2257
+ // log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
2258
+
2259
+ // Determine new caret position – one char forward if we inserted a space
2260
+ const newCaretAbsoluteCol = (cellTextAfterDeletion.length === 0) ? selStart[1] + 1 : selStart[1];
2261
+ const newCaretPos = [lineNum, newCaretAbsoluteCol];
2262
+ // log(`${callAceLogPrefix} Setting caret position to: [${newCaretPos}].`);
2263
+ aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2264
+ // log(`${callAceLogPrefix} Selection change successful.`);
2265
+
2266
+ // log(`${callAceLogPrefix} Delete operations within ace_callWithAce completed successfully.`);
2267
+ }, 'tableDeleteTextOperations', true);
2268
+
2269
+ // log(`${deleteLogPrefix} Delete operation completed successfully.`);
2270
+ } catch (error) {
2271
+ console.error(`${deleteLogPrefix} ERROR during delete operation:`, error);
2272
+ // log(`${deleteLogPrefix} Delete operation failed. Error details:`, { message: error.message, stack: error.stack });
2273
+ }
2274
+ });
2275
+
2276
+ // *** DRAG AND DROP EVENT LISTENERS ***
2277
+ // log(`${callWithAceLogPrefix} Attaching drag and drop event listeners to $inner (inner iframe body).`);
2278
+
2279
+ // Prevent drops that could damage table structure
2280
+ $inner.on('drop', (evt) => {
2281
+ const dropLogPrefix = '[ep_data_tables:dropHandler]';
2282
+ // log(`${dropLogPrefix} DROP EVENT TRIGGERED. Event object:`, evt);
2283
+
2284
+ // log(`${dropLogPrefix} Getting current editor representation (rep).`);
2285
+ const rep = ed.ace_getRep();
2286
+ if (!rep || !rep.selStart) {
2287
+ // log(`${dropLogPrefix} WARNING: Could not get representation or selection. Allowing default drop.`);
2288
+ return; // Allow default
2289
+ }
2290
+
2291
+ const selStart = rep.selStart;
2292
+ const lineNum = selStart[0];
2293
+ // log(`${dropLogPrefix} Current line number: ${lineNum}.`);
2294
+
2295
+ // Check if we're dropping onto a table line
2296
+ // log(`${dropLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
2297
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2298
+ let isTableLine = !!lineAttrString;
2299
+
2300
+ if (!isTableLine) {
2301
+ const metadataFallback = getTableLineMetadata(lineNum, ed, docManager);
2302
+ isTableLine = !!metadataFallback;
2303
+ }
2304
+
2305
+ if (isTableLine) {
2306
+ // log(`${dropLogPrefix} Line ${lineNum} IS a table line. Preventing drop to protect table structure.`);
2307
+ evt.preventDefault();
2308
+ evt.stopPropagation();
2309
+ console.warn('[ep_data_tables] Drop operation prevented to protect table structure. Please use copy/paste within table cells.');
2310
+ }
2311
+ });
2312
+
2313
+ // Also prevent dragover to ensure drop events are properly handled
2314
+ $inner.on('dragover', (evt) => {
2315
+ const dragLogPrefix = '[ep_data_tables:dragoverHandler]';
2316
+
2317
+ const rep = ed.ace_getRep();
2318
+ if (!rep || !rep.selStart) {
2319
+ return; // Allow default
2320
+ }
2321
+
2322
+ const selStart = rep.selStart;
2323
+ const lineNum = selStart[0];
2324
+
2325
+ // Check if we're dragging over a table line
2326
+ // log(`${dragLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
2327
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2328
+ let isTableLine = !!lineAttrString;
2329
+
2330
+ if (!isTableLine) {
2331
+ isTableLine = !!getTableLineMetadata(lineNum, ed, docManager);
2332
+ }
2333
+
2334
+ if (isTableLine) {
2335
+ // log(`${dragLogPrefix} Preventing dragover on table line ${lineNum} to control drop handling.`);
2336
+ evt.preventDefault();
2337
+ }
2338
+ });
2339
+
2340
+ // *** EXISTING PASTE LISTENER ***
2341
+ // log(`${callWithAceLogPrefix} Attaching paste event listener to $inner (inner iframe body).`);
2342
+ $inner.on('paste', (evt) => {
2343
+ const pasteLogPrefix = '[ep_data_tables:pasteHandler]';
2344
+ // log(`${pasteLogPrefix} PASTE EVENT TRIGGERED. Event object:`, evt);
2345
+
2346
+ // log(`${pasteLogPrefix} Getting current editor representation (rep).`);
2347
+ const rep = ed.ace_getRep();
2348
+ if (!rep || !rep.selStart) {
2349
+ // log(`${pasteLogPrefix} WARNING: Could not get representation or selection. Allowing default paste.`);
2350
+ console.warn(`${pasteLogPrefix} Could not get rep or selStart.`);
2351
+ return; // Allow default
2352
+ }
2353
+ // log(`${pasteLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
2354
+ const selStart = rep.selStart;
2355
+ const selEnd = rep.selEnd;
2356
+ const lineNum = selStart[0];
2357
+ // log(`${pasteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
2358
+
2359
+ // NEW: Check if selection spans multiple lines
2360
+ if (selStart[0] !== selEnd[0]) {
2361
+ // log(`${pasteLogPrefix} WARNING: Selection spans multiple lines. Preventing paste to protect table structure.`);
2362
+ evt.preventDefault();
2363
+ return;
2364
+ }
2365
+
2366
+ // log(`${pasteLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
2367
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2368
+ let tableMetadata = null;
2369
+
2370
+ if (!lineAttrString) {
2371
+ // Block-styled row? Reconstruct metadata from the DOM.
2372
+ // log(`${pasteLogPrefix} No '${ATTR_TABLE_JSON}' attribute found. Checking if this is a block-styled table row via DOM reconstruction.`);
2373
+ const fallbackMeta = getTableLineMetadata(lineNum, ed, docManager);
2374
+ if (fallbackMeta) {
2375
+ tableMetadata = fallbackMeta;
2376
+ lineAttrString = JSON.stringify(fallbackMeta);
2377
+ // log(`${pasteLogPrefix} Block-styled table row detected. Reconstructed metadata:`, fallbackMeta);
2378
+ }
2379
+ }
2380
+
2381
+ if (!lineAttrString) {
2382
+ // log(`${pasteLogPrefix} Line ${lineNum} is NOT a table line (no '${ATTR_TABLE_JSON}' attribute found and no DOM reconstruction possible). Allowing default paste.`);
2383
+ return; // Not a table line
2384
+ }
2385
+ // log(`${pasteLogPrefix} Line ${lineNum} IS a table line. Attribute string: "${lineAttrString}".`);
2386
+
2387
+ try {
2388
+ // log(`${pasteLogPrefix} Parsing table metadata from attribute string.`);
2389
+ if (!tableMetadata) {
2390
+ tableMetadata = JSON.parse(lineAttrString);
2391
+ }
2392
+ // log(`${pasteLogPrefix} Parsed table metadata:`, tableMetadata);
2393
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
2394
+ // log(`${pasteLogPrefix} WARNING: Invalid or incomplete table metadata on line ${lineNum}. Allowing default paste. Metadata:`, tableMetadata);
2395
+ console.warn(`${pasteLogPrefix} Invalid table metadata for line ${lineNum}.`);
2396
+ return; // Allow default
2397
+ }
2398
+ // log(`${pasteLogPrefix} Table metadata validated successfully: tblId=${tableMetadata.tblId}, row=${tableMetadata.row}, cols=${tableMetadata.cols}.`);
2399
+ } catch(e) {
2400
+ console.error(`${pasteLogPrefix} ERROR parsing table metadata for line ${lineNum}:`, e);
2401
+ // log(`${pasteLogPrefix} Metadata parse error. Allowing default paste. Error details:`, { message: e.message, stack: e.stack });
2402
+ return; // Allow default
2403
+ }
2404
+
2405
+ // NEW: Validate selection is within cell boundaries
2406
+ const lineText = rep.lines.atIndex(lineNum)?.text || '';
2407
+ const cells = lineText.split(DELIMITER);
2408
+ let currentOffset = 0;
2409
+ let targetCellIndex = -1;
2410
+ let cellStartCol = 0;
2411
+ let cellEndCol = 0;
2412
+
2413
+ for (let i = 0; i < cells.length; i++) {
2414
+ const cellLength = cells[i]?.length ?? 0;
2415
+ const cellEndColThisIteration = currentOffset + cellLength;
2416
+
2417
+ if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
2418
+ targetCellIndex = i;
2419
+ cellStartCol = currentOffset;
2420
+ cellEndCol = cellEndColThisIteration;
2421
+ break;
2422
+ }
2423
+ currentOffset += cellLength + DELIMITER.length;
2424
+ }
2425
+
2426
+ /* allow "…cell content + delimiter" selections */
2427
+ if (targetCellIndex !== -1 &&
2428
+ selEnd[1] === cellEndCol + DELIMITER.length) {
2429
+ selEnd[1] = cellEndCol; // clamp
2430
+ }
2431
+ if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
2432
+ // log(`${pasteLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing paste to protect table structure.`);
2433
+ evt.preventDefault();
2434
+ return;
2435
+ }
2436
+
2437
+ // log(`${pasteLogPrefix} Accessing clipboard data.`);
2438
+ const clipboardData = evt.originalEvent.clipboardData || window.clipboardData;
2439
+ if (!clipboardData) {
2440
+ // log(`${pasteLogPrefix} WARNING: No clipboard data found. Allowing default paste.`);
2441
+ return; // Allow default
2442
+ }
2443
+ // log(`${pasteLogPrefix} Clipboard data object obtained:`, clipboardData);
2444
+
2445
+ // Allow default handling (so ep_hyperlinked_text plugin can process) if rich HTML is present
2446
+ const types = clipboardData.types || [];
2447
+ if (types.includes('text/html') && clipboardData.getData('text/html')) {
2448
+ // log(`${pasteLogPrefix} Detected text/html in clipboard – deferring to other plugins and default paste.`);
2449
+ return; // Do not intercept
2450
+ }
2451
+
2452
+ // log(`${pasteLogPrefix} Getting 'text/plain' from clipboard.`);
2453
+ const pastedTextRaw = clipboardData.getData('text/plain');
2454
+ // log(`${pasteLogPrefix} Pasted text raw: "${pastedTextRaw}" (Type: ${typeof pastedTextRaw})`);
2455
+
2456
+ // ENHANCED: More thorough sanitization of pasted content
2457
+ let pastedText = pastedTextRaw
2458
+ .replace(/(\r\n|\n|\r)/gm, " ") // Replace newlines with space
2459
+ .replace(new RegExp(DELIMITER, 'g'), ' ') // Strip our internal delimiter
2460
+ .replace(/\t/g, " ") // Replace tabs with space
2461
+ .replace(/\s+/g, " ") // Normalize whitespace
2462
+ .trim(); // Trim leading/trailing whitespace
2463
+
2464
+ // log(`${pasteLogPrefix} Pasted text after sanitization: "${pastedText}"`);
2465
+
2466
+ if (typeof pastedText !== 'string' || pastedText.length === 0) {
2467
+ // log(`${pasteLogPrefix} No plain text in clipboard or text is empty (after sanitization). Allowing default paste.`);
2468
+ const types = clipboardData.types;
2469
+ // log(`${pasteLogPrefix} Clipboard types available:`, types);
2470
+ if (types && types.includes('text/html')) {
2471
+ // log(`${pasteLogPrefix} Clipboard also contains HTML:`, clipboardData.getData('text/html'));
2472
+ }
2473
+ return; // Allow default if no plain text
2474
+ }
2475
+ // log(`${pasteLogPrefix} Plain text obtained from clipboard: "${pastedText}". Length: ${pastedText.length}.`);
2476
+
2477
+ // NEW: Check if paste would exceed cell boundaries
2478
+ const currentCellText = cells[targetCellIndex] || '';
2479
+ const selectionLength = selEnd[1] - selStart[1];
2480
+ const newCellLength = currentCellText.length - selectionLength + pastedText.length;
2481
+
2482
+ // Soft safety-valve: Etherpad can technically handle very long lines but
2483
+ // extremely large cells slow down rendering. 8 000 chars ≈ five classic
2484
+ // 'Lorem Ipsum' paragraphs and feels like a reasonable upper bound while
2485
+ // still letting users paste substantive text. Increase/decrease as you
2486
+ // see fit or set to `Infinity` to remove the cap entirely.
2487
+ const MAX_CELL_LENGTH = 8000;
2488
+ if (newCellLength > MAX_CELL_LENGTH) {
2489
+ // log(`${pasteLogPrefix} WARNING: Paste would exceed maximum cell length (${newCellLength} > ${MAX_CELL_LENGTH}). Truncating paste.`);
2490
+ const truncatedPaste = pastedText.substring(0, MAX_CELL_LENGTH - (currentCellText.length - selectionLength));
2491
+ if (truncatedPaste.length === 0) {
2492
+ // log(`${pasteLogPrefix} Paste would be completely truncated. Preventing paste.`);
2493
+ evt.preventDefault();
2494
+ return;
2495
+ }
2496
+ // log(`${pasteLogPrefix} Using truncated paste: "${truncatedPaste}"`);
2497
+ pastedText = truncatedPaste;
2498
+ }
2499
+
2500
+ // log(`${pasteLogPrefix} INTERCEPTING paste of plain text into table line ${lineNum}. PREVENTING DEFAULT browser action.`);
2501
+ evt.preventDefault();
2502
+ // Prevent other plugins from handling the same paste event once we
2503
+ // have intercepted it inside a table cell.
2504
+ evt.stopPropagation();
2505
+ if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
2506
+
2507
+ try {
2508
+ // log(`${pasteLogPrefix} Preparing to perform paste operations via ed.ace_callWithAce.`);
2509
+ ed.ace_callWithAce((aceInstance) => {
2510
+ const callAceLogPrefix = `${pasteLogPrefix}[ace_callWithAceOps]`;
2511
+ // log(`${callAceLogPrefix} Entered ace_callWithAce for paste operations. selStart:`, selStart, `selEnd:`, selEnd);
2512
+
2513
+ // log(`${callAceLogPrefix} Original line text from initial rep: "${rep.lines.atIndex(lineNum).text}". SelStartCol: ${selStart[1]}, SelEndCol: ${selEnd[1]}.`);
2514
+
2515
+ // log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to insert text: "${pastedText}".`);
2516
+ aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, pastedText);
2517
+ // log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
2518
+
2519
+ // log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
2520
+ const repAfterReplace = aceInstance.ace_getRep();
2521
+ // log(`${callAceLogPrefix} Fetched rep after replace for applyMeta. Line ${lineNum} text now: "${repAfterReplace.lines.atIndex(lineNum).text}"`);
2522
+
2523
+ ed.ep_data_tables_applyMeta(
2524
+ lineNum,
2525
+ tableMetadata.tblId,
2526
+ tableMetadata.row,
2527
+ tableMetadata.cols,
2528
+ repAfterReplace,
2529
+ ed,
2530
+ null,
2531
+ docManager
2532
+ );
2533
+ // log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
2534
+
2535
+ const newCaretCol = selStart[1] + pastedText.length;
2536
+ const newCaretPos = [lineNum, newCaretCol];
2537
+ // log(`${callAceLogPrefix} New calculated caret position: [${newCaretPos}]. Setting selection.`);
2538
+ aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2539
+ // log(`${callAceLogPrefix} Selection change successful.`);
2540
+
2541
+ // log(`${callAceLogPrefix} Requesting fastIncorp(10) for sync.`);
2542
+ aceInstance.ace_fastIncorp(10);
2543
+ // log(`${callAceLogPrefix} fastIncorp requested.`);
2544
+
2545
+ // Update stored click/caret info
2546
+ if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
2547
+ const newRelativePos = newCaretCol - cellStartCol;
2548
+ editor.ep_data_tables_last_clicked = {
2549
+ lineNum: lineNum,
2550
+ tblId: tableMetadata.tblId,
2551
+ cellIndex: targetCellIndex,
2552
+ relativePos: newRelativePos < 0 ? 0 : newRelativePos,
2553
+ };
2554
+ // log(`${callAceLogPrefix} Updated stored click/caret info:`, editor.ep_data_tables_last_clicked);
2555
+ }
2556
+
2557
+ // log(`${callAceLogPrefix} Paste operations within ace_callWithAce completed successfully.`);
2558
+ }, 'tablePasteTextOperations', true);
2559
+ // log(`${pasteLogPrefix} ed.ace_callWithAce for paste operations was called.`);
2560
+
2561
+ } catch (error) {
2562
+ console.error(`${pasteLogPrefix} CRITICAL ERROR during paste handling operation:`, error);
2563
+ // log(`${pasteLogPrefix} Error details:`, { message: error.message, stack: error.stack });
2564
+ // log(`${pasteLogPrefix} Paste handling FAILED. END OF HANDLER.`);
2565
+ }
2566
+ });
2567
+ // log(`${callWithAceLogPrefix} Paste event listener attached.`);
2568
+
2569
+ // *** NEW: Column resize listeners ***
2570
+ // log(`${callWithAceLogPrefix} Attaching column resize listeners...`);
2571
+
2572
+ // Get the iframe documents for proper event delegation
2573
+ const $iframeOuter = $('iframe[name="ace_outer"]');
2574
+ const $iframeInner = $iframeOuter.contents().find('iframe[name="ace_inner"]');
2575
+ const innerDoc = $iframeInner.contents();
2576
+ const outerDoc = $iframeOuter.contents();
2577
+
2578
+ // log(`${callWithAceLogPrefix} Found iframe documents: outer=${outerDoc.length}, inner=${innerDoc.length}`);
2579
+
2580
+ // Mousedown on resize handles
2581
+ $inner.on('mousedown', '.ep-data_tables-resize-handle', (evt) => {
2582
+ const resizeLogPrefix = '[ep_data_tables:resizeMousedown]';
2583
+ // log(`${resizeLogPrefix} Resize handle mousedown detected`);
2584
+
2585
+ // Only handle left mouse button clicks
2586
+ if (evt.button !== 0) {
2587
+ // log(`${resizeLogPrefix} Ignoring non-left mouse button: ${evt.button}`);
2588
+ return;
2589
+ }
2590
+
2591
+ // Check if this is related to an image element to avoid conflicts
2592
+ const target = evt.target;
2593
+ const $target = $(target);
2594
+ const isImageRelated = $target.closest('.inline-image, .image-placeholder, .image-inner').length > 0;
2595
+ const isImageResizeHandle = $target.hasClass('image-resize-handle') || $target.closest('.image-resize-handle').length > 0;
2596
+
2597
+ if (isImageRelated || isImageResizeHandle) {
2598
+ // log(`${resizeLogPrefix} Click detected on image-related element or image resize handle, ignoring for table resize`);
2599
+ return;
2600
+ }
2601
+
2602
+ evt.preventDefault();
2603
+ evt.stopPropagation();
2604
+
2605
+ const handle = evt.target;
2606
+ const columnIndex = parseInt(handle.getAttribute('data-column'), 10);
2607
+ const table = handle.closest('table.dataTable');
2608
+ const lineNode = table.closest('div.ace-line');
2609
+
2610
+ // log(`${resizeLogPrefix} Parsed resize target: columnIndex=${columnIndex}, table=${!!table}, lineNode=${!!lineNode}`);
2611
+
2612
+ if (table && lineNode && !isNaN(columnIndex)) {
2613
+ // Get table metadata
2614
+ const tblId = table.getAttribute('data-tblId');
2615
+ const rep = ed.ace_getRep();
2616
+
2617
+ if (!rep || !rep.lines) {
2618
+ console.error(`${resizeLogPrefix} Cannot get editor representation`);
2619
+ return;
2620
+ }
2621
+
2622
+ const lineNum = rep.lines.indexOfKey(lineNode.id);
2623
+
2624
+ // log(`${resizeLogPrefix} Table info: tblId=${tblId}, lineNum=${lineNum}`);
2625
+
2626
+ if (tblId && lineNum !== -1) {
2627
+ try {
2628
+ const lineAttrString = docManager.getAttributeOnLine(lineNum, 'tbljson');
2629
+ if (lineAttrString) {
2630
+ const metadata = JSON.parse(lineAttrString);
2631
+ if (metadata.tblId === tblId) {
2632
+ // log(`${resizeLogPrefix} Starting resize with metadata:`, metadata);
2633
+ startColumnResize(table, columnIndex, evt.clientX, metadata, lineNum);
2634
+ // log(`${resizeLogPrefix} Started resize for column ${columnIndex}`);
2635
+
2636
+ // DEBUG: Verify global state is set
2637
+ // log(`${resizeLogPrefix} Global resize state: isResizing=${isResizing}, targetTable=${!!resizeTargetTable}, targetColumn=${resizeTargetColumn}`);
2638
+ } else {
2639
+ // log(`${resizeLogPrefix} Table ID mismatch: ${metadata.tblId} vs ${tblId}`);
2640
+ }
2641
+ } else {
2642
+ // log(`${resizeLogPrefix} No table metadata found for line ${lineNum}, trying DOM reconstruction...`);
2643
+
2644
+ // Fallback: Reconstruct metadata from DOM (same logic as ace_doDatatableOptions)
2645
+ const rep = ed.ace_getRep();
2646
+ if (rep && rep.lines) {
2647
+ const lineEntry = rep.lines.atIndex(lineNum);
2648
+ if (lineEntry && lineEntry.lineNode) {
2649
+ const tableInDOM = lineEntry.lineNode.querySelector('table.dataTable[data-tblId]');
2650
+ if (tableInDOM) {
2651
+ const domTblId = tableInDOM.getAttribute('data-tblId');
2652
+ const domRow = tableInDOM.getAttribute('data-row');
2653
+ if (domTblId === tblId && domRow !== null) {
2654
+ const domCells = tableInDOM.querySelectorAll('td');
2655
+ if (domCells.length > 0) {
2656
+ // Extract column widths from DOM cells
2657
+ const columnWidths = [];
2658
+ domCells.forEach(cell => {
2659
+ const style = cell.getAttribute('style') || '';
2660
+ const widthMatch = style.match(/width:\s*([0-9.]+)%/);
2661
+ if (widthMatch) {
2662
+ columnWidths.push(parseFloat(widthMatch[1]));
2663
+ } else {
2664
+ // Fallback to equal distribution if no width found
2665
+ columnWidths.push(100 / domCells.length);
2666
+ }
2667
+ });
2668
+
2669
+ // Reconstruct metadata from DOM with preserved column widths
2670
+ const reconstructedMetadata = {
2671
+ tblId: domTblId,
2672
+ row: parseInt(domRow, 10),
2673
+ cols: domCells.length,
2674
+ columnWidths: columnWidths
2675
+ };
2676
+ // log(`${resizeLogPrefix} Reconstructed metadata from DOM:`, reconstructedMetadata);
2677
+
2678
+ startColumnResize(table, columnIndex, evt.clientX, reconstructedMetadata, lineNum);
2679
+ // log(`${resizeLogPrefix} Started resize for column ${columnIndex} using reconstructed metadata`);
2680
+
2681
+ // DEBUG: Verify global state is set
2682
+ // log(`${resizeLogPrefix} Global resize state: isResizing=${isResizing}, targetTable=${!!resizeTargetTable}, targetColumn=${resizeTargetColumn}`);
2683
+ } else {
2684
+ // log(`${resizeLogPrefix} DOM table found but no cells detected`);
2685
+ }
2686
+ } else {
2687
+ // log(`${resizeLogPrefix} DOM table found but tblId mismatch or missing row: domTblId=${domTblId}, domRow=${domRow}`);
2688
+ }
2689
+ } else {
2690
+ // log(`${resizeLogPrefix} No table found in DOM for line ${lineNum}`);
2691
+ }
2692
+ } else {
2693
+ // log(`${resizeLogPrefix} Could not get line entry or lineNode for line ${lineNum}`);
2694
+ }
2695
+ } else {
2696
+ // log(`${resizeLogPrefix} Could not get rep or rep.lines for DOM reconstruction`);
2697
+ }
2698
+ }
2699
+ } catch (e) {
2700
+ console.error(`${resizeLogPrefix} Error getting table metadata:`, e);
2701
+ }
2702
+ } else {
2703
+ // log(`${resizeLogPrefix} Invalid line number (${lineNum}) or table ID (${tblId})`);
2704
+ }
2705
+ } else {
2706
+ // log(`${resizeLogPrefix} Invalid resize target:`, { table: !!table, lineNode: !!lineNode, columnIndex });
2707
+ }
2708
+ });
2709
+
2710
+ // Enhanced mousemove and mouseup handlers - attach to multiple contexts for better coverage
2711
+ const setupGlobalHandlers = () => {
2712
+ const mouseupLogPrefix = '[ep_data_tables:resizeMouseup]';
2713
+ const mousemoveLogPrefix = '[ep_data_tables:resizeMousemove]';
2714
+
2715
+ // Mousemove handler
2716
+ const handleMousemove = (evt) => {
2717
+ if (isResizing) {
2718
+ evt.preventDefault();
2719
+ updateColumnResize(evt.clientX);
2720
+ }
2721
+ };
2722
+
2723
+ // Mouseup handler with enhanced debugging
2724
+ const handleMouseup = (evt) => {
2725
+ // log(`${mouseupLogPrefix} Mouseup detected on ${evt.target.tagName || 'unknown'}. isResizing: ${isResizing}`);
2726
+
2727
+ if (isResizing) {
2728
+ // log(`${mouseupLogPrefix} Processing resize completion...`);
2729
+ evt.preventDefault();
2730
+ evt.stopPropagation();
2731
+
2732
+ // Add a small delay to ensure all DOM updates are complete
2733
+ setTimeout(() => {
2734
+ // log(`${mouseupLogPrefix} Executing finishColumnResize after delay...`);
2735
+ finishColumnResize(ed, docManager);
2736
+ // log(`${mouseupLogPrefix} Resize completion finished.`);
2737
+ }, 50);
2738
+ } else {
2739
+ // log(`${mouseupLogPrefix} Not in resize mode, ignoring mouseup.`);
2740
+ }
2741
+ };
2742
+
2743
+ // Attach to multiple contexts to ensure we catch the event
2744
+ // log(`${callWithAceLogPrefix} Attaching global mousemove/mouseup handlers to multiple contexts...`);
2745
+
2746
+ // 1. Main document (outside iframes)
2747
+ $(document).on('mousemove', handleMousemove);
2748
+ $(document).on('mouseup', handleMouseup);
2749
+ // log(`${callWithAceLogPrefix} Attached to main document`);
2750
+
2751
+ // 2. Outer iframe document
2752
+ if (outerDoc.length > 0) {
2753
+ outerDoc.on('mousemove', handleMousemove);
2754
+ outerDoc.on('mouseup', handleMouseup);
2755
+ // log(`${callWithAceLogPrefix} Attached to outer iframe document`);
2756
+ }
2757
+
2758
+ // 3. Inner iframe document
2759
+ if (innerDoc.length > 0) {
2760
+ innerDoc.on('mousemove', handleMousemove);
2761
+ innerDoc.on('mouseup', handleMouseup);
2762
+ // log(`${callWithAceLogPrefix} Attached to inner iframe document`);
2763
+ }
2764
+
2765
+ // 4. Inner iframe body (the editing area)
2766
+ $inner.on('mousemove', handleMousemove);
2767
+ $inner.on('mouseup', handleMouseup);
2768
+ // log(`${callWithAceLogPrefix} Attached to inner iframe body`);
2769
+
2770
+ // 5. Add a failsafe - listen for any mouse events during resize
2771
+ const failsafeMouseup = (evt) => {
2772
+ if (isResizing) {
2773
+ // log(`${mouseupLogPrefix} FAILSAFE: Detected mouse event during resize: ${evt.type}`);
2774
+ if (evt.type === 'mouseup' || evt.type === 'mousedown' || evt.type === 'click') {
2775
+ // log(`${mouseupLogPrefix} FAILSAFE: Triggering resize completion due to ${evt.type}`);
2776
+ setTimeout(() => {
2777
+ if (isResizing) { // Double-check we're still resizing
2778
+ finishColumnResize(ed, docManager);
2779
+ }
2780
+ }, 100);
2781
+ }
2782
+ }
2783
+ };
2784
+
2785
+ // Attach failsafe to main document with capture=true to catch events early
2786
+ document.addEventListener('mouseup', failsafeMouseup, true);
2787
+ document.addEventListener('mousedown', failsafeMouseup, true);
2788
+ document.addEventListener('click', failsafeMouseup, true);
2789
+ // log(`${callWithAceLogPrefix} Attached failsafe event handlers`);
2790
+
2791
+ // *** DRAG PREVENTION FOR TABLE ELEMENTS ***
2792
+ const preventTableDrag = (evt) => {
2793
+ const target = evt.target;
2794
+ // Check if the target is a table element or inside a table
2795
+ if (target.tagName === 'TABLE' && target.classList.contains('dataTable') ||
2796
+ target.tagName === 'TD' && target.closest('table.dataTable') ||
2797
+ target.tagName === 'TR' && target.closest('table.dataTable') ||
2798
+ target.tagName === 'TBODY' && target.closest('table.dataTable')) {
2799
+ // log('[ep_data_tables:dragPrevention] Preventing drag operation on table element:', target.tagName);
2800
+ evt.preventDefault();
2801
+ evt.stopPropagation();
2802
+ return false;
2803
+ }
2804
+ };
2805
+
2806
+ // Add drag event listeners to prevent table dragging
2807
+ $inner.on('dragstart', preventTableDrag);
2808
+ $inner.on('drag', preventTableDrag);
2809
+ $inner.on('dragend', preventTableDrag);
2810
+ // log(`${callWithAceLogPrefix} Attached drag prevention handlers to inner body`);
2811
+ };
2812
+
2813
+ // Setup the global handlers
2814
+ setupGlobalHandlers();
2815
+
2816
+ // log(`${callWithAceLogPrefix} Column resize listeners attached successfully.`);
2817
+
2818
+ }, 'tablePasteAndResizeListeners', true);
2819
+ // log(`${logPrefix} ace_callWithAce for listeners setup completed.`);
2820
+
2821
+ // Helper function to apply the metadata attribute to a line
2822
+ function applyTableLineMetadataAttribute (lineNum, tblId, rowIndex, numCols, rep, editorInfo, attributeString = null, documentAttributeManager = null) {
2823
+ const funcName = 'applyTableLineMetadataAttribute';
2824
+ // log(`${logPrefix}:${funcName}: START - Applying METADATA attribute to line ${lineNum}`, {tblId, rowIndex, numCols});
2825
+
2826
+ let finalMetadata;
2827
+
2828
+ // If attributeString is provided, check if it contains columnWidths
2829
+ if (attributeString) {
2830
+ try {
2831
+ const providedMetadata = JSON.parse(attributeString);
2832
+ if (providedMetadata.columnWidths && Array.isArray(providedMetadata.columnWidths) && providedMetadata.columnWidths.length === numCols) {
2833
+ // Already has valid columnWidths, use as-is
2834
+ finalMetadata = providedMetadata;
2835
+ // log(`${logPrefix}:${funcName}: Using provided metadata with existing columnWidths`);
2836
+ } else {
2837
+ // Has metadata but missing/invalid columnWidths, extract from DOM
2838
+ finalMetadata = providedMetadata;
2839
+ // log(`${logPrefix}:${funcName}: Provided metadata missing columnWidths, attempting DOM extraction`);
2840
+ }
2841
+ } catch (e) {
2842
+ // log(`${logPrefix}:${funcName}: Error parsing provided attributeString, will reconstruct:`, e);
2843
+ finalMetadata = null;
2844
+ }
2845
+ }
2846
+
2847
+ // If we don't have complete metadata or need to extract columnWidths
2848
+ if (!finalMetadata || !finalMetadata.columnWidths) {
2849
+ let columnWidths = null;
2850
+
2851
+ // Try to extract existing column widths from DOM if available
2852
+ try {
2853
+ const lineEntry = rep.lines.atIndex(lineNum);
2854
+ if (lineEntry && lineEntry.lineNode) {
2855
+ const tableInDOM = lineEntry.lineNode.querySelector('table.dataTable[data-tblId]');
2856
+ if (tableInDOM) {
2857
+ const domTblId = tableInDOM.getAttribute('data-tblId');
2858
+ if (domTblId === tblId) {
2859
+ const domCells = tableInDOM.querySelectorAll('td');
2860
+ if (domCells.length === numCols) {
2861
+ // Extract column widths from DOM cells
2862
+ columnWidths = [];
2863
+ domCells.forEach(cell => {
2864
+ const style = cell.getAttribute('style') || '';
2865
+ const widthMatch = style.match(/width:\s*([0-9.]+)%/);
2866
+ if (widthMatch) {
2867
+ columnWidths.push(parseFloat(widthMatch[1]));
2868
+ } else {
2869
+ // Fallback to equal distribution if no width found
2870
+ columnWidths.push(100 / numCols);
2871
+ }
2872
+ });
2873
+ // log(`${logPrefix}:${funcName}: Extracted column widths from DOM: ${columnWidths.map(w => w.toFixed(1) + '%').join(', ')}`);
2874
+ }
2875
+ }
2876
+ }
2877
+ }
2878
+ } catch (e) {
2879
+ // log(`${logPrefix}:${funcName}: Error extracting column widths from DOM:`, e);
2880
+ }
2881
+
2882
+ // Build final metadata
2883
+ finalMetadata = finalMetadata || {
2884
+ tblId: tblId,
2885
+ row: rowIndex,
2886
+ cols: numCols
2887
+ };
2888
+
2889
+ // Add column widths if we found them
2890
+ if (columnWidths && columnWidths.length === numCols) {
2891
+ finalMetadata.columnWidths = columnWidths;
2892
+ }
2893
+ }
2894
+
2895
+ const finalAttributeString = JSON.stringify(finalMetadata);
2896
+ // log(`${logPrefix}:${funcName}: Final metadata attribute string: ${finalAttributeString}`);
2897
+
2898
+ try {
2899
+ // Get current line info
2900
+ const lineEntry = rep.lines.atIndex(lineNum);
2901
+ if (!lineEntry) {
2902
+ // log(`${logPrefix}:${funcName}: ERROR - Could not find line entry for line number ${lineNum}`);
2903
+ return;
2904
+ }
2905
+ const lineLength = Math.max(1, lineEntry.text.length);
2906
+ // log(`${logPrefix}:${funcName}: Line ${lineNum} text length: ${lineLength}`);
2907
+
2908
+ // Simple attribute application - just add the tbljson attribute
2909
+ const attributes = [[ATTR_TABLE_JSON, finalAttributeString]];
2910
+ const start = [lineNum, 0];
2911
+ const end = [lineNum, lineLength];
2912
+
2913
+ // log(`${logPrefix}:${funcName}: Applying tbljson attribute to range [${start}]-[${end}]`);
2914
+ editorInfo.ace_performDocumentApplyAttributesToRange(start, end, attributes);
2915
+ // log(`${logPrefix}:${funcName}: Successfully applied tbljson attribute to line ${lineNum}`);
2916
+
2917
+ } catch(e) {
2918
+ console.error(`[ep_data_tables] ${logPrefix}:${funcName}: Error applying metadata attribute on line ${lineNum}:`, e);
2919
+ }
2920
+ }
2921
+
2922
+ /** Insert a fresh rows×cols blank table at the caret */
2923
+ ed.ace_createTableViaAttributes = (rows = 2, cols = 2) => {
2924
+ const funcName = 'ace_createTableViaAttributes';
2925
+ // log(`${funcName}: START - Refactored Phase 4 (Get Selection Fix)`, { rows, cols });
2926
+ rows = Math.max(1, rows); cols = Math.max(1, cols);
2927
+ // log(`${funcName}: Ensuring minimum 1 row, 1 col.`);
2928
+
2929
+ // --- Phase 1: Prepare Data ---
2930
+ const tblId = rand();
2931
+ // log(`${funcName}: Generated table ID: ${tblId}`);
2932
+ const initialCellContent = ' '; // Start with a single space per cell
2933
+ const lineTxt = Array.from({ length: cols }).fill(initialCellContent).join(DELIMITER);
2934
+ // log(`${funcName}: Constructed initial line text for ${cols} cols: "${lineTxt}"`);
2935
+ const block = Array.from({ length: rows }).fill(lineTxt).join('\n') + '\n';
2936
+ // log(`${funcName}: Constructed block for ${rows} rows:\n${block}`);
2937
+
2938
+ // Get current selection BEFORE making changes using ace_getRep()
2939
+ // log(`${funcName}: Getting current representation and selection...`);
2940
+ const currentRepInitial = ed.ace_getRep();
2941
+ if (!currentRepInitial || !currentRepInitial.selStart || !currentRepInitial.selEnd) {
2942
+ console.error(`[ep_data_tables] ${funcName}: Could not get current representation or selection via ace_getRep(). Aborting.`);
2943
+ // log(`${funcName}: END - Error getting initial rep/selection`);
2944
+ return;
2945
+ }
2946
+ const start = currentRepInitial.selStart;
2947
+ const end = currentRepInitial.selEnd;
2948
+ const initialStartLine = start[0]; // Store the starting line number
2949
+ // log(`${funcName}: Current selection from initial rep:`, { start, end });
2950
+
2951
+ // --- Phase 2: Insert Text Block ---
2952
+ // log(`${funcName}: Phase 2 - Inserting text block...`);
2953
+ ed.ace_performDocumentReplaceRange(start, end, block);
2954
+ // log(`${funcName}: Inserted block of delimited text lines.`);
2955
+ // log(`${funcName}: Requesting text sync (ace_fastIncorp 20)...`);
2956
+ ed.ace_fastIncorp(20); // Sync text insertion
2957
+ // log(`${funcName}: Text sync requested.`);
2958
+
2959
+ // --- Phase 3: Apply Metadata Attributes ---
2960
+ // log(`${funcName}: Phase 3 - Applying metadata attributes to ${rows} inserted lines...`);
2961
+ // Need rep to be updated after text insertion to apply attributes correctly
2962
+ const currentRep = ed.ace_getRep(); // Get potentially updated rep
2963
+ if (!currentRep || !currentRep.lines) {
2964
+ console.error(`[ep_data_tables] ${funcName}: Could not get updated rep after text insertion. Cannot apply attributes reliably.`);
2965
+ // log(`${funcName}: END - Error getting updated rep`);
2966
+ // Maybe attempt to continue without rep? Risky.
2967
+ return;
2968
+ }
2969
+ // log(`${funcName}: Fetched updated rep for attribute application.`);
2970
+
2971
+ for (let r = 0; r < rows; r++) {
2972
+ const lineNumToApply = initialStartLine + r;
2973
+ // log(`${funcName}: -> Processing row ${r} on line ${lineNumToApply}`);
2974
+
2975
+ const lineEntry = currentRep.lines.atIndex(lineNumToApply);
2976
+ if (!lineEntry) {
2977
+ // log(`${funcName}: Could not find line entry for ${lineNumToApply}, skipping attribute application.`);
2978
+ continue;
2979
+ }
2980
+ const lineText = lineEntry.text || '';
2981
+ const cells = lineText.split(DELIMITER);
2982
+ let offset = 0;
2983
+
2984
+ // Apply cell-specific attributes to trigger authorship span splitting
2985
+ for (let c = 0; c < cols; c++) {
2986
+ const cellContent = (c < cells.length) ? cells[c] || '' : '';
2987
+ if (cellContent.length > 0) { // Only apply to non-empty cells
2988
+ const cellStart = [lineNumToApply, offset];
2989
+ const cellEnd = [lineNumToApply, offset + cellContent.length];
2990
+ // log(`${funcName}: Applying ${ATTR_CELL} attribute to Line ${lineNumToApply} Col ${c} Range ${offset}-${offset + cellContent.length}`);
2991
+ ed.ace_performDocumentApplyAttributesToRange(cellStart, cellEnd, [[ATTR_CELL, String(c)]]);
2992
+ }
2993
+ offset += cellContent.length;
2994
+ if (c < cols - 1) {
2995
+ offset += DELIMITER.length;
2996
+ }
2997
+ }
2998
+
2999
+ // Call the module-level helper, passing necessary context (currentRep, ed)
3000
+ // Note: documentAttributeManager not available in this context for new table creation
3001
+ applyTableLineMetadataAttribute(lineNumToApply, tblId, r, cols, currentRep, ed, null, null);
3002
+ }
3003
+ // log(`${funcName}: Finished applying metadata attributes.`);
3004
+ // log(`${funcName}: Requesting attribute sync (ace_fastIncorp 20)...`);
3005
+ ed.ace_fastIncorp(20); // Final sync after attributes
3006
+ // log(`${funcName}: Attribute sync requested.`);
3007
+
3008
+ // --- Phase 4: Set Caret Position ---
3009
+ // log(`${funcName}: Phase 4 - Setting final caret position...`);
3010
+ const finalCaretLine = initialStartLine + rows; // Line number after the last inserted row
3011
+ const finalCaretPos = [finalCaretLine, 0];
3012
+ // log(`${funcName}: Target caret position:`, finalCaretPos);
3013
+ try {
3014
+ ed.ace_performSelectionChange(finalCaretPos, finalCaretPos, false);
3015
+ // log(`${funcName}: Successfully set caret position.`);
3016
+ } catch(e) {
3017
+ console.error(`[ep_data_tables] ${funcName}: Error setting caret position after table creation:`, e);
3018
+ // log(`[ep_data_tables] ${funcName}: Error details:`, { message: e.message, stack: e.stack });
3019
+ }
3020
+
3021
+ // log(`${funcName}: END - Refactored Phase 4`);
3022
+ };
3023
+
3024
+ ed.ace_doDatatableOptions = (action) => {
3025
+ const funcName = 'ace_doDatatableOptions';
3026
+ // log(`${funcName}: START - Processing action: ${action}`);
3027
+
3028
+ // Get the last clicked cell info to determine which table to operate on
3029
+ const editor = ed.ep_data_tables_editor;
3030
+ if (!editor) {
3031
+ console.error(`[ep_data_tables] ${funcName}: Could not get editor reference.`);
3032
+ return;
3033
+ }
3034
+
3035
+ const lastClick = editor.ep_data_tables_last_clicked;
3036
+ if (!lastClick || !lastClick.tblId) {
3037
+ // log(`${funcName}: No table selected. Please click on a table cell first.`);
3038
+ console.warn('[ep_data_tables] No table selected. Please click on a table cell first.');
3039
+ return;
3040
+ }
3041
+
3042
+ // log(`${funcName}: Operating on table ${lastClick.tblId}, clicked line ${lastClick.lineNum}, cell ${lastClick.cellIndex}`);
3043
+
3044
+ try {
3045
+ // Get current representation and document manager
3046
+ const currentRep = ed.ace_getRep();
3047
+ if (!currentRep || !currentRep.lines) {
3048
+ console.error(`[ep_data_tables] ${funcName}: Could not get current representation.`);
3049
+ return;
3050
+ }
3051
+
3052
+ // Use the stored documentAttributeManager reference
3053
+ const docManager = ed.ep_data_tables_docManager;
3054
+ if (!docManager) {
3055
+ console.error(`[ep_data_tables] ${funcName}: Could not get document attribute manager from stored reference.`);
3056
+ return;
3057
+ }
3058
+
3059
+ // log(`${funcName}: Successfully obtained documentAttributeManager from stored reference.`);
3060
+
3061
+ // Find all lines that belong to this table
3062
+ const tableLines = [];
3063
+ const totalLines = currentRep.lines.length();
3064
+
3065
+ for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
3066
+ try {
3067
+ // Use the same robust approach as acePostWriteDomLineHTML to find nested tables
3068
+ let lineAttrString = docManager.getAttributeOnLine(lineIndex, ATTR_TABLE_JSON);
3069
+
3070
+ // If no attribute found directly, check if there's a table in the DOM even though attribute is missing
3071
+ if (!lineAttrString) {
3072
+ const lineEntry = currentRep.lines.atIndex(lineIndex);
3073
+ if (lineEntry && lineEntry.lineNode) {
3074
+ const tableInDOM = lineEntry.lineNode.querySelector('table.dataTable[data-tblId]');
3075
+ if (tableInDOM) {
3076
+ const domTblId = tableInDOM.getAttribute('data-tblId');
3077
+ const domRow = tableInDOM.getAttribute('data-row');
3078
+ if (domTblId && domRow !== null) {
3079
+ const domCells = tableInDOM.querySelectorAll('td');
3080
+ if (domCells.length > 0) {
3081
+ // Reconstruct metadata from DOM
3082
+ const reconstructedMetadata = {
3083
+ tblId: domTblId,
3084
+ row: parseInt(domRow, 10),
3085
+ cols: domCells.length
3086
+ };
3087
+ lineAttrString = JSON.stringify(reconstructedMetadata);
3088
+ // log(`${funcName}: Reconstructed metadata from DOM for line ${lineIndex}: ${lineAttrString}`);
3089
+ }
3090
+ }
3091
+ }
3092
+ }
3093
+ }
3094
+
3095
+ if (lineAttrString) {
3096
+ const lineMetadata = JSON.parse(lineAttrString);
3097
+ if (lineMetadata.tblId === lastClick.tblId) {
3098
+ const lineEntry = currentRep.lines.atIndex(lineIndex);
3099
+ if (lineEntry) {
3100
+ tableLines.push({
3101
+ lineIndex,
3102
+ row: lineMetadata.row,
3103
+ cols: lineMetadata.cols,
3104
+ lineText: lineEntry.text,
3105
+ metadata: lineMetadata
3106
+ });
3107
+ }
3108
+ }
3109
+ }
3110
+ } catch (e) {
3111
+ continue;
3112
+ }
3113
+ }
3114
+
3115
+ if (tableLines.length === 0) {
3116
+ // log(`${funcName}: No table lines found for table ${lastClick.tblId}`);
3117
+ return;
3118
+ }
3119
+
3120
+ // Sort by row number to ensure correct order
3121
+ tableLines.sort((a, b) => a.row - b.row);
3122
+ // log(`${funcName}: Found ${tableLines.length} table lines`);
3123
+
3124
+ // Determine table dimensions and target indices with robust matching
3125
+ const numRows = tableLines.length;
3126
+ const numCols = tableLines[0].cols;
3127
+
3128
+ // More robust way to find the target row - match by both line number AND row metadata
3129
+ let targetRowIndex = -1;
3130
+
3131
+ // First try to match by line number
3132
+ targetRowIndex = tableLines.findIndex(line => line.lineIndex === lastClick.lineNum);
3133
+
3134
+ // If that fails, try to match by finding the row that contains the clicked table
3135
+ if (targetRowIndex === -1) {
3136
+ // log(`${funcName}: Direct line number match failed, searching by DOM structure...`);
3137
+ const clickedLineEntry = currentRep.lines.atIndex(lastClick.lineNum);
3138
+ if (clickedLineEntry && clickedLineEntry.lineNode) {
3139
+ const clickedTable = clickedLineEntry.lineNode.querySelector('table.dataTable[data-tblId="' + lastClick.tblId + '"]');
3140
+ if (clickedTable) {
3141
+ const clickedRowAttr = clickedTable.getAttribute('data-row');
3142
+ if (clickedRowAttr !== null) {
3143
+ const clickedRowNum = parseInt(clickedRowAttr, 10);
3144
+ targetRowIndex = tableLines.findIndex(line => line.row === clickedRowNum);
3145
+ // log(`${funcName}: Found target row by DOM attribute matching: row ${clickedRowNum}, index ${targetRowIndex}`);
3146
+ }
3147
+ }
3148
+ }
3149
+ }
3150
+
3151
+ // If still not found, default to first row but log the issue
3152
+ if (targetRowIndex === -1) {
3153
+ // log(`${funcName}: Warning: Could not find target row, defaulting to row 0`);
3154
+ targetRowIndex = 0;
3155
+ }
3156
+
3157
+ const targetColIndex = lastClick.cellIndex || 0;
3158
+
3159
+ // log(`${funcName}: Table dimensions: ${numRows} rows x ${numCols} cols. Target: row ${targetRowIndex}, col ${targetColIndex}`);
3160
+
3161
+ // Perform table operations with both text and metadata updates
3162
+ let newNumCols = numCols;
3163
+ let success = false;
3164
+
3165
+ switch (action) {
3166
+ case 'addTblRowA': // Insert row above
3167
+ // log(`${funcName}: Inserting row above row ${targetRowIndex}`);
3168
+ success = addTableRowAboveWithText(tableLines, targetRowIndex, numCols, lastClick.tblId, ed, docManager);
3169
+ break;
3170
+
3171
+ case 'addTblRowB': // Insert row below
3172
+ // log(`${funcName}: Inserting row below row ${targetRowIndex}`);
3173
+ success = addTableRowBelowWithText(tableLines, targetRowIndex, numCols, lastClick.tblId, ed, docManager);
3174
+ break;
3175
+
3176
+ case 'addTblColL': // Insert column left
3177
+ // log(`${funcName}: Inserting column left of column ${targetColIndex}`);
3178
+ newNumCols = numCols + 1;
3179
+ success = addTableColumnLeftWithText(tableLines, targetColIndex, ed, docManager);
3180
+ break;
3181
+
3182
+ case 'addTblColR': // Insert column right
3183
+ // log(`${funcName}: Inserting column right of column ${targetColIndex}`);
3184
+ newNumCols = numCols + 1;
3185
+ success = addTableColumnRightWithText(tableLines, targetColIndex, ed, docManager);
3186
+ break;
3187
+
3188
+ case 'delTblRow': // Delete row
3189
+ // Show confirmation prompt for row deletion
3190
+ const rowConfirmMessage = `Are you sure you want to delete Row ${targetRowIndex + 1} and all content within?`;
3191
+ if (!confirm(rowConfirmMessage)) {
3192
+ // log(`${funcName}: Row deletion cancelled by user`);
3193
+ return;
3194
+ }
3195
+ // log(`${funcName}: Deleting row ${targetRowIndex}`);
3196
+ success = deleteTableRowWithText(tableLines, targetRowIndex, ed, docManager);
3197
+ break;
3198
+
3199
+ case 'delTblCol': // Delete column
3200
+ // Show confirmation prompt for column deletion
3201
+ const colConfirmMessage = `Are you sure you want to delete Column ${targetColIndex + 1} and all content within?`;
3202
+ if (!confirm(colConfirmMessage)) {
3203
+ // log(`${funcName}: Column deletion cancelled by user`);
3204
+ return;
3205
+ }
3206
+ // log(`${funcName}: Deleting column ${targetColIndex}`);
3207
+ newNumCols = numCols - 1;
3208
+ success = deleteTableColumnWithText(tableLines, targetColIndex, ed, docManager);
3209
+ break;
3210
+
3211
+ default:
3212
+ // log(`${funcName}: Unknown action: ${action}`);
3213
+ return;
3214
+ }
3215
+
3216
+ if (!success) {
3217
+ console.error(`[ep_data_tables] ${funcName}: Table operation failed for action: ${action}`);
3218
+ return;
3219
+ }
3220
+
3221
+ // log(`${funcName}: Table operation completed successfully with text and metadata synchronization`);
3222
+
3223
+ } catch (error) {
3224
+ console.error(`[ep_data_tables] ${funcName}: Error during table operation:`, error);
3225
+ // log(`${funcName}: Error details:`, { message: error.message, stack: error.stack });
3226
+ }
3227
+ };
3228
+
3229
+ // Helper functions for table operations with text updates
3230
+ function addTableRowAboveWithText(tableLines, targetRowIndex, numCols, tblId, editorInfo, docManager) {
3231
+ try {
3232
+ const targetLine = tableLines[targetRowIndex];
3233
+ const newLineText = Array.from({ length: numCols }).fill(' ').join(DELIMITER);
3234
+ const insertLineIndex = targetLine.lineIndex;
3235
+
3236
+ // Insert new line in text
3237
+ editorInfo.ace_performDocumentReplaceRange([insertLineIndex, 0], [insertLineIndex, 0], newLineText + '\n');
3238
+
3239
+ // Apply cell-specific attributes to the new row for authorship
3240
+ const rep = editorInfo.ace_getRep();
3241
+ const cells = newLineText.split(DELIMITER);
3242
+ let offset = 0;
3243
+ for (let c = 0; c < numCols; c++) {
3244
+ const cellContent = (c < cells.length) ? cells[c] || '' : '';
3245
+ if (cellContent.length > 0) {
3246
+ const cellStart = [insertLineIndex, offset];
3247
+ const cellEnd = [insertLineIndex, offset + cellContent.length];
3248
+ editorInfo.ace_performDocumentApplyAttributesToRange(cellStart, cellEnd, [[ATTR_CELL, String(c)]]);
3249
+ }
3250
+ offset += cellContent.length;
3251
+ if (c < numCols - 1) {
3252
+ offset += DELIMITER.length;
3253
+ }
3254
+ }
3255
+
3256
+ // Preserve column widths from existing metadata or extract from DOM
3257
+ let columnWidths = targetLine.metadata.columnWidths;
3258
+ if (!columnWidths) {
3259
+ // Extract from DOM for block-styled rows
3260
+ try {
3261
+ const rep = editorInfo.ace_getRep();
3262
+ const lineEntry = rep.lines.atIndex(targetLine.lineIndex + 1); // +1 because we inserted a line
3263
+ if (lineEntry && lineEntry.lineNode) {
3264
+ const tableInDOM = lineEntry.lineNode.querySelector(`table.dataTable[data-tblId="${tblId}"]`);
3265
+ if (tableInDOM) {
3266
+ const domCells = tableInDOM.querySelectorAll('td');
3267
+ if (domCells.length === numCols) {
3268
+ columnWidths = [];
3269
+ domCells.forEach(cell => {
3270
+ const style = cell.getAttribute('style') || '';
3271
+ const widthMatch = style.match(/width:\s*([0-9.]+)%/);
3272
+ if (widthMatch) {
3273
+ columnWidths.push(parseFloat(widthMatch[1]));
3274
+ } else {
3275
+ columnWidths.push(100 / numCols);
3276
+ }
3277
+ });
3278
+ // log('[ep_data_tables] addTableRowAbove: Extracted column widths from DOM:', columnWidths);
3279
+ }
3280
+ }
3281
+ }
3282
+ } catch (e) {
3283
+ console.error('[ep_data_tables] addTableRowAbove: Error extracting column widths from DOM:', e);
3284
+ }
3285
+ }
3286
+
3287
+ // Update metadata for all subsequent rows
3288
+ for (let i = targetRowIndex; i < tableLines.length; i++) {
3289
+ const lineToUpdate = tableLines[i].lineIndex + 1; // +1 because we inserted a line
3290
+ const newRowIndex = tableLines[i].metadata.row + 1;
3291
+ const newMetadata = { ...tableLines[i].metadata, row: newRowIndex, columnWidths };
3292
+
3293
+ applyTableLineMetadataAttribute(lineToUpdate, tblId, newRowIndex, numCols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
3294
+ }
3295
+
3296
+ // Apply metadata to the new row
3297
+ const newMetadata = { tblId, row: targetLine.metadata.row, cols: numCols, columnWidths };
3298
+ applyTableLineMetadataAttribute(insertLineIndex, tblId, targetLine.metadata.row, numCols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
3299
+
3300
+ editorInfo.ace_fastIncorp(10);
3301
+ return true;
3302
+ } catch (e) {
3303
+ console.error('[ep_data_tables] Error adding row above with text:', e);
3304
+ return false;
3305
+ }
3306
+ }
3307
+
3308
+ function addTableRowBelowWithText(tableLines, targetRowIndex, numCols, tblId, editorInfo, docManager) {
3309
+ try {
3310
+ const targetLine = tableLines[targetRowIndex];
3311
+ const newLineText = Array.from({ length: numCols }).fill(' ').join(DELIMITER);
3312
+ const insertLineIndex = targetLine.lineIndex + 1;
3313
+
3314
+ // Insert new line in text
3315
+ editorInfo.ace_performDocumentReplaceRange([insertLineIndex, 0], [insertLineIndex, 0], newLineText + '\n');
3316
+
3317
+ // Apply cell-specific attributes to the new row for authorship
3318
+ const rep = editorInfo.ace_getRep();
3319
+ const cells = newLineText.split(DELIMITER);
3320
+ let offset = 0;
3321
+ for (let c = 0; c < numCols; c++) {
3322
+ const cellContent = (c < cells.length) ? cells[c] || '' : '';
3323
+ if (cellContent.length > 0) {
3324
+ const cellStart = [insertLineIndex, offset];
3325
+ const cellEnd = [insertLineIndex, offset + cellContent.length];
3326
+ editorInfo.ace_performDocumentApplyAttributesToRange(cellStart, cellEnd, [[ATTR_CELL, String(c)]]);
3327
+ }
3328
+ offset += cellContent.length;
3329
+ if (c < numCols - 1) {
3330
+ offset += DELIMITER.length;
3331
+ }
3332
+ }
3333
+
3334
+ // Preserve column widths from existing metadata or extract from DOM
3335
+ let columnWidths = targetLine.metadata.columnWidths;
3336
+ if (!columnWidths) {
3337
+ // Extract from DOM for block-styled rows
3338
+ try {
3339
+ const rep = editorInfo.ace_getRep();
3340
+ const lineEntry = rep.lines.atIndex(targetLine.lineIndex);
3341
+ if (lineEntry && lineEntry.lineNode) {
3342
+ const tableInDOM = lineEntry.lineNode.querySelector(`table.dataTable[data-tblId="${tblId}"]`);
3343
+ if (tableInDOM) {
3344
+ const domCells = tableInDOM.querySelectorAll('td');
3345
+ if (domCells.length === numCols) {
3346
+ columnWidths = [];
3347
+ domCells.forEach(cell => {
3348
+ const style = cell.getAttribute('style') || '';
3349
+ const widthMatch = style.match(/width:\s*([0-9.]+)%/);
3350
+ if (widthMatch) {
3351
+ columnWidths.push(parseFloat(widthMatch[1]));
3352
+ } else {
3353
+ columnWidths.push(100 / numCols);
3354
+ }
3355
+ });
3356
+ // log('[ep_data_tables] addTableRowBelow: Extracted column widths from DOM:', columnWidths);
3357
+ }
3358
+ }
3359
+ }
3360
+ } catch (e) {
3361
+ console.error('[ep_data_tables] addTableRowBelow: Error extracting column widths from DOM:', e);
3362
+ }
3363
+ }
3364
+
3365
+ // Update metadata for all subsequent rows
3366
+ for (let i = targetRowIndex + 1; i < tableLines.length; i++) {
3367
+ const lineToUpdate = tableLines[i].lineIndex + 1; // +1 because we inserted a line
3368
+ const newRowIndex = tableLines[i].metadata.row + 1;
3369
+ const newMetadata = { ...tableLines[i].metadata, row: newRowIndex, columnWidths };
3370
+
3371
+ applyTableLineMetadataAttribute(lineToUpdate, tblId, newRowIndex, numCols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
3372
+ }
3373
+
3374
+ // Apply metadata to the new row
3375
+ const newMetadata = { tblId, row: targetLine.metadata.row + 1, cols: numCols, columnWidths };
3376
+ applyTableLineMetadataAttribute(insertLineIndex, tblId, targetLine.metadata.row + 1, numCols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
3377
+
3378
+ editorInfo.ace_fastIncorp(10);
3379
+ return true;
3380
+ } catch (e) {
3381
+ console.error('[ep_data_tables] Error adding row below with text:', e);
3382
+ return false;
3383
+ }
3384
+ }
3385
+
3386
+ function addTableColumnLeftWithText(tableLines, targetColIndex, editorInfo, docManager) {
3387
+ const funcName = 'addTableColumnLeftWithText';
3388
+ try {
3389
+ // Process each line individually like table creation does
3390
+ for (const tableLine of tableLines) {
3391
+ const lineText = tableLine.lineText;
3392
+ const cells = lineText.split(DELIMITER);
3393
+
3394
+ // Calculate the exact insertion position - stop BEFORE the target column's delimiter
3395
+ let insertPos = 0;
3396
+ for (let i = 0; i < targetColIndex; i++) {
3397
+ insertPos += (cells[i]?.length ?? 0) + DELIMITER.length;
3398
+ }
3399
+
3400
+ // Insert blank cell then delimiter (BLANK + separator)
3401
+ const textToInsert = ' ' + DELIMITER;
3402
+ const insertStart = [tableLine.lineIndex, insertPos];
3403
+ const insertEnd = [tableLine.lineIndex, insertPos];
3404
+
3405
+ editorInfo.ace_performDocumentReplaceRange(insertStart, insertEnd, textToInsert);
3406
+
3407
+ // Immediately apply authorship attributes like table creation does
3408
+ const rep = editorInfo.ace_getRep();
3409
+ const lineEntry = rep.lines.atIndex(tableLine.lineIndex);
3410
+ if (lineEntry) {
3411
+ const newLineText = lineEntry.text || '';
3412
+ const newCells = newLineText.split(DELIMITER);
3413
+ let offset = 0;
3414
+
3415
+ // Apply cell-specific attributes to ALL cells (like table creation)
3416
+ for (let c = 0; c < tableLine.cols + 1; c++) { // +1 for the new column
3417
+ const cellContent = (c < newCells.length) ? newCells[c] || '' : '';
3418
+ if (cellContent.length > 0) { // Only apply to non-empty cells
3419
+ const cellStart = [tableLine.lineIndex, offset];
3420
+ const cellEnd = [tableLine.lineIndex, offset + cellContent.length];
3421
+ // log(`[ep_data_tables] ${funcName}: Applying ${ATTR_CELL} attribute to Line ${tableLine.lineIndex} Col ${c} Range ${offset}-${offset + cellContent.length}`);
3422
+ editorInfo.ace_performDocumentApplyAttributesToRange(cellStart, cellEnd, [[ATTR_CELL, String(c)]]);
3423
+ }
3424
+ offset += cellContent.length;
3425
+ if (c < newCells.length - 1) {
3426
+ offset += DELIMITER.length;
3427
+ }
3428
+ }
3429
+ }
3430
+
3431
+ // Reset all column widths to equal distribution when adding a column
3432
+ // This avoids complex width calculations and ensures robust behavior
3433
+ const newColCount = tableLine.cols + 1;
3434
+ const equalWidth = 100 / newColCount;
3435
+ const normalizedWidths = Array(newColCount).fill(equalWidth);
3436
+ // log(`[ep_data_tables] addTableColumnLeft: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
3437
+
3438
+ // Apply updated metadata
3439
+ const newMetadata = { ...tableLine.metadata, cols: tableLine.cols + 1, columnWidths: normalizedWidths };
3440
+ applyTableLineMetadataAttribute(tableLine.lineIndex, tableLine.metadata.tblId, tableLine.metadata.row, tableLine.cols + 1, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
3441
+ }
3442
+
3443
+ // Final sync
3444
+ editorInfo.ace_fastIncorp(10);
3445
+ return true;
3446
+ } catch (e) {
3447
+ console.error('[ep_data_tables] Error adding column left with text:', e);
3448
+ return false;
3449
+ }
3450
+ }
3451
+
3452
+ function addTableColumnRightWithText(tableLines, targetColIndex, editorInfo, docManager) {
3453
+ const funcName = 'addTableColumnRightWithText';
3454
+ try {
3455
+ // Process each line individually like table creation does
3456
+ for (const tableLine of tableLines) {
3457
+ const lineText = tableLine.lineText;
3458
+ const cells = lineText.split(DELIMITER);
3459
+
3460
+ // Calculate the exact insertion position - stop BEFORE the target column's trailing delimiter
3461
+ let insertPos = 0;
3462
+ for (let i = 0; i <= targetColIndex; i++) {
3463
+ insertPos += (cells[i]?.length ?? 0);
3464
+ if (i < targetColIndex) insertPos += DELIMITER.length;
3465
+ }
3466
+
3467
+ // Insert delimiter then blank cell (separator + BLANK)
3468
+ const textToInsert = DELIMITER + ' ';
3469
+ const insertStart = [tableLine.lineIndex, insertPos];
3470
+ const insertEnd = [tableLine.lineIndex, insertPos];
3471
+
3472
+ editorInfo.ace_performDocumentReplaceRange(insertStart, insertEnd, textToInsert);
3473
+
3474
+ // Immediately apply authorship attributes like table creation does
3475
+ const rep = editorInfo.ace_getRep();
3476
+ const lineEntry = rep.lines.atIndex(tableLine.lineIndex);
3477
+ if (lineEntry) {
3478
+ const newLineText = lineEntry.text || '';
3479
+ const newCells = newLineText.split(DELIMITER);
3480
+ let offset = 0;
3481
+
3482
+ // Apply cell-specific attributes to ALL cells (like table creation)
3483
+ for (let c = 0; c < tableLine.cols + 1; c++) { // +1 for the new column
3484
+ const cellContent = (c < newCells.length) ? newCells[c] || '' : '';
3485
+ if (cellContent.length > 0) { // Only apply to non-empty cells
3486
+ const cellStart = [tableLine.lineIndex, offset];
3487
+ const cellEnd = [tableLine.lineIndex, offset + cellContent.length];
3488
+ // log(`[ep_data_tables] ${funcName}: Applying ${ATTR_CELL} attribute to Line ${tableLine.lineIndex} Col ${c} Range ${offset}-${offset + cellContent.length}`);
3489
+ editorInfo.ace_performDocumentApplyAttributesToRange(cellStart, cellEnd, [[ATTR_CELL, String(c)]]);
3490
+ }
3491
+ offset += cellContent.length;
3492
+ if (c < newCells.length - 1) {
3493
+ offset += DELIMITER.length;
3494
+ }
3495
+ }
3496
+ }
3497
+
3498
+ // Reset all column widths to equal distribution when adding a column
3499
+ // This avoids complex width calculations and ensures robust behavior
3500
+ const newColCount = tableLine.cols + 1;
3501
+ const equalWidth = 100 / newColCount;
3502
+ const normalizedWidths = Array(newColCount).fill(equalWidth);
3503
+ // log(`[ep_data_tables] addTableColumnRight: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
3504
+
3505
+ // Apply updated metadata
3506
+ const newMetadata = { ...tableLine.metadata, cols: tableLine.cols + 1, columnWidths: normalizedWidths };
3507
+ applyTableLineMetadataAttribute(tableLine.lineIndex, tableLine.metadata.tblId, tableLine.metadata.row, tableLine.cols + 1, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
3508
+ }
3509
+
3510
+ // Final sync
3511
+ editorInfo.ace_fastIncorp(10);
3512
+ return true;
3513
+ } catch (e) {
3514
+ console.error('[ep_data_tables] Error adding column right with text:', e);
3515
+ return false;
3516
+ }
3517
+ }
3518
+
3519
+ function deleteTableRowWithText(tableLines, targetRowIndex, editorInfo, docManager) {
3520
+ try {
3521
+ const targetLine = tableLines[targetRowIndex];
3522
+
3523
+ // Special handling for deleting the first row (row index 0)
3524
+ // Insert a blank line to prevent the table from getting stuck at line 1
3525
+ if (targetRowIndex === 0) {
3526
+ // log('[ep_data_tables] Deleting first row (row 0) - inserting blank line to prevent table from getting stuck');
3527
+ const insertStart = [targetLine.lineIndex, 0];
3528
+ editorInfo.ace_performDocumentReplaceRange(insertStart, insertStart, '\n');
3529
+
3530
+ // Now delete the table line (which is now at lineIndex + 1)
3531
+ const deleteStart = [targetLine.lineIndex + 1, 0];
3532
+ const deleteEnd = [targetLine.lineIndex + 2, 0];
3533
+ editorInfo.ace_performDocumentReplaceRange(deleteStart, deleteEnd, '');
3534
+ } else {
3535
+ // Delete the entire line normally
3536
+ const deleteStart = [targetLine.lineIndex, 0];
3537
+ const deleteEnd = [targetLine.lineIndex + 1, 0];
3538
+ editorInfo.ace_performDocumentReplaceRange(deleteStart, deleteEnd, '');
3539
+ }
3540
+
3541
+ // Extract column widths from target line before deletion for preserving in remaining rows
3542
+ let columnWidths = targetLine.metadata.columnWidths;
3543
+ if (!columnWidths) {
3544
+ // Extract from DOM for block-styled rows
3545
+ try {
3546
+ const rep = editorInfo.ace_getRep();
3547
+ // Check any remaining table line for column widths
3548
+ for (const tableLine of tableLines) {
3549
+ if (tableLine.lineIndex !== targetLine.lineIndex) {
3550
+ const lineEntry = rep.lines.atIndex(tableLine.lineIndex >= targetLine.lineIndex ? tableLine.lineIndex - 1 : tableLine.lineIndex);
3551
+ if (lineEntry && lineEntry.lineNode) {
3552
+ const tableInDOM = lineEntry.lineNode.querySelector(`table.dataTable[data-tblId="${targetLine.metadata.tblId}"]`);
3553
+ if (tableInDOM) {
3554
+ const domCells = tableInDOM.querySelectorAll('td');
3555
+ if (domCells.length === targetLine.metadata.cols) {
3556
+ columnWidths = [];
3557
+ domCells.forEach(cell => {
3558
+ const style = cell.getAttribute('style') || '';
3559
+ const widthMatch = style.match(/width:\s*([0-9.]+)%/);
3560
+ if (widthMatch) {
3561
+ columnWidths.push(parseFloat(widthMatch[1]));
3562
+ } else {
3563
+ columnWidths.push(100 / targetLine.metadata.cols);
3564
+ }
3565
+ });
3566
+ // log('[ep_data_tables] deleteTableRow: Extracted column widths from DOM:', columnWidths);
3567
+ break;
3568
+ }
3569
+ }
3570
+ }
3571
+ }
3572
+ }
3573
+ } catch (e) {
3574
+ console.error('[ep_data_tables] deleteTableRow: Error extracting column widths from DOM:', e);
3575
+ }
3576
+ }
3577
+
3578
+ // Update metadata for all subsequent rows
3579
+ for (let i = targetRowIndex + 1; i < tableLines.length; i++) {
3580
+ const lineToUpdate = tableLines[i].lineIndex - 1; // -1 because we deleted a line
3581
+ const newRowIndex = tableLines[i].metadata.row - 1;
3582
+ const newMetadata = { ...tableLines[i].metadata, row: newRowIndex, columnWidths };
3583
+
3584
+ applyTableLineMetadataAttribute(lineToUpdate, tableLines[i].metadata.tblId, newRowIndex, tableLines[i].cols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
3585
+ }
3586
+
3587
+ editorInfo.ace_fastIncorp(10);
3588
+ return true;
3589
+ } catch (e) {
3590
+ console.error('[ep_data_tables] Error deleting row with text:', e);
3591
+ return false;
3592
+ }
3593
+ }
3594
+
3595
+ function deleteTableColumnWithText(tableLines, targetColIndex, editorInfo, docManager) {
3596
+ try {
3597
+ // Update text content for all table lines using precise character deletion
3598
+ for (const tableLine of tableLines) {
3599
+ const lineText = tableLine.lineText;
3600
+ const cells = lineText.split(DELIMITER);
3601
+
3602
+ if (targetColIndex >= cells.length) {
3603
+ // log(`[ep_data_tables] Warning: Target column ${targetColIndex} doesn't exist in line with ${cells.length} columns`);
3604
+ continue;
3605
+ }
3606
+
3607
+ // Calculate the exact character range to delete
3608
+ let deleteStart = 0;
3609
+ let deleteEnd = 0;
3610
+
3611
+ // Calculate start position
3612
+ for (let i = 0; i < targetColIndex; i++) {
3613
+ deleteStart += (cells[i]?.length ?? 0) + DELIMITER.length;
3614
+ }
3615
+
3616
+ // Calculate end position
3617
+ deleteEnd = deleteStart + (cells[targetColIndex]?.length ?? 0);
3618
+
3619
+ // Include the delimiter in deletion
3620
+ if (targetColIndex === 0 && cells.length > 1) {
3621
+ // If deleting first column, include the delimiter after it
3622
+ deleteEnd += DELIMITER.length;
3623
+ } else if (targetColIndex > 0) {
3624
+ // If deleting any other column, include the delimiter before it
3625
+ deleteStart -= DELIMITER.length;
3626
+ }
3627
+
3628
+ // log(`[ep_data_tables] Deleting column ${targetColIndex} from line ${tableLine.lineIndex}: chars ${deleteStart}-${deleteEnd} from "${lineText}"`);
3629
+
3630
+ // Perform the precise deletion
3631
+ const rangeStart = [tableLine.lineIndex, deleteStart];
3632
+ const rangeEnd = [tableLine.lineIndex, deleteEnd];
3633
+
3634
+ editorInfo.ace_performDocumentReplaceRange(rangeStart, rangeEnd, '');
3635
+
3636
+ // Reset all column widths to equal distribution when deleting a column
3637
+ // This avoids complex width calculations and ensures robust behavior
3638
+ const newColCount = tableLine.cols - 1;
3639
+ if (newColCount > 0) {
3640
+ const equalWidth = 100 / newColCount;
3641
+ const normalizedWidths = Array(newColCount).fill(equalWidth);
3642
+ // log(`[ep_data_tables] deleteTableColumn: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
3643
+
3644
+ // Update metadata
3645
+ const newMetadata = { ...tableLine.metadata, cols: newColCount, columnWidths: normalizedWidths };
3646
+ applyTableLineMetadataAttribute(tableLine.lineIndex, tableLine.metadata.tblId, tableLine.metadata.row, newColCount, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
3647
+ }
3648
+ }
3649
+
3650
+ editorInfo.ace_fastIncorp(10);
3651
+ return true;
3652
+ } catch (e) {
3653
+ console.error('[ep_data_tables] Error deleting column with text:', e);
3654
+ return false;
3655
+ }
3656
+ }
3657
+
3658
+ // ... existing code ...
3659
+
3660
+ // log('aceInitialized: END - helpers defined.');
3661
+ };
3662
+
3663
+ // ───────────────────── required no‑op stubs ─────────────────────
3664
+ exports.aceStartLineAndCharForPoint = () => { return undefined; };
3665
+ exports.aceEndLineAndCharForPoint = () => { return undefined; };
3666
+
3667
+ // NEW: Style protection for table cells
3668
+ exports.aceSetAuthorStyle = (hook, ctx) => {
3669
+ const logPrefix = '[ep_data_tables:aceSetAuthorStyle]';
3670
+ // log(`${logPrefix} START`, { hook, ctx });
3671
+
3672
+ // If no selection or no style to apply, allow default
3673
+ if (!ctx || !ctx.rep || !ctx.rep.selStart || !ctx.rep.selEnd || !ctx.key) {
3674
+ // log(`${logPrefix} No selection or style key. Allowing default.`);
3675
+ return;
3676
+ }
3677
+
3678
+ // Check if selection is within a table
3679
+ const startLine = ctx.rep.selStart[0];
3680
+ const endLine = ctx.rep.selEnd[0];
3681
+
3682
+ // If selection spans multiple lines, prevent style application
3683
+ if (startLine !== endLine) {
3684
+ // log(`${logPrefix} Selection spans multiple lines. Preventing style application to protect table structure.`);
3685
+ return false;
3686
+ }
3687
+
3688
+ // Check if the line is a table line
3689
+ const lineAttrString = ctx.documentAttributeManager?.getAttributeOnLine(startLine, ATTR_TABLE_JSON);
3690
+ if (!lineAttrString) {
3691
+ // log(`${logPrefix} Line ${startLine} is not a table line. Allowing default style application.`);
3692
+ return;
3693
+ }
3694
+
3695
+ // List of styles that could break table structure
3696
+ const BLOCKED_STYLES = [
3697
+ 'list', 'listType', 'indent', 'align', 'heading', 'code', 'quote',
3698
+ 'horizontalrule', 'pagebreak', 'linebreak', 'clear'
3699
+ ];
3700
+
3701
+ if (BLOCKED_STYLES.includes(ctx.key)) {
3702
+ // log(`${logPrefix} Blocked potentially harmful style '${ctx.key}' from being applied to table cell.`);
3703
+ return false;
3704
+ }
3705
+
3706
+ // For allowed styles, ensure they only apply within cell boundaries
3707
+ try {
3708
+ const tableMetadata = JSON.parse(lineAttrString);
3709
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
3710
+ // log(`${logPrefix} Invalid table metadata. Preventing style application.`);
3711
+ return false;
3712
+ }
3713
+
3714
+ const lineText = ctx.rep.lines.atIndex(startLine)?.text || '';
3715
+ const cells = lineText.split(DELIMITER);
3716
+ let currentOffset = 0;
3717
+ let selectionStartCell = -1;
3718
+ let selectionEndCell = -1;
3719
+
3720
+ // Find which cells the selection spans
3721
+ for (let i = 0; i < cells.length; i++) {
3722
+ const cellLength = cells[i]?.length ?? 0;
3723
+ const cellEndCol = currentOffset + cellLength;
3724
+
3725
+ if (ctx.rep.selStart[1] >= currentOffset && ctx.rep.selStart[1] <= cellEndCol) {
3726
+ selectionStartCell = i;
3727
+ }
3728
+ if (ctx.rep.selEnd[1] >= currentOffset && ctx.rep.selEnd[1] <= cellEndCol) {
3729
+ selectionEndCell = i;
3730
+ }
3731
+ currentOffset += cellLength + DELIMITER.length;
3732
+ }
3733
+
3734
+ // If selection spans multiple cells, prevent style application
3735
+ if (selectionStartCell !== selectionEndCell) {
3736
+ // log(`${logPrefix} Selection spans multiple cells. Preventing style application to protect table structure.`);
3737
+ return false;
3738
+ }
3739
+
3740
+ // If selection includes cell delimiters, prevent style application
3741
+ const cellStartCol = cells.slice(0, selectionStartCell).reduce((acc, cell) => acc + cell.length + DELIMITER.length, 0);
3742
+ const cellEndCol = cellStartCol + cells[selectionStartCell].length;
3743
+
3744
+ if (ctx.rep.selStart[1] <= cellStartCol || ctx.rep.selEnd[1] >= cellEndCol) {
3745
+ // log(`${logPrefix} Selection includes cell delimiters. Preventing style application to protect table structure.`);
3746
+ return false;
3747
+ }
3748
+
3749
+ // log(`${logPrefix} Style '${ctx.key}' allowed within cell boundaries.`);
3750
+ return; // Allow the style to be applied
3751
+ } catch (e) {
3752
+ console.error(`${logPrefix} Error processing style application:`, e);
3753
+ // log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
3754
+ return false; // Prevent style application on error
3755
+ }
3756
+ };
3757
+
3758
+ exports.aceEditorCSS = () => {
3759
+ // Path relative to Etherpad's static/plugins/ directory
3760
+ // Format should be: pluginName/path/to/file.css
3761
+ return ['ep_data_tables/static/css/datatables-editor.css', 'ep_data_tables/static/css/caret.css'];
3762
+ };
3763
+
3764
+ // Register TABLE as a block element, hoping it influences rendering behavior
3765
+ exports.aceRegisterBlockElements = () => ['table'];
3766
+
3767
+ // NEW: Column resize helper functions (adapted from images plugin)
3768
+ const startColumnResize = (table, columnIndex, startX, metadata, lineNum) => {
3769
+ const funcName = 'startColumnResize';
3770
+ // log(`${funcName}: Starting resize for column ${columnIndex}`);
3771
+
3772
+ isResizing = true;
3773
+ resizeStartX = startX;
3774
+ resizeCurrentX = startX; // Initialize current position
3775
+ resizeTargetTable = table;
3776
+ resizeTargetColumn = columnIndex;
3777
+ resizeTableMetadata = metadata;
3778
+ resizeLineNum = lineNum;
3779
+
3780
+ // Get current column widths
3781
+ const numCols = metadata.cols;
3782
+ resizeOriginalWidths = metadata.columnWidths ? [...metadata.columnWidths] : Array(numCols).fill(100 / numCols);
3783
+
3784
+ // log(`${funcName}: Original widths:`, resizeOriginalWidths);
3785
+
3786
+ // Create visual overlay instead of modifying table directly
3787
+ createResizeOverlay(table, columnIndex);
3788
+
3789
+ // Prevent text selection during resize
3790
+ document.body.style.userSelect = 'none';
3791
+ document.body.style.webkitUserSelect = 'none';
3792
+ document.body.style.mozUserSelect = 'none';
3793
+ document.body.style.msUserSelect = 'none';
3794
+ };
3795
+
3796
+ const createResizeOverlay = (table, columnIndex) => {
3797
+ // Create a visual overlay that shows resize feedback using the same positioning logic as image plugin
3798
+ if (resizeOverlay) {
3799
+ resizeOverlay.remove();
3800
+ }
3801
+
3802
+ // Get all the necessary container references like image plugin
3803
+ const $innerIframe = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]');
3804
+ if ($innerIframe.length === 0) {
3805
+ console.error('[ep_data_tables] createResizeOverlay: Could not find inner iframe');
3806
+ return;
3807
+ }
3808
+
3809
+ const innerDocBody = $innerIframe.contents().find('body')[0];
3810
+ const padOuter = $('iframe[name="ace_outer"]').contents().find('body');
3811
+
3812
+ if (!innerDocBody || padOuter.length === 0) {
3813
+ console.error('[ep_data_tables] createResizeOverlay: Could not find required container elements');
3814
+ return;
3815
+ }
3816
+
3817
+ // Find all tables that belong to the same table (same tblId)
3818
+ const tblId = table.getAttribute('data-tblId');
3819
+ if (!tblId) {
3820
+ console.error('[ep_data_tables] createResizeOverlay: No tblId found on table');
3821
+ return;
3822
+ }
3823
+
3824
+ const allTableRows = innerDocBody.querySelectorAll(`table.dataTable[data-tblId="${tblId}"]`);
3825
+ if (allTableRows.length === 0) {
3826
+ console.error('[ep_data_tables] createResizeOverlay: No table rows found for tblId:', tblId);
3827
+ return;
3828
+ }
3829
+
3830
+ // Calculate the bounding box that encompasses all table rows
3831
+ let minTop = Infinity;
3832
+ let maxBottom = -Infinity;
3833
+ let tableLeft = 0;
3834
+ let tableWidth = 0;
3835
+
3836
+ Array.from(allTableRows).forEach((tableRow, index) => {
3837
+ const rect = tableRow.getBoundingClientRect();
3838
+ minTop = Math.min(minTop, rect.top);
3839
+ maxBottom = Math.max(maxBottom, rect.bottom);
3840
+
3841
+ // Use the first table row for horizontal positioning
3842
+ if (index === 0) {
3843
+ tableLeft = rect.left;
3844
+ tableWidth = rect.width;
3845
+ }
3846
+ });
3847
+
3848
+ const totalTableHeight = maxBottom - minTop;
3849
+
3850
+ // log(`createResizeOverlay: Found ${allTableRows.length} table rows, total height: ${totalTableHeight}px`);
3851
+
3852
+ // Calculate positioning using the same method as image plugin
3853
+ let innerBodyRect, innerIframeRect, outerBodyRect;
3854
+ let scrollTopInner, scrollLeftInner, scrollTopOuter, scrollLeftOuter;
3855
+
3856
+ try {
3857
+ innerBodyRect = innerDocBody.getBoundingClientRect();
3858
+ innerIframeRect = $innerIframe[0].getBoundingClientRect();
3859
+ outerBodyRect = padOuter[0].getBoundingClientRect();
3860
+ scrollTopInner = innerDocBody.scrollTop;
3861
+ scrollLeftInner = innerDocBody.scrollLeft;
3862
+ scrollTopOuter = padOuter.scrollTop();
3863
+ scrollLeftOuter = padOuter.scrollLeft();
3864
+ } catch (e) {
3865
+ console.error('[ep_data_tables] createResizeOverlay: Error getting container rects/scrolls:', e);
3866
+ return;
3867
+ }
3868
+
3869
+ // Get table position relative to inner body using the full table bounds
3870
+ const tableTopRelInner = minTop - innerBodyRect.top + scrollTopInner;
3871
+ const tableLeftRelInner = tableLeft - innerBodyRect.left + scrollLeftInner;
3872
+
3873
+ // Calculate position in outer body coordinates (like image plugin)
3874
+ const innerFrameTopRelOuter = innerIframeRect.top - outerBodyRect.top + scrollTopOuter;
3875
+ const innerFrameLeftRelOuter = innerIframeRect.left - outerBodyRect.left + scrollLeftOuter;
3876
+
3877
+ const overlayTopOuter = innerFrameTopRelOuter + tableTopRelInner;
3878
+ const overlayLeftOuter = innerFrameLeftRelOuter + tableLeftRelInner;
3879
+
3880
+ // Apply padding and manual offsets like image plugin
3881
+ const outerPadding = window.getComputedStyle(padOuter[0]);
3882
+ const outerPaddingTop = parseFloat(outerPadding.paddingTop) || 0;
3883
+ const outerPaddingLeft = parseFloat(outerPadding.paddingLeft) || 0;
3884
+
3885
+ // Use the same manual offsets as image plugin
3886
+ const MANUAL_OFFSET_TOP = 6;
3887
+ const MANUAL_OFFSET_LEFT = 39;
3888
+
3889
+ const finalOverlayTop = overlayTopOuter + outerPaddingTop + MANUAL_OFFSET_TOP;
3890
+ const finalOverlayLeft = overlayLeftOuter + outerPaddingLeft + MANUAL_OFFSET_LEFT;
3891
+
3892
+ // Calculate the position for the blue line at the right edge of the target column
3893
+ const tds = table.querySelectorAll('td');
3894
+ const tds_array = Array.from(tds);
3895
+ let linePosition = 0;
3896
+
3897
+ if (columnIndex < tds_array.length) {
3898
+ const currentTd = tds_array[columnIndex];
3899
+ const currentTdRect = currentTd.getBoundingClientRect();
3900
+ const currentRelativeLeft = currentTdRect.left - tableLeft; // Use tableLeft instead of tableRect.left
3901
+ const currentWidth = currentTdRect.width;
3902
+ linePosition = currentRelativeLeft + currentWidth;
3903
+ }
3904
+
3905
+ // Create overlay container (invisible background) that spans the entire table height
3906
+ resizeOverlay = document.createElement('div');
3907
+ resizeOverlay.className = 'ep-data_tables-resize-overlay';
3908
+ resizeOverlay.style.cssText = `
3909
+ position: absolute;
3910
+ left: ${finalOverlayLeft}px;
3911
+ top: ${finalOverlayTop}px;
3912
+ width: ${tableWidth}px;
3913
+ height: ${totalTableHeight}px;
3914
+ pointer-events: none;
3915
+ z-index: 1000;
3916
+ background: transparent;
3917
+ box-sizing: border-box;
3918
+ `;
3919
+
3920
+ // Create the blue vertical line (Google Docs style) spanning the full table height
3921
+ const resizeLine = document.createElement('div');
3922
+ resizeLine.className = 'resize-line';
3923
+ resizeLine.style.cssText = `
3924
+ position: absolute;
3925
+ left: ${linePosition}px;
3926
+ top: 0;
3927
+ width: 2px;
3928
+ height: 100%;
3929
+ background: #1a73e8;
3930
+ z-index: 1001;
3931
+ `;
3932
+ resizeOverlay.appendChild(resizeLine);
3933
+
3934
+ // Append to outer body like image plugin does with its outline
3935
+ padOuter.append(resizeOverlay);
3936
+
3937
+ // log('createResizeOverlay: Created Google Docs style blue line overlay spanning entire table height');
3938
+ };
3939
+
3940
+ const updateColumnResize = (currentX) => {
3941
+ if (!isResizing || !resizeTargetTable || !resizeOverlay) return;
3942
+
3943
+ resizeCurrentX = currentX; // Store current position for finishColumnResize
3944
+ const deltaX = currentX - resizeStartX;
3945
+
3946
+ // Get the table width from the first row for percentage calculation
3947
+ const tblId = resizeTargetTable.getAttribute('data-tblId');
3948
+ if (!tblId) return;
3949
+
3950
+ // Find the first table row to get consistent width measurements
3951
+ const $innerIframe = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]');
3952
+ const innerDocBody = $innerIframe.contents().find('body')[0];
3953
+ const firstTableRow = innerDocBody.querySelector(`table.dataTable[data-tblId="${tblId}"]`);
3954
+
3955
+ if (!firstTableRow) return;
3956
+
3957
+ const tableRect = firstTableRow.getBoundingClientRect();
3958
+ const deltaPercent = (deltaX / tableRect.width) * 100;
3959
+
3960
+ // Calculate new widths for final application
3961
+ const newWidths = [...resizeOriginalWidths];
3962
+ const currentColumn = resizeTargetColumn;
3963
+ const nextColumn = currentColumn + 1;
3964
+
3965
+ if (nextColumn < newWidths.length) {
3966
+ const transfer = Math.min(deltaPercent, newWidths[nextColumn] - 5);
3967
+ const actualTransfer = Math.max(transfer, -(newWidths[currentColumn] - 5));
3968
+
3969
+ newWidths[currentColumn] += actualTransfer;
3970
+ newWidths[nextColumn] -= actualTransfer;
3971
+
3972
+ // Update the blue line position to show the new column boundary
3973
+ const resizeLine = resizeOverlay.querySelector('.resize-line');
3974
+ if (resizeLine) {
3975
+ // Calculate new position based on the updated column width
3976
+ const newColumnWidth = (newWidths[currentColumn] / 100) * tableRect.width;
3977
+
3978
+ // Find the original left position relative to the first table row
3979
+ const tds = firstTableRow.querySelectorAll('td');
3980
+ const tds_array = Array.from(tds);
3981
+
3982
+ if (currentColumn < tds_array.length) {
3983
+ const currentTd = tds_array[currentColumn];
3984
+ const currentTdRect = currentTd.getBoundingClientRect();
3985
+ const currentRelativeLeft = currentTdRect.left - tableRect.left;
3986
+
3987
+ // New line position is the original left position plus the new width
3988
+ const newLinePosition = currentRelativeLeft + newColumnWidth;
3989
+ resizeLine.style.left = newLinePosition + 'px';
3990
+ }
3991
+ }
3992
+ }
3993
+ };
3994
+
3995
+ const finishColumnResize = (editorInfo, docManager) => {
3996
+ if (!isResizing || !resizeTargetTable) {
3997
+ // log('finishColumnResize: Not in resize mode');
3998
+ return;
3999
+ }
4000
+
4001
+ const funcName = 'finishColumnResize';
4002
+ // log(`${funcName}: Finishing resize`);
4003
+
4004
+ // Calculate final widths from actual mouse movement
4005
+ const tableRect = resizeTargetTable.getBoundingClientRect();
4006
+ const deltaX = resizeCurrentX - resizeStartX;
4007
+ const deltaPercent = (deltaX / tableRect.width) * 100;
4008
+
4009
+ // log(`${funcName}: Mouse moved ${deltaX}px (${deltaPercent.toFixed(1)}%)`);
4010
+
4011
+ const finalWidths = [...resizeOriginalWidths];
4012
+ const currentColumn = resizeTargetColumn;
4013
+ const nextColumn = currentColumn + 1;
4014
+
4015
+ if (nextColumn < finalWidths.length) {
4016
+ // Transfer width between columns with minimum constraints
4017
+ const transfer = Math.min(deltaPercent, finalWidths[nextColumn] - 5);
4018
+ const actualTransfer = Math.max(transfer, -(finalWidths[currentColumn] - 5));
4019
+
4020
+ finalWidths[currentColumn] += actualTransfer;
4021
+ finalWidths[nextColumn] -= actualTransfer;
4022
+
4023
+ // log(`${funcName}: Transferred ${actualTransfer.toFixed(1)}% from column ${nextColumn} to column ${currentColumn}`);
4024
+ }
4025
+
4026
+ // Normalize widths
4027
+ const totalWidth = finalWidths.reduce((sum, width) => sum + width, 0);
4028
+ if (totalWidth > 0) {
4029
+ finalWidths.forEach((width, index) => {
4030
+ finalWidths[index] = (width / totalWidth) * 100;
4031
+ });
4032
+ }
4033
+
4034
+ // log(`${funcName}: Final normalized widths:`, finalWidths.map(w => w.toFixed(1) + '%'));
4035
+
4036
+ // Clean up overlay
4037
+ if (resizeOverlay) {
4038
+ resizeOverlay.remove();
4039
+ resizeOverlay = null;
4040
+ }
4041
+
4042
+ // Clean up global styles
4043
+ document.body.style.userSelect = '';
4044
+ document.body.style.webkitUserSelect = '';
4045
+ document.body.style.mozUserSelect = '';
4046
+ document.body.style.msUserSelect = '';
4047
+
4048
+ // Set isResizing to false BEFORE making changes
4049
+ isResizing = false;
4050
+
4051
+ // Apply updated metadata to ALL rows in the table (not just the resized row)
4052
+ editorInfo.ace_callWithAce((ace) => {
4053
+ const callWithAceLogPrefix = `${funcName}[ace_callWithAce]`;
4054
+ // log(`${callWithAceLogPrefix}: Finding and updating all table rows with tblId: ${resizeTableMetadata.tblId}`);
4055
+
4056
+ try {
4057
+ const rep = ace.ace_getRep();
4058
+ if (!rep || !rep.lines) {
4059
+ console.error(`${callWithAceLogPrefix}: Invalid rep`);
4060
+ return;
4061
+ }
4062
+
4063
+ // Find all lines that belong to this table
4064
+ const tableLines = [];
4065
+ const totalLines = rep.lines.length();
4066
+
4067
+ for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
4068
+ try {
4069
+ // Get line metadata to check if it belongs to our table
4070
+ let lineAttrString = docManager.getAttributeOnLine(lineIndex, ATTR_TABLE_JSON);
4071
+
4072
+ if (lineAttrString) {
4073
+ const lineMetadata = JSON.parse(lineAttrString);
4074
+ if (lineMetadata.tblId === resizeTableMetadata.tblId) {
4075
+ tableLines.push({
4076
+ lineIndex,
4077
+ metadata: lineMetadata
4078
+ });
4079
+ }
4080
+ } else {
4081
+ // Fallback: Check if there's a table in the DOM even though attribute is missing (block styles)
4082
+ const lineEntry = rep.lines.atIndex(lineIndex);
4083
+ if (lineEntry && lineEntry.lineNode) {
4084
+ const tableInDOM = lineEntry.lineNode.querySelector('table.dataTable[data-tblId]');
4085
+ if (tableInDOM) {
4086
+ const domTblId = tableInDOM.getAttribute('data-tblId');
4087
+ const domRow = tableInDOM.getAttribute('data-row');
4088
+ if (domTblId === resizeTableMetadata.tblId && domRow !== null) {
4089
+ const domCells = tableInDOM.querySelectorAll('td');
4090
+ if (domCells.length > 0) {
4091
+ // Extract column widths from DOM cells
4092
+ const columnWidths = [];
4093
+ domCells.forEach(cell => {
4094
+ const style = cell.getAttribute('style') || '';
4095
+ const widthMatch = style.match(/width:\s*([0-9.]+)%/);
4096
+ if (widthMatch) {
4097
+ columnWidths.push(parseFloat(widthMatch[1]));
4098
+ } else {
4099
+ // Fallback to equal distribution if no width found
4100
+ columnWidths.push(100 / domCells.length);
4101
+ }
4102
+ });
4103
+
4104
+ // Reconstruct metadata from DOM with preserved column widths
4105
+ const reconstructedMetadata = {
4106
+ tblId: domTblId,
4107
+ row: parseInt(domRow, 10),
4108
+ cols: domCells.length,
4109
+ columnWidths: columnWidths
4110
+ };
4111
+ // log(`${callWithAceLogPrefix}: Reconstructed metadata from DOM for line ${lineIndex}:`, reconstructedMetadata);
4112
+ tableLines.push({
4113
+ lineIndex,
4114
+ metadata: reconstructedMetadata
4115
+ });
4116
+ }
4117
+ }
4118
+ }
4119
+ }
4120
+ }
4121
+ } catch (e) {
4122
+ continue; // Skip lines with invalid metadata
4123
+ }
4124
+ }
4125
+
4126
+ // log(`${callWithAceLogPrefix}: Found ${tableLines.length} table lines to update`);
4127
+
4128
+ // Update all table lines with new column widths
4129
+ for (const tableLine of tableLines) {
4130
+ const updatedMetadata = { ...tableLine.metadata, columnWidths: finalWidths };
4131
+ const updatedMetadataString = JSON.stringify(updatedMetadata);
4132
+
4133
+ // Get the full line range for this table line
4134
+ const lineEntry = rep.lines.atIndex(tableLine.lineIndex);
4135
+ if (!lineEntry) {
4136
+ console.error(`${callWithAceLogPrefix}: Could not get line entry for line ${tableLine.lineIndex}`);
4137
+ continue;
4138
+ }
4139
+
4140
+ const lineLength = Math.max(1, lineEntry.text.length);
4141
+ const rangeStart = [tableLine.lineIndex, 0];
4142
+ const rangeEnd = [tableLine.lineIndex, lineLength];
4143
+
4144
+ // log(`${callWithAceLogPrefix}: Updating line ${tableLine.lineIndex} (row ${tableLine.metadata.row}) with new column widths`);
4145
+
4146
+ // Apply the updated metadata attribute directly
4147
+ ace.ace_performDocumentApplyAttributesToRange(rangeStart, rangeEnd, [
4148
+ [ATTR_TABLE_JSON, updatedMetadataString]
4149
+ ]);
4150
+ }
4151
+
4152
+ // log(`${callWithAceLogPrefix}: Successfully applied updated column widths to all ${tableLines.length} table rows`);
4153
+
4154
+ } catch (error) {
4155
+ console.error(`${callWithAceLogPrefix}: Error applying updated metadata:`, error);
4156
+ // log(`${callWithAceLogPrefix}: Error details:`, { message: error.message, stack: error.stack });
4157
+ }
4158
+ }, 'applyTableResizeToAllRows', true);
4159
+
4160
+ // log(`${funcName}: Column width update initiated for all table rows via ace_callWithAce`);
4161
+
4162
+ // Reset state
4163
+ resizeStartX = 0;
4164
+ resizeCurrentX = 0;
4165
+ resizeTargetTable = null;
4166
+ resizeTargetColumn = -1;
4167
+ resizeOriginalWidths = [];
4168
+ resizeTableMetadata = null;
4169
+ resizeLineNum = -1;
4170
+
4171
+ // log(`${funcName}: Resize complete - state reset`);
4172
+ };
4173
+
4174
+ // NEW: Undo/Redo protection
4175
+ exports.aceUndoRedo = (hook, ctx) => {
4176
+ const logPrefix = '[ep_data_tables:aceUndoRedo]';
4177
+ // log(`${logPrefix} START`, { hook, ctx });
4178
+
4179
+ if (!ctx || !ctx.rep || !ctx.rep.selStart || !ctx.rep.selEnd) {
4180
+ // log(`${logPrefix} No selection or context. Allowing default.`);
4181
+ return;
4182
+ }
4183
+
4184
+ // Get the affected line range
4185
+ const startLine = ctx.rep.selStart[0];
4186
+ const endLine = ctx.rep.selEnd[0];
4187
+
4188
+ // Check if any affected lines are table lines
4189
+ let hasTableLines = false;
4190
+ let tableLines = [];
4191
+
4192
+ for (let line = startLine; line <= endLine; line++) {
4193
+ const lineAttrString = ctx.documentAttributeManager?.getAttributeOnLine(line, ATTR_TABLE_JSON);
4194
+ if (lineAttrString) {
4195
+ hasTableLines = true;
4196
+ tableLines.push(line);
4197
+ }
4198
+ }
4199
+
4200
+ if (!hasTableLines) {
4201
+ // log(`${logPrefix} No table lines affected. Allowing default undo/redo.`);
4202
+ return;
4203
+ }
4204
+
4205
+ // log(`${logPrefix} Table lines affected:`, { tableLines });
4206
+
4207
+ // Validate table structure after undo/redo
4208
+ try {
4209
+ for (const line of tableLines) {
4210
+ const lineAttrString = ctx.documentAttributeManager?.getAttributeOnLine(line, ATTR_TABLE_JSON);
4211
+ if (!lineAttrString) continue;
4212
+
4213
+ const tableMetadata = JSON.parse(lineAttrString);
4214
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
4215
+ // log(`${logPrefix} Invalid table metadata after undo/redo. Attempting recovery.`);
4216
+ // Attempt to recover table structure
4217
+ const lineText = ctx.rep.lines.atIndex(line)?.text || '';
4218
+ const cells = lineText.split(DELIMITER);
4219
+
4220
+ // If we have valid cells, try to reconstruct the table metadata
4221
+ if (cells.length > 1) {
4222
+ const newMetadata = {
4223
+ cols: cells.length,
4224
+ rows: 1,
4225
+ cells: cells.map((_, i) => ({ col: i, row: 0 }))
4226
+ };
4227
+
4228
+ // Apply the recovered metadata
4229
+ ctx.documentAttributeManager.setAttributeOnLine(line, ATTR_TABLE_JSON, JSON.stringify(newMetadata));
4230
+ // log(`${logPrefix} Recovered table structure for line ${line}`);
4231
+ } else {
4232
+ // If we can't recover, remove the table attribute
4233
+ ctx.documentAttributeManager.removeAttributeOnLine(line, ATTR_TABLE_JSON);
4234
+ // log(`${logPrefix} Removed invalid table attribute from line ${line}`);
4235
+ }
4236
+ }
4237
+ }
4238
+ } catch (e) {
4239
+ console.error(`${logPrefix} Error during undo/redo validation:`, e);
4240
+ // log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
4241
+ }
4242
+ };
4243
+
4244
+ // *** ADDED: postAceInit hook for attaching listeners ***
4245
+ exports.postAceInit = (hookName, ctx) => {
4246
+ const func = '[ep_data_tables:postAceInit]';
4247
+ // log(`${func} START`);
4248
+ const editorInfo = ctx.ace; // Get editorInfo from context
4249
+
4250
+ if (!editorInfo) {
4251
+ console.error(`${func} ERROR: editorInfo (ctx.ace) is not available.`);
4252
+ return;
4253
+ }
4254
+
4255
+ const attachReconnectHandler = () => {
4256
+ try {
4257
+ const padObj = window.pad;
4258
+ const socket = padObj && padObj.socket;
4259
+ if (!socket) return false; // Not ready yet
4260
+
4261
+ if (socket.ep_data_tables_reconnect_listener_attached) return true;
4262
+
4263
+ let triggered = false;
4264
+ const triggerHardReconnect = (evtName) => {
4265
+ if (triggered) return;
4266
+ triggered = true;
4267
+ console.log(`[ep_data_tables] Socket.IO event '${evtName}' – invoking pad.forceReconnect()`);
4268
+ if (window.pad && typeof window.pad.forceReconnect === 'function') {
4269
+ try { window.pad.forceReconnect(); } catch(e) { console.error('[ep_data_tables] pad.forceReconnect() failed', e); window.location.reload(); }
4270
+ } else {
4271
+ window.location.reload();
4272
+ }
4273
+ };
4274
+
4275
+ socket.on('reconnect_attempt', () => triggerHardReconnect('reconnect_attempt'));
4276
+ socket.on('reconnect', () => triggerHardReconnect('reconnect'));
4277
+ socket.on('connect', () => { if (socket.disconnectedPreviously) triggerHardReconnect('connect'); });
4278
+ socket.on('disconnect', () => { socket.disconnectedPreviously = true; });
4279
+
4280
+ socket.ep_data_tables_reconnect_listener_attached = true;
4281
+ console.log('[ep_data_tables] Reconnect handler fully attached to pad.socket');
4282
+ return true;
4283
+ } catch (e) {
4284
+ console.error('[ep_data_tables] Error attaching reconnect listener:', e);
4285
+ return false;
4286
+ }
4287
+ };
4288
+
4289
+ // Keep trying until it attaches (no max attempts)
4290
+ if (!attachReconnectHandler()) {
4291
+ const intervalId = setInterval(() => {
4292
+ if (attachReconnectHandler()) clearInterval(intervalId);
4293
+ }, 500);
4294
+ }
4295
+
4296
+ // Setup mousedown listener via callWithAce
4297
+ editorInfo.ace_callWithAce((ace) => {
4298
+ const editor = ace.editor;
4299
+ const inner = ace.editor.container; // Use the main container
4300
+
4301
+ if (!editor || !inner) {
4302
+ console.error(`${func} ERROR: ace.editor or ace.editor.container not found within ace_callWithAce.`);
4303
+ return;
4304
+ }
4305
+
4306
+ // log(`${func} Inside callWithAce for attaching mousedown listeners.`);
4307
+
4308
+ // Initialize shared state on the editor object
4309
+ if (!editor.ep_data_tables_last_clicked) {
4310
+ editor.ep_data_tables_last_clicked = null;
4311
+ // log(`${func} Initialized ace.editor.ep_data_tables_last_clicked`);
4312
+ }
4313
+
4314
+ // log(`${func} Attempting to attach mousedown listener to editor container for cell selection...`);
4315
+
4316
+ inner.addEventListener('mousedown', (evt) => {
4317
+ const target = evt.target;
4318
+ const mousedownFuncName = '[ep_data_tables mousedown]';
4319
+ // log(`${mousedownFuncName} RAW MOUSE DOWN detected. Target:`, target);
4320
+ // log(`${mousedownFuncName} Target tagName: ${target.tagName}`);
4321
+ // log(`${mousedownFuncName} Target className: ${target.className}`);
4322
+ // log(`${mousedownFuncName} Target ID: ${target.id}`);
4323
+
4324
+ // Don't interfere with resize handle clicks
4325
+ if (target.classList && target.classList.contains('ep-data_tables-resize-handle')) {
4326
+ // log(`${mousedownFuncName} Click on resize handle, skipping cell selection logic.`);
4327
+ return;
4328
+ }
4329
+
4330
+ // *** ENHANCED DEBUG: Check for image-related elements ***
4331
+ const $target = $(target);
4332
+ const isImageElement = $target.closest('.inline-image, .image-placeholder, .image-inner, .image-resize-handle').length > 0;
4333
+ // log(`${mousedownFuncName} Is target or ancestor image-related?`, isImageElement);
4334
+
4335
+ if (isImageElement) {
4336
+ // log(`${mousedownFuncName} *** IMAGE ELEMENT DETECTED ***`);
4337
+ // log(`${mousedownFuncName} Closest image container:`, $target.closest('.inline-image, .image-placeholder')[0]);
4338
+ // log(`${mousedownFuncName} Is .inline-image:`, $target.hasClass('inline-image') || $target.closest('.inline-image').length > 0);
4339
+ // log(`${mousedownFuncName} Is .image-placeholder:`, $target.hasClass('image-placeholder') || $target.closest('.image-placeholder').length > 0);
4340
+ // log(`${mousedownFuncName} Is .image-inner:`, $target.hasClass('image-inner') || $target.closest('.image-inner').length > 0);
4341
+ // log(`${mousedownFuncName} Is .image-resize-handle:`, $target.hasClass('image-resize-handle') || $target.closest('.image-resize-handle').length > 0);
4342
+ }
4343
+
4344
+ // Check if the click is on an image or image-related element - if so, completely skip
4345
+ if (isImageElement) {
4346
+ // log(`${mousedownFuncName} Click detected on image element within table cell. Completely skipping table processing to avoid interference.`);
4347
+ return;
4348
+ }
4349
+
4350
+ // *** ENHANCED DEBUG: Check table context ***
4351
+ const clickedTD = target.closest('td');
4352
+ const clickedTR = target.closest('tr');
4353
+ const clickedTable = target.closest('table.dataTable');
4354
+
4355
+ // log(`${mousedownFuncName} Table context analysis:`);
4356
+ // log(`${mousedownFuncName} - Clicked TD:`, !!clickedTD);
4357
+ // log(`${mousedownFuncName} - Clicked TR:`, !!clickedTR);
4358
+ // log(`${mousedownFuncName} - Clicked table.dataTable:`, !!clickedTable);
4359
+
4360
+ if (clickedTable) {
4361
+ // log(`${mousedownFuncName} - Table tblId:`, clickedTable.getAttribute('data-tblId'));
4362
+ // log(`${mousedownFuncName} - Table row:`, clickedTable.getAttribute('data-row'));
4363
+ }
4364
+ if (clickedTD) {
4365
+ // log(`${mousedownFuncName} - TD data-column:`, clickedTD.getAttribute('data-column'));
4366
+ // log(`${mousedownFuncName} - TD innerHTML length:`, clickedTD.innerHTML?.length || 0);
4367
+ // log(`${mousedownFuncName} - TD contains images:`, clickedTD.querySelector('.inline-image, .image-placeholder') ? 'YES' : 'NO');
4368
+ }
4369
+
4370
+ // Clear previous selection state regardless of where click happened
4371
+ if (editor.ep_data_tables_last_clicked) {
4372
+ // log(`${mousedownFuncName} Clearing previous selection info.`);
4373
+ // TODO: Add visual class removal if needed
4374
+ }
4375
+ editor.ep_data_tables_last_clicked = null; // Clear state first
4376
+
4377
+ if (clickedTD && clickedTR && clickedTable) {
4378
+ // log(`${mousedownFuncName} Click detected inside table.dataTable td.`);
4379
+ try {
4380
+ const cellIndex = Array.from(clickedTR.children).indexOf(clickedTD);
4381
+ const lineNode = clickedTable.closest('div.ace-line');
4382
+ const tblId = clickedTable.getAttribute('data-tblId');
4383
+
4384
+ // log(`${mousedownFuncName} Cell analysis:`);
4385
+ // log(`${mousedownFuncName} - Cell index:`, cellIndex);
4386
+ // log(`${mousedownFuncName} - Line node:`, !!lineNode);
4387
+ // log(`${mousedownFuncName} - Line node ID:`, lineNode?.id);
4388
+ // log(`${mousedownFuncName} - Table ID:`, tblId);
4389
+
4390
+ // Ensure ace.rep and ace.rep.lines are available
4391
+ if (!ace.rep || !ace.rep.lines) {
4392
+ console.error(`${mousedownFuncName} ERROR: ace.rep or ace.rep.lines not available inside mousedown listener.`);
4393
+ return;
4394
+ }
4395
+
4396
+ if (lineNode && lineNode.id && tblId !== null && cellIndex !== -1) {
4397
+ const lineNum = ace.rep.lines.indexOfKey(lineNode.id);
4398
+ if (lineNum !== -1) {
4399
+ // Store the accurately determined cell info
4400
+ // Initialize relative position - might be refined later if needed
4401
+ const clickInfo = { lineNum, tblId, cellIndex, relativePos: 0 }; // Set initial relativePos to 0
4402
+ editor.ep_data_tables_last_clicked = clickInfo;
4403
+ // log(`${mousedownFuncName} Clicked cell (SUCCESS): Line=${lineNum}, TblId=${tblId}, CellIndex=${cellIndex}. Stored click info:`, clickInfo);
4404
+
4405
+ // --- NEW: Jump caret immediately for snappier UX ---
4406
+ try {
4407
+ const docMgr = ace.ep_data_tables_docManager;
4408
+ if (docMgr && typeof navigateToCell === 'function') {
4409
+ const navOk = navigateToCell(lineNum, cellIndex, ace, docMgr);
4410
+ // log(`${mousedownFuncName} Immediate navigateToCell result: ${navOk}`);
4411
+ }
4412
+ } catch (navErr) {
4413
+ console.error(`${mousedownFuncName} Error during immediate caret navigation:`, navErr);
4414
+ }
4415
+
4416
+ // TODO: Add visual class for selection if desired
4417
+ // log(`${mousedownFuncName} TEST: Skipped adding/removing selected-table-cell class`);
4418
+
4419
+ } else {
4420
+ // log(`${mousedownFuncName} Clicked cell (ERROR): Could not find line number for node ID: ${lineNode.id}`);
4421
+ }
4422
+ } else {
4423
+ // log(`${mousedownFuncName} Clicked cell (ERROR): Missing required info (lineNode, lineNode.id, tblId, or valid cellIndex).`, { lineNode, tblId, cellIndex });
4424
+ }
4425
+ } catch (e) {
4426
+ console.error(`${mousedownFuncName} Error processing table cell click:`, e);
4427
+ // log(`${mousedownFuncName} Error details:`, { message: e.message, stack: e.stack });
4428
+ editor.ep_data_tables_last_clicked = null; // Ensure state is clear on error
4429
+ }
4430
+ } else {
4431
+ // log(`${mousedownFuncName} Click was outside a table.dataTable td.`);
4432
+ }
4433
+ });
4434
+ // log(`${func} Mousedown listeners for cell selection attached successfully (inside callWithAce).`);
4435
+
4436
+ }, 'tableCellSelectionPostAce', true); // Unique name for callstack
4437
+
4438
+ // log(`${func} END`);
4439
+ };
4440
+
4441
+ // END OF FILE