ep_data_tables 0.0.7 → 0.0.9

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