ep_data_tables 0.0.8 → 0.0.95

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