ep_data_tables 0.0.3 → 0.0.5
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.
- package/package.json +1 -1
- package/static/js/client_hooks.js +421 -5
package/package.json
CHANGED
|
@@ -60,6 +60,20 @@ let resizeOriginalWidths = [];
|
|
|
60
60
|
let resizeTableMetadata = null;
|
|
61
61
|
let resizeLineNum = -1;
|
|
62
62
|
let resizeOverlay = null; // Visual overlay element
|
|
63
|
+
// Android Chrome composition handling state
|
|
64
|
+
let suppressNextBeforeInputInsertTextOnce = false;
|
|
65
|
+
let isAndroidChromeComposition = false;
|
|
66
|
+
let handledCurrentComposition = false;
|
|
67
|
+
// Suppress all beforeinput insertText events during an Android Chrome IME composition
|
|
68
|
+
let suppressBeforeInputInsertTextDuringComposition = false;
|
|
69
|
+
// Helper to detect any Android browser (exclude iOS/Safari)
|
|
70
|
+
function isAndroidUA() {
|
|
71
|
+
const ua = (navigator.userAgent || '').toLowerCase();
|
|
72
|
+
const isAndroid = ua.includes('android');
|
|
73
|
+
const isIOS = ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod') || ua.includes('crios');
|
|
74
|
+
// Safari on Android is rare (WebKit ports), but our exclusion target is iOS Safari; we exclude all iOS above
|
|
75
|
+
return isAndroid && !isIOS;
|
|
76
|
+
}
|
|
63
77
|
|
|
64
78
|
// ─────────────────── Reusable Helper Functions ───────────────────
|
|
65
79
|
|
|
@@ -936,7 +950,7 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
936
950
|
const delimiterCount = (delimitedTextFromLine || '').split(DELIMITER).length - 1;
|
|
937
951
|
// log(`${logPrefix} NodeID#${nodeId}: Delimiter '${DELIMITER}' count in innerHTML: ${delimiterCount}`);
|
|
938
952
|
// log(`${logPrefix} NodeID#${nodeId}: Expected delimiters for ${rowMetadata.cols} columns: ${rowMetadata.cols - 1}`);
|
|
939
|
-
|
|
953
|
+
|
|
940
954
|
// log all delimiter positions
|
|
941
955
|
let pos = -1;
|
|
942
956
|
const delimiterPositions = [];
|
|
@@ -2180,10 +2194,99 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2180
2194
|
const lineNum = selStart[0];
|
|
2181
2195
|
// log(`${deleteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
|
|
2182
2196
|
|
|
2183
|
-
//
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2197
|
+
// Android Chrome IME: collapsed backspace/forward-delete often comes via beforeinput
|
|
2198
|
+
const isAndroidChrome = isAndroidUA();
|
|
2199
|
+
const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
|
|
2200
|
+
|
|
2201
|
+
// Handle collapsed deletes on Android Chrome inside a table line to protect delimiters
|
|
2202
|
+
const isCollapsed = (selStart[0] === selEnd[0] && selStart[1] === selEnd[1]);
|
|
2203
|
+
if (isCollapsed && isAndroidChrome && (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward')) {
|
|
2204
|
+
// Resolve metadata for this line
|
|
2205
|
+
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2206
|
+
let tableMetadata = null;
|
|
2207
|
+
if (lineAttrString) { try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {} }
|
|
2208
|
+
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2209
|
+
if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
|
|
2210
|
+
return; // Not a table line; allow default
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// Compute current cell and boundaries
|
|
2214
|
+
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2215
|
+
const cells = lineText.split(DELIMITER);
|
|
2216
|
+
let currentOffset = 0;
|
|
2217
|
+
let targetCellIndex = -1;
|
|
2218
|
+
let cellStartCol = 0;
|
|
2219
|
+
let cellEndCol = 0;
|
|
2220
|
+
for (let i = 0; i < cells.length; i++) {
|
|
2221
|
+
const cellLength = cells[i]?.length ?? 0;
|
|
2222
|
+
const cellEndColThisIteration = currentOffset + cellLength;
|
|
2223
|
+
if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
|
|
2224
|
+
targetCellIndex = i;
|
|
2225
|
+
cellStartCol = currentOffset;
|
|
2226
|
+
cellEndCol = cellEndColThisIteration;
|
|
2227
|
+
break;
|
|
2228
|
+
}
|
|
2229
|
+
currentOffset += cellLength + DELIMITER.length;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
if (targetCellIndex === -1) return; // Allow default if not within a cell
|
|
2233
|
+
|
|
2234
|
+
const isBackward = inputType === 'deleteContentBackward';
|
|
2235
|
+
const caretCol = selStart[1];
|
|
2236
|
+
|
|
2237
|
+
// Prevent deletion across delimiters or line boundaries
|
|
2238
|
+
if ((isBackward && caretCol === cellStartCol) || (!isBackward && caretCol === cellEndCol)) {
|
|
2239
|
+
evt.preventDefault();
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// Intercept and perform one-character deletion via Ace
|
|
2244
|
+
evt.preventDefault();
|
|
2245
|
+
try {
|
|
2246
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2247
|
+
const delStart = isBackward ? [lineNum, caretCol - 1] : [lineNum, caretCol];
|
|
2248
|
+
const delEnd = isBackward ? [lineNum, caretCol] : [lineNum, caretCol + 1];
|
|
2249
|
+
aceInstance.ace_performDocumentReplaceRange(delStart, delEnd, '');
|
|
2250
|
+
|
|
2251
|
+
// Re-apply table metadata attribute to ensure renderer stability
|
|
2252
|
+
const repAfter = aceInstance.ace_getRep();
|
|
2253
|
+
ed.ep_data_tables_applyMeta(
|
|
2254
|
+
lineNum,
|
|
2255
|
+
tableMetadata.tblId,
|
|
2256
|
+
tableMetadata.row,
|
|
2257
|
+
tableMetadata.cols,
|
|
2258
|
+
repAfter,
|
|
2259
|
+
ed,
|
|
2260
|
+
null,
|
|
2261
|
+
docManager
|
|
2262
|
+
);
|
|
2263
|
+
|
|
2264
|
+
// Set caret position after deletion
|
|
2265
|
+
const newCaretCol = isBackward ? caretCol - 1 : caretCol;
|
|
2266
|
+
const newCaretPos = [lineNum, newCaretCol];
|
|
2267
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2268
|
+
aceInstance.ace_fastIncorp(10);
|
|
2269
|
+
|
|
2270
|
+
// Update last-clicked state if available
|
|
2271
|
+
if (ed.ep_data_tables_editor && ed.ep_data_tables_editor.ep_data_tables_last_clicked && ed.ep_data_tables_editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
2272
|
+
const newRelativePos = newCaretCol - cellStartCol;
|
|
2273
|
+
ed.ep_data_tables_editor.ep_data_tables_last_clicked = {
|
|
2274
|
+
lineNum: lineNum,
|
|
2275
|
+
tblId: tableMetadata.tblId,
|
|
2276
|
+
cellIndex: targetCellIndex,
|
|
2277
|
+
relativePos: newRelativePos < 0 ? 0 : newRelativePos,
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
}, 'tableCollapsedDeleteHandler', true);
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
console.error(`${deleteLogPrefix} ERROR handling collapsed delete:`, error);
|
|
2283
|
+
}
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// Non-Android or non-collapsed: require an actual selection to handle here; otherwise allow default
|
|
2288
|
+
if (isCollapsed) {
|
|
2289
|
+
return; // Allow default
|
|
2187
2290
|
}
|
|
2188
2291
|
|
|
2189
2292
|
// Check if selection spans multiple lines
|
|
@@ -2337,6 +2440,319 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2337
2440
|
}
|
|
2338
2441
|
});
|
|
2339
2442
|
|
|
2443
|
+
// Android Chrome IME insert handling (targeted fix)
|
|
2444
|
+
$inner.on('beforeinput', (evt) => {
|
|
2445
|
+
const insertLogPrefix = '[ep_data_tables:beforeinputInsertHandler]';
|
|
2446
|
+
const inputType = evt.originalEvent && evt.originalEvent.inputType || '';
|
|
2447
|
+
|
|
2448
|
+
// Only intercept insert types
|
|
2449
|
+
if (!inputType || !inputType.startsWith('insert')) return;
|
|
2450
|
+
|
|
2451
|
+
// Target only Android browsers (exclude iOS)
|
|
2452
|
+
if (!isAndroidUA()) return;
|
|
2453
|
+
|
|
2454
|
+
// Get current selection and ensure we are inside a table line
|
|
2455
|
+
const rep = ed.ace_getRep();
|
|
2456
|
+
if (!rep || !rep.selStart) return;
|
|
2457
|
+
const selStart = rep.selStart;
|
|
2458
|
+
const selEnd = rep.selEnd;
|
|
2459
|
+
const lineNum = selStart[0];
|
|
2460
|
+
|
|
2461
|
+
// Resolve table metadata for this line
|
|
2462
|
+
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2463
|
+
let tableMetadata = null;
|
|
2464
|
+
if (lineAttrString) {
|
|
2465
|
+
try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {}
|
|
2466
|
+
}
|
|
2467
|
+
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2468
|
+
if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
|
|
2469
|
+
return; // not a table line
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// Compute cell boundaries and target cell index
|
|
2473
|
+
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2474
|
+
const cells = lineText.split(DELIMITER);
|
|
2475
|
+
let currentOffset = 0;
|
|
2476
|
+
let targetCellIndex = -1;
|
|
2477
|
+
let cellStartCol = 0;
|
|
2478
|
+
let cellEndCol = 0;
|
|
2479
|
+
for (let i = 0; i < cells.length; i++) {
|
|
2480
|
+
const cellLength = cells[i]?.length ?? 0;
|
|
2481
|
+
const cellEndColThisIteration = currentOffset + cellLength;
|
|
2482
|
+
if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
|
|
2483
|
+
targetCellIndex = i;
|
|
2484
|
+
cellStartCol = currentOffset;
|
|
2485
|
+
cellEndCol = cellEndColThisIteration;
|
|
2486
|
+
break;
|
|
2487
|
+
}
|
|
2488
|
+
currentOffset += cellLength + DELIMITER.length;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// Clamp selection end if it includes a trailing delimiter of the same cell
|
|
2492
|
+
if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
|
|
2493
|
+
selEnd[1] = cellEndCol;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// If selection spills outside the cell boundaries, block to protect structure
|
|
2497
|
+
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
|
|
2498
|
+
evt.preventDefault();
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// Handle line breaks by routing to Enter behavior
|
|
2503
|
+
if (inputType === 'insertParagraph' || inputType === 'insertLineBreak') {
|
|
2504
|
+
evt.preventDefault();
|
|
2505
|
+
try {
|
|
2506
|
+
navigateToCellBelow(lineNum, targetCellIndex, tableMetadata, ed, docManager);
|
|
2507
|
+
} catch (e) { console.error(`${insertLogPrefix} Error navigating on line break:`, e); }
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// If we are in Android Chrome composition flow, suppress all insertText until composition ends
|
|
2512
|
+
if (suppressBeforeInputInsertTextDuringComposition && inputType === 'insertText') {
|
|
2513
|
+
evt.preventDefault();
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// If we already handled one insertion via composition handler, skip once (legacy single-shot)
|
|
2518
|
+
if (suppressNextBeforeInputInsertTextOnce && inputType === 'insertText') {
|
|
2519
|
+
suppressNextBeforeInputInsertTextOnce = false;
|
|
2520
|
+
evt.preventDefault();
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// If composition session is active, let composition handler manage
|
|
2525
|
+
if (isAndroidChromeComposition) return;
|
|
2526
|
+
|
|
2527
|
+
// Only proceed for textual insertions we can retrieve
|
|
2528
|
+
const rawData = evt.originalEvent && typeof evt.originalEvent.data === 'string' ? evt.originalEvent.data : '';
|
|
2529
|
+
// If no data for this insert type, allow default (paste/drop paths are handled elsewhere)
|
|
2530
|
+
if (!rawData) return;
|
|
2531
|
+
|
|
2532
|
+
// Sanitize inserted text: remove our delimiter and zero-width characters
|
|
2533
|
+
let insertedText = rawData
|
|
2534
|
+
.replace(new RegExp(DELIMITER, 'g'), ' ')
|
|
2535
|
+
.replace(/[\u200B\u200C\u200D\uFEFF]/g, '')
|
|
2536
|
+
.replace(/\s+/g, ' ');
|
|
2537
|
+
|
|
2538
|
+
if (insertedText.length === 0) {
|
|
2539
|
+
evt.preventDefault();
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// Intercept the browser default and perform the edit via Ace APIs
|
|
2544
|
+
evt.preventDefault();
|
|
2545
|
+
evt.stopPropagation();
|
|
2546
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2547
|
+
|
|
2548
|
+
try {
|
|
2549
|
+
// Defer to next tick to avoid racing with browser internal composition state
|
|
2550
|
+
setTimeout(() => {
|
|
2551
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2552
|
+
aceInstance.ace_fastIncorp(10);
|
|
2553
|
+
const freshRep = aceInstance.ace_getRep();
|
|
2554
|
+
const freshSelStart = freshRep.selStart;
|
|
2555
|
+
const freshSelEnd = freshRep.selEnd;
|
|
2556
|
+
|
|
2557
|
+
// Replace selection with sanitized text
|
|
2558
|
+
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
|
|
2559
|
+
|
|
2560
|
+
// Re-apply cell attribute to the newly inserted text (clamped to line length)
|
|
2561
|
+
const repAfterReplace = aceInstance.ace_getRep();
|
|
2562
|
+
const freshLineIndex = freshSelStart[0];
|
|
2563
|
+
const freshLineEntry = repAfterReplace.lines.atIndex(freshLineIndex);
|
|
2564
|
+
const maxLen = Math.max(0, (freshLineEntry && freshLineEntry.text) ? freshLineEntry.text.length : 0);
|
|
2565
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
2566
|
+
const endColRaw = startCol + insertedText.length;
|
|
2567
|
+
const endCol = Math.min(endColRaw, maxLen);
|
|
2568
|
+
if (endCol > startCol) {
|
|
2569
|
+
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
2570
|
+
[freshLineIndex, startCol], [freshLineIndex, endCol], [[ATTR_CELL, String(targetCellIndex)]]
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// Re-apply table metadata line attribute
|
|
2575
|
+
ed.ep_data_tables_applyMeta(
|
|
2576
|
+
freshLineIndex,
|
|
2577
|
+
tableMetadata.tblId,
|
|
2578
|
+
tableMetadata.row,
|
|
2579
|
+
tableMetadata.cols,
|
|
2580
|
+
repAfterReplace,
|
|
2581
|
+
ed,
|
|
2582
|
+
null,
|
|
2583
|
+
docManager
|
|
2584
|
+
);
|
|
2585
|
+
|
|
2586
|
+
// Move caret to end of inserted text and update last-click state
|
|
2587
|
+
const newCaretCol = endCol;
|
|
2588
|
+
const newCaretPos = [freshLineIndex, newCaretCol];
|
|
2589
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2590
|
+
aceInstance.ace_fastIncorp(10);
|
|
2591
|
+
|
|
2592
|
+
if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
2593
|
+
// Recompute cellStartCol for the fresh line to avoid mismatches
|
|
2594
|
+
const freshLineText = (freshLineEntry && freshLineEntry.text) || '';
|
|
2595
|
+
const freshCells = freshLineText.split(DELIMITER);
|
|
2596
|
+
let freshOffset = 0;
|
|
2597
|
+
for (let i = 0; i < targetCellIndex; i++) {
|
|
2598
|
+
freshOffset += (freshCells[i]?.length ?? 0) + DELIMITER.length;
|
|
2599
|
+
}
|
|
2600
|
+
const newRelativePos = newCaretCol - freshOffset;
|
|
2601
|
+
editor.ep_data_tables_last_clicked = {
|
|
2602
|
+
lineNum: freshLineIndex,
|
|
2603
|
+
tblId: tableMetadata.tblId,
|
|
2604
|
+
cellIndex: targetCellIndex,
|
|
2605
|
+
relativePos: newRelativePos < 0 ? 0 : newRelativePos,
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
}, 'tableInsertTextOperations', true);
|
|
2609
|
+
}, 0);
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
console.error(`${insertLogPrefix} ERROR during insert handling:`, error);
|
|
2612
|
+
}
|
|
2613
|
+
});
|
|
2614
|
+
|
|
2615
|
+
// Composition start marker (Android Chrome/table only)
|
|
2616
|
+
$inner.on('compositionstart', (evt) => {
|
|
2617
|
+
if (!isAndroidUA()) return;
|
|
2618
|
+
const rep = ed.ace_getRep();
|
|
2619
|
+
if (!rep || !rep.selStart) return;
|
|
2620
|
+
const lineNum = rep.selStart[0];
|
|
2621
|
+
let meta = null; let s = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2622
|
+
if (s) { try { meta = JSON.parse(s); } catch (_) {} }
|
|
2623
|
+
if (!meta) meta = getTableLineMetadata(lineNum, ed, docManager);
|
|
2624
|
+
if (!meta || typeof meta.cols !== 'number') return;
|
|
2625
|
+
isAndroidChromeComposition = true;
|
|
2626
|
+
handledCurrentComposition = false;
|
|
2627
|
+
suppressBeforeInputInsertTextDuringComposition = false;
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
// Android Chrome composition handling for whitespace (space) to prevent DOM mutation breaking delimiters
|
|
2631
|
+
$inner.on('compositionupdate', (evt) => {
|
|
2632
|
+
const compLogPrefix = '[ep_data_tables:compositionHandler]';
|
|
2633
|
+
|
|
2634
|
+
if (!isAndroidUA()) return;
|
|
2635
|
+
|
|
2636
|
+
const rep = ed.ace_getRep();
|
|
2637
|
+
if (!rep || !rep.selStart) return;
|
|
2638
|
+
const selStart = rep.selStart;
|
|
2639
|
+
const selEnd = rep.selEnd;
|
|
2640
|
+
const lineNum = selStart[0];
|
|
2641
|
+
|
|
2642
|
+
// Ensure we are inside a table line
|
|
2643
|
+
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2644
|
+
let tableMetadata = null;
|
|
2645
|
+
if (lineAttrString) { try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {} }
|
|
2646
|
+
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2647
|
+
if (!tableMetadata || typeof tableMetadata.cols !== 'number') return;
|
|
2648
|
+
|
|
2649
|
+
// Only act on whitespace-only composition updates (space/non-breaking space)
|
|
2650
|
+
const d = evt.originalEvent && typeof evt.originalEvent.data === 'string' ? evt.originalEvent.data : '';
|
|
2651
|
+
if (evt.type === 'compositionupdate') {
|
|
2652
|
+
const isWhitespaceOnly = d && d.replace(/\u00A0/g, ' ').trim() === '';
|
|
2653
|
+
if (!isWhitespaceOnly) return;
|
|
2654
|
+
|
|
2655
|
+
// Compute target cell and clamp selection to cell boundaries
|
|
2656
|
+
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2657
|
+
const cells = lineText.split(DELIMITER);
|
|
2658
|
+
let currentOffset = 0;
|
|
2659
|
+
let targetCellIndex = -1;
|
|
2660
|
+
let cellStartCol = 0;
|
|
2661
|
+
let cellEndCol = 0;
|
|
2662
|
+
for (let i = 0; i < cells.length; i++) {
|
|
2663
|
+
const cellLength = cells[i]?.length ?? 0;
|
|
2664
|
+
const cellEndColThisIteration = currentOffset + cellLength;
|
|
2665
|
+
if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
|
|
2666
|
+
targetCellIndex = i;
|
|
2667
|
+
cellStartCol = currentOffset;
|
|
2668
|
+
cellEndCol = cellEndColThisIteration;
|
|
2669
|
+
break;
|
|
2670
|
+
}
|
|
2671
|
+
currentOffset += cellLength + DELIMITER.length;
|
|
2672
|
+
}
|
|
2673
|
+
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) return;
|
|
2674
|
+
|
|
2675
|
+
// Prevent composition DOM mutation and insert sanitized space via Ace
|
|
2676
|
+
evt.preventDefault();
|
|
2677
|
+
evt.stopPropagation();
|
|
2678
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2679
|
+
|
|
2680
|
+
let insertedText = d.replace(/\u00A0/g, ' ');
|
|
2681
|
+
if (insertedText.length === 0) insertedText = ' ';
|
|
2682
|
+
|
|
2683
|
+
try {
|
|
2684
|
+
setTimeout(() => {
|
|
2685
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2686
|
+
aceInstance.ace_fastIncorp(10);
|
|
2687
|
+
const freshRep = aceInstance.ace_getRep();
|
|
2688
|
+
const freshSelStart = freshRep.selStart;
|
|
2689
|
+
const freshSelEnd = freshRep.selEnd;
|
|
2690
|
+
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
|
|
2691
|
+
|
|
2692
|
+
// Clamp attribute application to current line bounds and use fresh line index
|
|
2693
|
+
const repAfterReplace = aceInstance.ace_getRep();
|
|
2694
|
+
const freshLineIndex = freshSelStart[0];
|
|
2695
|
+
const freshLineEntry = repAfterReplace.lines.atIndex(freshLineIndex);
|
|
2696
|
+
const maxLen = Math.max(0, (freshLineEntry && freshLineEntry.text) ? freshLineEntry.text.length : 0);
|
|
2697
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
2698
|
+
const endColRaw = startCol + insertedText.length;
|
|
2699
|
+
const endCol = Math.min(endColRaw, maxLen);
|
|
2700
|
+
if (endCol > startCol) {
|
|
2701
|
+
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
2702
|
+
[freshLineIndex, startCol], [freshLineIndex, endCol], [[ATTR_CELL, String(targetCellIndex)]]
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
ed.ep_data_tables_applyMeta(
|
|
2707
|
+
freshLineIndex,
|
|
2708
|
+
tableMetadata.tblId,
|
|
2709
|
+
tableMetadata.row,
|
|
2710
|
+
tableMetadata.cols,
|
|
2711
|
+
repAfterReplace,
|
|
2712
|
+
ed,
|
|
2713
|
+
null,
|
|
2714
|
+
docManager
|
|
2715
|
+
);
|
|
2716
|
+
|
|
2717
|
+
const newCaretCol = endCol;
|
|
2718
|
+
const newCaretPos = [freshLineIndex, newCaretCol];
|
|
2719
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2720
|
+
aceInstance.ace_fastIncorp(10);
|
|
2721
|
+
if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
2722
|
+
// Recompute cellStartCol for fresh line
|
|
2723
|
+
const freshLineText = (freshLineEntry && freshLineEntry.text) || '';
|
|
2724
|
+
const freshCells = freshLineText.split(DELIMITER);
|
|
2725
|
+
let freshOffset = 0;
|
|
2726
|
+
for (let i = 0; i < targetCellIndex; i++) {
|
|
2727
|
+
freshOffset += (freshCells[i]?.length ?? 0) + DELIMITER.length;
|
|
2728
|
+
}
|
|
2729
|
+
const newRelativePos = newCaretCol - freshOffset;
|
|
2730
|
+
editor.ep_data_tables_last_clicked = {
|
|
2731
|
+
lineNum: freshLineIndex,
|
|
2732
|
+
tblId: tableMetadata.tblId,
|
|
2733
|
+
cellIndex: targetCellIndex,
|
|
2734
|
+
relativePos: newRelativePos < 0 ? 0 : newRelativePos,
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
}, 'tableCompositionSpaceInsert', true);
|
|
2738
|
+
}, 0);
|
|
2739
|
+
// Suppress all subsequent beforeinput insertText events in this composition session
|
|
2740
|
+
suppressBeforeInputInsertTextDuringComposition = true;
|
|
2741
|
+
} catch (error) {
|
|
2742
|
+
console.error(`${compLogPrefix} ERROR inserting space during composition:`, error);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
// Composition end cleanup
|
|
2748
|
+
$inner.on('compositionend', () => {
|
|
2749
|
+
if (isAndroidChromeComposition) {
|
|
2750
|
+
isAndroidChromeComposition = false;
|
|
2751
|
+
handledCurrentComposition = false;
|
|
2752
|
+
suppressBeforeInputInsertTextDuringComposition = false;
|
|
2753
|
+
}
|
|
2754
|
+
});
|
|
2755
|
+
|
|
2340
2756
|
// *** DRAG AND DROP EVENT LISTENERS ***
|
|
2341
2757
|
// log(`${callWithAceLogPrefix} Attaching drag and drop event listeners to $inner (inner iframe body).`);
|
|
2342
2758
|
|