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