ep_data_tables 0.0.98 → 0.0.99
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 +114 -36
package/package.json
CHANGED
|
@@ -2601,12 +2601,18 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2601
2601
|
let originalTableLine = null; // Track where the table actually is
|
|
2602
2602
|
|
|
2603
2603
|
// Recover cursor if browser moved it to wrong line between compositions
|
|
2604
|
+
// CONSERVATIVE: More validation, shorter timeout, always clear after check
|
|
2604
2605
|
let usedCursorRecovery = false;
|
|
2605
2606
|
if (desktopComposition && desktopComposition._expectedCursor) {
|
|
2606
2607
|
const stored = desktopComposition._expectedCursor;
|
|
2607
2608
|
const timeSinceStored = Date.now() - stored.timestamp;
|
|
2608
2609
|
|
|
2609
|
-
|
|
2610
|
+
// Reduced timeout from 1000ms to 500ms for rapid typing safety
|
|
2611
|
+
const isFresh = timeSinceStored < 500;
|
|
2612
|
+
const sameTable = stored.tblId && tableMetadata && stored.tblId === tableMetadata.tblId;
|
|
2613
|
+
const reasonableLineDelta = typeof stored.lineNum === 'number' && Math.abs(lineNum - stored.lineNum) <= 1;
|
|
2614
|
+
|
|
2615
|
+
if (isFresh && sameTable && reasonableLineDelta) {
|
|
2610
2616
|
const currentLine = lineNum;
|
|
2611
2617
|
const expectedLine = stored.lineNum;
|
|
2612
2618
|
const expectedCell = stored.cellIndex;
|
|
@@ -2615,26 +2621,35 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2615
2621
|
const seemsWrong = lineChanged && (currentLine === expectedLine + 1);
|
|
2616
2622
|
|
|
2617
2623
|
if (seemsWrong) {
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
originalTableLine = expectedLine;
|
|
2624
|
-
usedCursorRecovery = true;
|
|
2624
|
+
// Additional validation: verify stored position is still valid
|
|
2625
|
+
try {
|
|
2626
|
+
const storedEntry = rep0.lines.atIndex(expectedLine);
|
|
2627
|
+
const storedText = storedEntry?.text || '';
|
|
2628
|
+
const storedCells = storedText.split(DELIMITER);
|
|
2625
2629
|
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2630
|
+
// Only recover if cell structure matches expectations
|
|
2631
|
+
if (expectedCell >= 0 && expectedCell < storedCells.length) {
|
|
2632
|
+
const storedMeta = getTableMetadataForLine(expectedLine);
|
|
2633
|
+
if (storedMeta && storedMeta.tblId === stored.tblId) {
|
|
2634
|
+
lineNum = expectedLine;
|
|
2635
|
+
cellIndex = expectedCell;
|
|
2636
|
+
tableMetadata = storedMeta;
|
|
2637
|
+
originalTableLine = expectedLine;
|
|
2638
|
+
usedCursorRecovery = true;
|
|
2639
|
+
|
|
2640
|
+
try {
|
|
2641
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2642
|
+
aceInstance.ace_performSelectionChange([expectedLine, stored.col], [expectedLine, stored.col], false);
|
|
2643
|
+
}, 'compositionstart-cursor-recovery', true);
|
|
2644
|
+
} catch (_) {}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
} catch (_) { /* validation failed, don't recover */ }
|
|
2632
2648
|
}
|
|
2633
2649
|
}
|
|
2634
2650
|
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
}
|
|
2651
|
+
// ALWAYS clear after checking to prevent stale reuse
|
|
2652
|
+
delete desktopComposition._expectedCursor;
|
|
2638
2653
|
}
|
|
2639
2654
|
|
|
2640
2655
|
try {
|
|
@@ -4081,12 +4096,29 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4081
4096
|
const dataPreview = typeof nativeEvt?.data === 'string' ? nativeEvt.data : '';
|
|
4082
4097
|
logCompositionEvent('compositionend-desktop-fired', evt, { data: dataPreview });
|
|
4083
4098
|
|
|
4084
|
-
// Capture
|
|
4085
|
-
//
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4099
|
+
// CRITICAL: Capture composition state with DEEP COPY before async deferral.
|
|
4100
|
+
// This prevents race conditions where a new compositionstart overwrites state
|
|
4101
|
+
// before this callback executes. Deep copy all arrays to prevent shared references.
|
|
4102
|
+
const capturedComposition = desktopComposition
|
|
4103
|
+
? {
|
|
4104
|
+
active: desktopComposition.active,
|
|
4105
|
+
start: desktopComposition.start ? desktopComposition.start.slice() : null,
|
|
4106
|
+
end: desktopComposition.end ? desktopComposition.end.slice() : null,
|
|
4107
|
+
lineNum: desktopComposition.lineNum,
|
|
4108
|
+
cellIndex: desktopComposition.cellIndex,
|
|
4109
|
+
tblId: desktopComposition.tblId,
|
|
4110
|
+
snapshot: desktopComposition.snapshot ? desktopComposition.snapshot.slice() : null,
|
|
4111
|
+
snapshotMeta: desktopComposition.snapshotMeta ? { ...desktopComposition.snapshotMeta } : null,
|
|
4112
|
+
originalTableLine: desktopComposition.originalTableLine,
|
|
4113
|
+
usedCursorRecovery: desktopComposition.usedCursorRecovery,
|
|
4114
|
+
}
|
|
4115
|
+
: null;
|
|
4116
|
+
|
|
4117
|
+
// Capture the commit string synchronously too (before any async deferral)
|
|
4118
|
+
const commitStrRawSync = typeof nativeEvt?.data === 'string' ? nativeEvt.data : '';
|
|
4119
|
+
|
|
4120
|
+
// Prevent the immediate post-composition input commit from running; we pipeline instead
|
|
4121
|
+
suppressNextInputCommit = true;
|
|
4090
4122
|
requestAnimationFrame(() => {
|
|
4091
4123
|
try {
|
|
4092
4124
|
ed.ace_callWithAce((aceInstance) => {
|
|
@@ -4106,12 +4138,12 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4106
4138
|
return;
|
|
4107
4139
|
}
|
|
4108
4140
|
|
|
4109
|
-
// Pipeline: Apply committed IME string
|
|
4110
|
-
|
|
4111
|
-
const commitStr = sanitizeCellContent(
|
|
4141
|
+
// Pipeline: Apply committed IME string to the target cell
|
|
4142
|
+
// Use the synchronously captured commit string (commitStrRawSync) to avoid race conditions
|
|
4143
|
+
const commitStr = sanitizeCellContent(commitStrRawSync || '');
|
|
4112
4144
|
// Only suppress the next beforeinput commit if the IME provided a non-empty commit string.
|
|
4113
4145
|
// Normalize using the same soft-whitespace rules the editor uses.
|
|
4114
|
-
const willCommit = typeof
|
|
4146
|
+
const willCommit = typeof commitStrRawSync === 'string' && normalizeSoftWhitespace(commitStrRawSync).trim().length > 0;
|
|
4115
4147
|
if (willCommit) suppressNextBeforeinputCommitOnce = true;
|
|
4116
4148
|
const repNow = aceInstance.ace_getRep();
|
|
4117
4149
|
const caret = repNow && repNow.selStart;
|
|
@@ -4239,15 +4271,44 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4239
4271
|
}
|
|
4240
4272
|
const cellsNow = (entry.text || '').split(DELIMITER);
|
|
4241
4273
|
while (cellsNow.length < metadata.cols) cellsNow.push(' ');
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4274
|
+
|
|
4275
|
+
// DEFENSIVE CELL INDEX RESOLUTION:
|
|
4276
|
+
// 1. Prefer the cell index captured at compositionstart
|
|
4277
|
+
// 2. Fall back to live DOM selection (authoritative for current cursor)
|
|
4278
|
+
// 3. Fall back to raw line text mapping using current caret
|
|
4279
|
+
// 4. Final fallback: first cell (ensures we never lose text)
|
|
4280
|
+
let idx = -1;
|
|
4281
|
+
|
|
4282
|
+
// Strategy 1: Use captured cell index from compositionstart
|
|
4283
|
+
if (capturedComposition && capturedComposition.cellIndex >= 0) {
|
|
4284
|
+
idx = capturedComposition.cellIndex;
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
// Strategy 2: Use live DOM selection as authoritative source
|
|
4288
|
+
if (idx < 0) {
|
|
4289
|
+
try {
|
|
4290
|
+
const liveDomTarget = getDomCellTargetFromSelection();
|
|
4291
|
+
if (liveDomTarget && typeof liveDomTarget.idx === 'number' && liveDomTarget.idx >= 0) {
|
|
4292
|
+
idx = liveDomTarget.idx;
|
|
4293
|
+
}
|
|
4294
|
+
} catch (_) { /* ignore DOM errors */ }
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
// Strategy 3: Compute from raw line text using captured or current caret
|
|
4298
|
+
if (idx < 0) {
|
|
4299
|
+
try {
|
|
4300
|
+
const selCol = (capturedComposition && capturedComposition.start)
|
|
4301
|
+
? capturedComposition.start[1]
|
|
4302
|
+
: (caret ? caret[1] : 0);
|
|
4303
|
+
const rawMap = computeTargetCellIndexFromRaw(entry, selCol);
|
|
4304
|
+
if (rawMap.index >= 0) idx = rawMap.index;
|
|
4305
|
+
} catch (_) { /* ignore */ }
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
// Strategy 4: Final fallback - first cell (ensures no data loss)
|
|
4309
|
+
if (idx < 0) idx = 0;
|
|
4310
|
+
// Clamp to valid range
|
|
4311
|
+
if (idx >= metadata.cols) idx = metadata.cols - 1;
|
|
4251
4312
|
|
|
4252
4313
|
// Compute relative selection in cell
|
|
4253
4314
|
let baseOffset = 0;
|
|
@@ -4314,7 +4375,21 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4314
4375
|
}
|
|
4315
4376
|
|
|
4316
4377
|
// Post-composition orphan detection and repair using snapshot
|
|
4378
|
+
// Capture timestamp for staleness check
|
|
4379
|
+
const orphanRepairScheduledAt = Date.now();
|
|
4317
4380
|
setTimeout(() => {
|
|
4381
|
+
// GUARD: Skip if a new composition started since we scheduled this
|
|
4382
|
+
// This prevents concurrent document modifications during rapid typing
|
|
4383
|
+
if (__epDT_compositionActive) {
|
|
4384
|
+
console.debug('[ep_data_tables:compositionend-orphan-repair] skipped - new composition active');
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
// Also skip if another composition just ended (within 30ms)
|
|
4388
|
+
if (Date.now() - __epDT_lastCompositionEndTime < 30 && __epDT_lastCompositionEndTime > orphanRepairScheduledAt) {
|
|
4389
|
+
console.debug('[ep_data_tables:compositionend-orphan-repair] skipped - very recent composition end');
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4318
4393
|
// Pre-check editor state
|
|
4319
4394
|
// (keyToNodeMap.get(...) is undefined error)
|
|
4320
4395
|
try {
|
|
@@ -4686,7 +4761,10 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4686
4761
|
}
|
|
4687
4762
|
}, 50); // Small delay to let Etherpad process the cell edit first
|
|
4688
4763
|
|
|
4764
|
+
// Reset composition state but PRESERVE _expectedCursor for next compositionstart
|
|
4765
|
+
const preservedCursor = desktopComposition._expectedCursor;
|
|
4689
4766
|
desktopComposition = { active: false, start: null, end: null, lineNum: null, cellIndex: -1, snapshot: null, snapshotMeta: null };
|
|
4767
|
+
if (preservedCursor) desktopComposition._expectedCursor = preservedCursor;
|
|
4690
4768
|
}, 'tableDesktopCompositionEnd');
|
|
4691
4769
|
} catch (compositionErr) {
|
|
4692
4770
|
console.error(`${compLogPrefix} ERROR during desktop composition repair:`, compositionErr);
|