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