ep_data_tables 0.0.7 → 0.0.8
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 +519 -54
package/package.json
CHANGED
|
@@ -22,6 +22,8 @@ const DELIMITER = '\u241F'; // Internal column delimiter (␟)
|
|
|
22
22
|
// still find delimiters when it splits node.innerHTML.
|
|
23
23
|
// Users never see this because the span is contenteditable=false and styled away.
|
|
24
24
|
const HIDDEN_DELIM = DELIMITER;
|
|
25
|
+
// InputEvent inputTypes used by mobile autocorrect/IME commit
|
|
26
|
+
const INPUTTYPE_REPLACEMENT_TYPES = new Set(['insertReplacementText', 'insertFromComposition']);
|
|
25
27
|
|
|
26
28
|
// helper for stable random ids
|
|
27
29
|
const rand = () => Math.random().toString(36).slice(2, 8);
|
|
@@ -75,6 +77,12 @@ function isAndroidUA() {
|
|
|
75
77
|
return isAndroid && !isIOS;
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
// Helper to detect any iOS browser (Safari, Chrome iOS, Firefox iOS, Edge iOS)
|
|
81
|
+
function isIOSUA() {
|
|
82
|
+
const ua = (navigator.userAgent || '').toLowerCase();
|
|
83
|
+
return ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod') || ua.includes('ios') || ua.includes('crios') || ua.includes('fxios') || ua.includes('edgios');
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
// ─────────────────── Reusable Helper Functions ───────────────────
|
|
79
87
|
|
|
80
88
|
/**
|
|
@@ -671,7 +679,6 @@ function escapeHtml(text = '') {
|
|
|
671
679
|
};
|
|
672
680
|
return strText.replace(/[&<>"'']/g, function(m) { return map[m]; });
|
|
673
681
|
}
|
|
674
|
-
|
|
675
682
|
// NEW Helper function to build table HTML from pre-rendered delimited content with resize handles
|
|
676
683
|
function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
|
|
677
684
|
const funcName = 'buildTableFromDelimitedHTML';
|
|
@@ -734,6 +741,42 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
|
|
|
734
741
|
? `<span class="${encodedTbljsonClass ? `${encodedTbljsonClass} ` : ''}tblCell-${index}">${modifiedSegment}${caretAnchorSpan}</span>`
|
|
735
742
|
: `${modifiedSegment}${caretAnchorSpan}`);
|
|
736
743
|
|
|
744
|
+
// Normalize: ensure first content span has correct tblCell-N and strip tblCell-* from subsequent spans
|
|
745
|
+
try {
|
|
746
|
+
const requiredCellClass = `tblCell-${index}`;
|
|
747
|
+
// Skip a leading delimiter span if present
|
|
748
|
+
const leadingDelimMatch = modifiedSegment.match(/^\s*<span[^>]*\bep-data_tables-delim\b[^>]*>[\s\S]*?<\/span>\s*/i);
|
|
749
|
+
const head = leadingDelimMatch ? leadingDelimMatch[0] : '';
|
|
750
|
+
const tail = leadingDelimMatch ? modifiedSegment.slice(head.length) : modifiedSegment;
|
|
751
|
+
|
|
752
|
+
const openSpanMatch = tail.match(/^\s*<span([^>]*)>/i);
|
|
753
|
+
if (!openSpanMatch) {
|
|
754
|
+
// No span at start: wrap with required cell class. Keep tbljson if available for stability.
|
|
755
|
+
const baseClasses = `${encodedTbljsonClass ? `${encodedTbljsonClass} ` : ''}${requiredCellClass}`;
|
|
756
|
+
modifiedSegment = `${head}<span class="${baseClasses}">${tail}</span>`;
|
|
757
|
+
} else {
|
|
758
|
+
const fullOpen = openSpanMatch[0];
|
|
759
|
+
const attrs = openSpanMatch[1] || '';
|
|
760
|
+
const classMatch = /\bclass\s*=\s*"([^"]*)"/i.exec(attrs);
|
|
761
|
+
let classList = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
|
|
762
|
+
// Remove any stale tblCell-* from the first span's class list
|
|
763
|
+
classList = classList.filter(c => !/^tblCell-\d+$/.test(c));
|
|
764
|
+
// Ensure required cell class exists
|
|
765
|
+
classList.push(requiredCellClass);
|
|
766
|
+
const unique = Array.from(new Set(classList));
|
|
767
|
+
const newClassAttr = ` class="${unique.join(' ')}"`;
|
|
768
|
+
const attrsWithoutClass = classMatch ? attrs.replace(/\s*class\s*=\s*"[^"]*"/i, '') : attrs;
|
|
769
|
+
const rebuiltOpen = `<span${newClassAttr}${attrsWithoutClass}>`;
|
|
770
|
+
const afterOpen = tail.slice(fullOpen.length);
|
|
771
|
+
// Remove tblCell-* from any subsequent spans to avoid cascading mismatches (keep tbljson- as-is)
|
|
772
|
+
const cleanedTail = afterOpen.replace(/(<span[^>]*class=")([^"]*)(")/ig, (m, p1, classes, p3) => {
|
|
773
|
+
const filtered = classes.split(/\s+/).filter(c => c && !/^tblCell-\d+$/.test(c)).join(' ');
|
|
774
|
+
return p1 + filtered + p3;
|
|
775
|
+
});
|
|
776
|
+
modifiedSegment = head + rebuiltOpen + cleanedTail;
|
|
777
|
+
}
|
|
778
|
+
} catch (_) { /* ignore normalization errors */ }
|
|
779
|
+
|
|
737
780
|
// Width & other decorations remain unchanged
|
|
738
781
|
const widthPercent = columnWidths[index] || (100 / numCols);
|
|
739
782
|
const cellStyle = `${tdStyle} width: ${widthPercent}%;`;
|
|
@@ -964,11 +1007,15 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
964
1007
|
// NEW: Remove all hidden-delimiter <span> wrappers **before** we split so
|
|
965
1008
|
// the embedded delimiter character they carry doesn't inflate or shrink
|
|
966
1009
|
// the segment count.
|
|
967
|
-
|
|
1010
|
+
// Preserve the delimiter character when stripping its wrapper span so splitting works
|
|
1011
|
+
const spanDelimRegex = /<span class="ep-data_tables-delim"[^>]*>[\s\S]*?<\/span>/ig;
|
|
968
1012
|
const sanitizedHTMLForSplit = (delimitedTextFromLine || '')
|
|
969
|
-
.replace(spanDelimRegex,
|
|
1013
|
+
.replace(spanDelimRegex, DELIMITER)
|
|
970
1014
|
// strip caret anchors from raw line html before split
|
|
971
|
-
.replace(/<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig, '')
|
|
1015
|
+
.replace(/<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig, '')
|
|
1016
|
+
.replace(/\r?\n/g, ' ')
|
|
1017
|
+
.replace(/\u00A0/gu, ' ')
|
|
1018
|
+
.replace(/<br\s*\/?>/gi, ' ');
|
|
972
1019
|
const htmlSegments = sanitizedHTMLForSplit.split(DELIMITER);
|
|
973
1020
|
|
|
974
1021
|
// log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT ANALYSIS ***`);
|
|
@@ -987,6 +1034,16 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
987
1034
|
if (segment.includes('image:') || segment.includes('image-placeholder') || segment.includes('currently-selected')) {
|
|
988
1035
|
// log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT[${i}] CONTAINS IMAGE CONTENT ***`);
|
|
989
1036
|
}
|
|
1037
|
+
// Diagnostics: surface suspicious class patterns that can precede breakage
|
|
1038
|
+
try {
|
|
1039
|
+
const tblCellMatches = segment.match(/\btblCell-(\d+)\b/g) || [];
|
|
1040
|
+
const tbljsonMatches = segment.match(/\btbljson-[A-Za-z0-9_-]+\b/g) || [];
|
|
1041
|
+
const uniqueCells = Array.from(new Set(tblCellMatches));
|
|
1042
|
+
if (uniqueCells.length > 1) {
|
|
1043
|
+
console.warn('[ep_data_tables][diag] segment contains multiple tblCell-* markers', { segIndex: i, uniqueCells });
|
|
1044
|
+
}
|
|
1045
|
+
// intentionally ignore duplicate tbljson-* occurrences – not root cause
|
|
1046
|
+
} catch (_) {}
|
|
990
1047
|
}
|
|
991
1048
|
|
|
992
1049
|
// log(`${logPrefix} NodeID#${nodeId}: Parsed HTML segments (${htmlSegments.length}):`, htmlSegments.map(s => (s || '').substring(0,50) + (s && s.length > 50 ? '...' : '')));
|
|
@@ -996,7 +1053,8 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
996
1053
|
|
|
997
1054
|
if (htmlSegments.length !== rowMetadata.cols) {
|
|
998
1055
|
// log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH DETECTED *** - Attempting reconstruction.`);
|
|
999
|
-
|
|
1056
|
+
console.warn('[ep_data_tables][diag] Segment/column mismatch', { nodeId, lineNum, segs: htmlSegments.length, cols: rowMetadata.cols, tblId: rowMetadata.tblId, row: rowMetadata.row });
|
|
1057
|
+
|
|
1000
1058
|
// Check if this is an image selection issue
|
|
1001
1059
|
const hasImageSelected = delimitedTextFromLine.includes('currently-selected');
|
|
1002
1060
|
const hasImageContent = delimitedTextFromLine.includes('image:');
|
|
@@ -1004,47 +1062,9 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
1004
1062
|
// log(`${logPrefix} NodeID#${nodeId}: *** POTENTIAL CAUSE: Image selection state may be affecting segment parsing ***`);
|
|
1005
1063
|
}
|
|
1006
1064
|
|
|
1007
|
-
// First attempt:
|
|
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.
|
|
1008
1067
|
let usedClassReconstruction = false;
|
|
1009
|
-
try {
|
|
1010
|
-
const cols = Math.max(0, Number(rowMetadata.cols) || 0);
|
|
1011
|
-
const grouped = Array.from({ length: cols }, () => '');
|
|
1012
|
-
const candidates = Array.from(node.querySelectorAll('[class*="tblCell-"]'));
|
|
1013
|
-
|
|
1014
|
-
const classNum = (el) => {
|
|
1015
|
-
if (!el || !el.classList) return -1;
|
|
1016
|
-
for (const cls of el.classList) {
|
|
1017
|
-
const m = /^tblCell-(\d+)$/.exec(cls);
|
|
1018
|
-
if (m) return parseInt(m[1], 10);
|
|
1019
|
-
}
|
|
1020
|
-
return -1;
|
|
1021
|
-
};
|
|
1022
|
-
const hasAncestorWithSameCell = (el, n) => {
|
|
1023
|
-
let p = el?.parentElement;
|
|
1024
|
-
while (p) {
|
|
1025
|
-
if (p.classList && p.classList.contains(`tblCell-${n}`)) return true;
|
|
1026
|
-
p = p.parentElement;
|
|
1027
|
-
}
|
|
1028
|
-
return false;
|
|
1029
|
-
};
|
|
1030
|
-
|
|
1031
|
-
for (const el of candidates) {
|
|
1032
|
-
const n = classNum(el);
|
|
1033
|
-
if (n >= 0 && n < cols) {
|
|
1034
|
-
if (!hasAncestorWithSameCell(el, n)) {
|
|
1035
|
-
grouped[n] += el.outerHTML || '';
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
const usable = grouped.some(s => s && s.trim() !== '');
|
|
1040
|
-
if (usable) {
|
|
1041
|
-
finalHtmlSegments = grouped.map(s => (s && s.trim() !== '') ? s : ' ');
|
|
1042
|
-
usedClassReconstruction = true;
|
|
1043
|
-
console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Reconstructed ${finalHtmlSegments.length} segments from tblCell-N classes.`);
|
|
1044
|
-
}
|
|
1045
|
-
} catch (e) {
|
|
1046
|
-
console.debug(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Class-based reconstruction error; falling back.`, e);
|
|
1047
|
-
}
|
|
1048
1068
|
|
|
1049
1069
|
// Fallback: reconstruct from string segments
|
|
1050
1070
|
if (!usedClassReconstruction) {
|
|
@@ -1133,7 +1153,6 @@ function _getLineNumberOfElement(element) {
|
|
|
1133
1153
|
}
|
|
1134
1154
|
return count;
|
|
1135
1155
|
}
|
|
1136
|
-
|
|
1137
1156
|
// ───────────────────── Handle Key Events ─────────────────────
|
|
1138
1157
|
exports.aceKeyEvent = (h, ctx) => {
|
|
1139
1158
|
const funcName = 'aceKeyEvent';
|
|
@@ -1887,7 +1906,6 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1887
1906
|
// log(`${logPrefix} [caretTrace] Final rep.selStart at end of aceKeyEvent (if unhandled): Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
|
|
1888
1907
|
return false; // Allow default browser/ACE handling
|
|
1889
1908
|
};
|
|
1890
|
-
|
|
1891
1909
|
// ───────────────────── ace init + public helpers ─────────────────────
|
|
1892
1910
|
exports.aceInitialized = (h, ctx) => {
|
|
1893
1911
|
const logPrefix = '[ep_data_tables:aceInitialized]';
|
|
@@ -1951,6 +1969,121 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
1951
1969
|
}
|
|
1952
1970
|
$inner = $(innerDocBody[0]); // Ensure it's a jQuery object of the body itself
|
|
1953
1971
|
// log(`${callWithAceLogPrefix} Successfully found inner iframe body:`, $inner);
|
|
1972
|
+
|
|
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
|
+
const mobileSuggestionBlocker = (evt) => {
|
|
1986
|
+
const t = evt && evt.inputType || '';
|
|
1987
|
+
const dataStr = (evt && typeof evt.data === 'string') ? evt.data : '';
|
|
1988
|
+
const isProblem = (
|
|
1989
|
+
t === 'insertReplacementText' ||
|
|
1990
|
+
t === 'insertFromComposition' ||
|
|
1991
|
+
(t === 'insertText' && !evt.isComposing && (!dataStr || dataStr.length > 1))
|
|
1992
|
+
);
|
|
1993
|
+
if (!isProblem) return;
|
|
1994
|
+
|
|
1995
|
+
// Only act if selection is inside a table line
|
|
1996
|
+
try {
|
|
1997
|
+
const repQuick = ed.ace_getRep && ed.ace_getRep();
|
|
1998
|
+
if (!repQuick || !repQuick.selStart) return; // don't block outside editor selection
|
|
1999
|
+
const lineNumQuick = repQuick.selStart[0];
|
|
2000
|
+
let metaStrQuick = docManager && docManager.getAttributeOnLine
|
|
2001
|
+
? docManager.getAttributeOnLine(lineNumQuick, ATTR_TABLE_JSON)
|
|
2002
|
+
: null;
|
|
2003
|
+
let metaQuick = null;
|
|
2004
|
+
if (metaStrQuick) { try { metaQuick = JSON.parse(metaStrQuick); } catch (_) {} }
|
|
2005
|
+
if (!metaQuick) metaQuick = getTableLineMetadata(lineNumQuick, ed, docManager);
|
|
2006
|
+
if (!metaQuick || typeof metaQuick.cols !== 'number') return; // not a table line → do not block
|
|
2007
|
+
} catch (_) { return; }
|
|
2008
|
+
|
|
2009
|
+
// Cancel the browser's DOM mutation early
|
|
2010
|
+
evt.preventDefault();
|
|
2011
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2012
|
+
|
|
2013
|
+
// Replace selection with a single plain space using Ace APIs
|
|
2014
|
+
setTimeout(() => {
|
|
2015
|
+
try {
|
|
2016
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2017
|
+
aceInstance.ace_fastIncorp(10);
|
|
2018
|
+
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);
|
|
2036
|
+
}
|
|
2037
|
+
} catch (_) {}
|
|
2038
|
+
}, 'mobileSuggestionBlocker', true);
|
|
2039
|
+
} catch (e) {
|
|
2040
|
+
console.error('[ep_data_tables:mobileSuggestionBlocker] Error inserting space:', e);
|
|
2041
|
+
}
|
|
2042
|
+
}, 0);
|
|
2043
|
+
};
|
|
2044
|
+
|
|
2045
|
+
// Listen in capture phase so we win the race with browser/default handlers
|
|
2046
|
+
if ($inner && $inner.length > 0 && $inner[0].addEventListener) {
|
|
2047
|
+
$inner[0].addEventListener('beforeinput', mobileSuggestionBlocker, true);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Fallback for Safari iOS which may not emit beforeinput reliably: use input
|
|
2051
|
+
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);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Android legacy fallback: some keyboards dispatch 'textInput' instead of beforeinput
|
|
2061
|
+
if (isAndroidUA && isAndroidUA() && $inner && $inner.length > 0 && $inner[0].addEventListener) {
|
|
2062
|
+
$inner[0].addEventListener('textInput', (evt) => {
|
|
2063
|
+
const s = typeof evt.data === 'string' ? evt.data : '';
|
|
2064
|
+
if (s && s.length > 1) {
|
|
2065
|
+
mobileSuggestionBlocker({
|
|
2066
|
+
inputType: 'insertText',
|
|
2067
|
+
isComposing: false,
|
|
2068
|
+
data: s,
|
|
2069
|
+
preventDefault: () => { try { evt.preventDefault(); } catch (_) {} },
|
|
2070
|
+
stopImmediatePropagation: () => { try { evt.stopImmediatePropagation(); } catch (_) {} },
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
}, true);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// Reduce chance of autocorrect/spellcheck kicking in at all
|
|
2077
|
+
try {
|
|
2078
|
+
const disableAuto = (el) => {
|
|
2079
|
+
if (!el) return;
|
|
2080
|
+
el.setAttribute('autocorrect', 'off');
|
|
2081
|
+
el.setAttribute('autocomplete', 'off');
|
|
2082
|
+
el.setAttribute('autocapitalize', 'off');
|
|
2083
|
+
el.setAttribute('spellcheck', 'false');
|
|
2084
|
+
};
|
|
2085
|
+
disableAuto(innerDocBody[0] || innerDocBody);
|
|
2086
|
+
} catch (_) {}
|
|
1954
2087
|
} catch (e) {
|
|
1955
2088
|
console.error(`${callWithAceLogPrefix} ERROR: Exception while trying to find inner iframe body:`, e);
|
|
1956
2089
|
// log(`${callWithAceLogPrefix} Exception details:`, { message: e.message, stack: e.stack });
|
|
@@ -2488,17 +2621,60 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2488
2621
|
currentOffset += cellLength + DELIMITER.length;
|
|
2489
2622
|
}
|
|
2490
2623
|
|
|
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
2624
|
// If selection spills outside the cell boundaries, block to protect structure
|
|
2497
2625
|
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
|
|
2498
2626
|
evt.preventDefault();
|
|
2499
2627
|
return;
|
|
2500
2628
|
}
|
|
2501
2629
|
|
|
2630
|
+
// iOS soft break (insertParagraph/insertLineBreak) → replace with plain space via Ace
|
|
2631
|
+
if (inputType === 'insertParagraph' || inputType === 'insertLineBreak') {
|
|
2632
|
+
evt.preventDefault();
|
|
2633
|
+
evt.stopPropagation();
|
|
2634
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2635
|
+
setTimeout(() => {
|
|
2636
|
+
try {
|
|
2637
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2638
|
+
aceInstance.ace_fastIncorp(10);
|
|
2639
|
+
const freshRep = aceInstance.ace_getRep();
|
|
2640
|
+
const freshSelStart = freshRep.selStart;
|
|
2641
|
+
const freshSelEnd = freshRep.selEnd;
|
|
2642
|
+
// Replace soft break with space
|
|
2643
|
+
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, ' ');
|
|
2644
|
+
|
|
2645
|
+
// Apply td attr to inserted space
|
|
2646
|
+
const afterRep = aceInstance.ace_getRep();
|
|
2647
|
+
const maxLen = Math.max(0, afterRep.lines.atIndex(lineNum)?.text?.length || 0);
|
|
2648
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
2649
|
+
const endCol = Math.min(startCol + 1, maxLen);
|
|
2650
|
+
if (endCol > startCol) {
|
|
2651
|
+
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
2652
|
+
[lineNum, startCol], [lineNum, endCol], [[ATTR_CELL, String(targetCellIndex)]]
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
// Re-apply line metadata
|
|
2657
|
+
ed.ep_data_tables_applyMeta(
|
|
2658
|
+
lineNum, tableMetadata.tblId, tableMetadata.row, tableMetadata.cols,
|
|
2659
|
+
afterRep, ed, null, docManager
|
|
2660
|
+
);
|
|
2661
|
+
|
|
2662
|
+
const newCaretPos = [lineNum, endCol];
|
|
2663
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2664
|
+
aceInstance.ace_fastIncorp(10);
|
|
2665
|
+
}, 'iosSoftBreakToSpace', true);
|
|
2666
|
+
} catch (e) {
|
|
2667
|
+
console.error(`${autoLogPrefix} ERROR replacing soft break:`, e);
|
|
2668
|
+
}
|
|
2669
|
+
}, 0);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
// Clamp selection end if it includes a trailing delimiter of the same cell
|
|
2674
|
+
if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
|
|
2675
|
+
selEnd[1] = cellEndCol;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2502
2678
|
// Handle line breaks by routing to Enter behavior
|
|
2503
2679
|
if (inputType === 'insertParagraph' || inputType === 'insertLineBreak') {
|
|
2504
2680
|
evt.preventDefault();
|
|
@@ -2612,6 +2788,297 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2612
2788
|
}
|
|
2613
2789
|
});
|
|
2614
2790
|
|
|
2791
|
+
// Desktop (non-Android/iOS) – sanitize NBSP and table delimiter on any insert* beforeinput
|
|
2792
|
+
$inner.on('beforeinput', (evt) => {
|
|
2793
|
+
const genericLogPrefix = '[ep_data_tables:beforeinputInsertTextGeneric]';
|
|
2794
|
+
const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
|
|
2795
|
+
// log diagnostics for all insert* types on table lines
|
|
2796
|
+
if (!inputType || !inputType.startsWith('insert')) return;
|
|
2797
|
+
|
|
2798
|
+
// Skip on Android or iOS (they have dedicated handlers above)
|
|
2799
|
+
if (isAndroidUA() || isIOSUA()) return;
|
|
2800
|
+
|
|
2801
|
+
const rep = ed.ace_getRep();
|
|
2802
|
+
if (!rep || !rep.selStart) return;
|
|
2803
|
+
const selStart = rep.selStart;
|
|
2804
|
+
const selEnd = rep.selEnd;
|
|
2805
|
+
const lineNum = selStart[0];
|
|
2806
|
+
|
|
2807
|
+
// Ensure we are inside a table line by resolving metadata
|
|
2808
|
+
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2809
|
+
let tableMetadata = null;
|
|
2810
|
+
if (lineAttrString) {
|
|
2811
|
+
try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {}
|
|
2812
|
+
}
|
|
2813
|
+
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2814
|
+
if (!tableMetadata || typeof tableMetadata.cols !== 'number') return; // not a table line
|
|
2815
|
+
console.debug(`${genericLogPrefix} event`, { inputType, data: evt.originalEvent && evt.originalEvent.data });
|
|
2816
|
+
|
|
2817
|
+
// Treat null data (composition/IME) as a single space to prevent NBSP/BR side effects
|
|
2818
|
+
const rawData = evt.originalEvent && typeof evt.originalEvent.data === 'string' ? evt.originalEvent.data : ' ';
|
|
2819
|
+
|
|
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, ' ');
|
|
2826
|
+
|
|
2827
|
+
if (!insertedText) { evt.preventDefault(); return; }
|
|
2828
|
+
|
|
2829
|
+
// Compute cell boundaries to keep selection within current cell
|
|
2830
|
+
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2831
|
+
const cells = lineText.split(DELIMITER);
|
|
2832
|
+
let currentOffset = 0;
|
|
2833
|
+
let targetCellIndex = -1;
|
|
2834
|
+
let cellStartCol = 0;
|
|
2835
|
+
let cellEndCol = 0;
|
|
2836
|
+
for (let i = 0; i < cells.length; i++) {
|
|
2837
|
+
const len = cells[i]?.length ?? 0;
|
|
2838
|
+
const end = currentOffset + len;
|
|
2839
|
+
if (selStart[1] >= currentOffset && selStart[1] <= end) {
|
|
2840
|
+
targetCellIndex = i;
|
|
2841
|
+
cellStartCol = currentOffset;
|
|
2842
|
+
cellEndCol = end;
|
|
2843
|
+
break;
|
|
2844
|
+
}
|
|
2845
|
+
currentOffset += len + DELIMITER.length;
|
|
2846
|
+
}
|
|
2847
|
+
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) { evt.preventDefault(); console.debug(`${genericLogPrefix} abort: selection outside cell`, { selStart, selEnd, cellStartCol, cellEndCol }); return; }
|
|
2848
|
+
|
|
2849
|
+
evt.preventDefault();
|
|
2850
|
+
evt.stopPropagation();
|
|
2851
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2852
|
+
|
|
2853
|
+
try {
|
|
2854
|
+
ed.ace_callWithAce((ace) => {
|
|
2855
|
+
ace.ace_fastIncorp(10);
|
|
2856
|
+
const freshRep = ace.ace_getRep();
|
|
2857
|
+
const freshSelStart = freshRep.selStart;
|
|
2858
|
+
const freshSelEnd = freshRep.selEnd;
|
|
2859
|
+
|
|
2860
|
+
ace.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
|
|
2861
|
+
|
|
2862
|
+
// Re-apply td attribute on inserted range
|
|
2863
|
+
const afterRep = ace.ace_getRep();
|
|
2864
|
+
const lineEntry = afterRep.lines.atIndex(lineNum);
|
|
2865
|
+
const maxLen = lineEntry ? lineEntry.text.length : 0;
|
|
2866
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
2867
|
+
const endCol = Math.min(startCol + insertedText.length, maxLen);
|
|
2868
|
+
if (endCol > startCol) {
|
|
2869
|
+
ace.ace_performDocumentApplyAttributesToRange([lineNum, startCol], [lineNum, endCol], [[ATTR_CELL, String(targetCellIndex)]]);
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// Re-apply tbljson line attribute
|
|
2873
|
+
ed.ep_data_tables_applyMeta(lineNum, tableMetadata.tblId, tableMetadata.row, tableMetadata.cols, afterRep, ed, null, docManager);
|
|
2874
|
+
|
|
2875
|
+
// Move caret to end of inserted text
|
|
2876
|
+
const newCaretPos = [lineNum, endCol];
|
|
2877
|
+
ace.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2878
|
+
}, 'tableGenericInsertText', true);
|
|
2879
|
+
} catch (e) {
|
|
2880
|
+
console.error(`${genericLogPrefix} ERROR handling generic insertText:`, e);
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
|
|
2884
|
+
// iOS (all browsers) – intercept autocorrect/IME commit replacements to protect table structure
|
|
2885
|
+
$inner.on('beforeinput', (evt) => {
|
|
2886
|
+
const autoLogPrefix = '[ep_data_tables:beforeinputAutoReplaceHandler]';
|
|
2887
|
+
const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
|
|
2888
|
+
|
|
2889
|
+
// Only handle on iOS family UAs
|
|
2890
|
+
if (!isIOSUA()) return;
|
|
2891
|
+
|
|
2892
|
+
// Get current selection and ensure we are inside a table line (define before computing hasSelection)
|
|
2893
|
+
const rep = ed.ace_getRep();
|
|
2894
|
+
if (!rep || !rep.selStart) return;
|
|
2895
|
+
const selStart = rep.selStart;
|
|
2896
|
+
const selEnd = rep.selEnd;
|
|
2897
|
+
const lineNum = selStart[0];
|
|
2898
|
+
|
|
2899
|
+
// Intercept replacement/IME commit types; also catch iOS insertText when it behaves like autocorrect
|
|
2900
|
+
const dataStr = (evt.originalEvent && typeof evt.originalEvent.data === 'string') ? evt.originalEvent.data : '';
|
|
2901
|
+
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)
|
|
2904
|
+
const shouldHandle = INPUTTYPE_REPLACEMENT_TYPES.has(inputType) || looksLikeIOSAutoReplace || (inputType === 'insertText' && (hasSelection || insertTextNull));
|
|
2905
|
+
if (!shouldHandle) return;
|
|
2906
|
+
|
|
2907
|
+
// Resolve table metadata for this line
|
|
2908
|
+
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2909
|
+
let tableMetadata = null;
|
|
2910
|
+
if (lineAttrString) {
|
|
2911
|
+
try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {}
|
|
2912
|
+
}
|
|
2913
|
+
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2914
|
+
if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
|
|
2915
|
+
return; // not a table line
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
// Compute cell boundaries and target cell index
|
|
2919
|
+
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2920
|
+
const cells = lineText.split(DELIMITER);
|
|
2921
|
+
let currentOffset = 0;
|
|
2922
|
+
let targetCellIndex = -1;
|
|
2923
|
+
let cellStartCol = 0;
|
|
2924
|
+
let cellEndCol = 0;
|
|
2925
|
+
for (let i = 0; i < cells.length; i++) {
|
|
2926
|
+
const cellLength = cells[i]?.length ?? 0;
|
|
2927
|
+
const cellEndColThisIteration = currentOffset + cellLength;
|
|
2928
|
+
if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
|
|
2929
|
+
targetCellIndex = i;
|
|
2930
|
+
cellStartCol = currentOffset;
|
|
2931
|
+
cellEndCol = cellEndColThisIteration;
|
|
2932
|
+
break;
|
|
2933
|
+
}
|
|
2934
|
+
currentOffset += cellLength + DELIMITER.length;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
// Clamp selection end if it includes a trailing delimiter of the same cell
|
|
2938
|
+
if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
|
|
2939
|
+
selEnd[1] = cellEndCol;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// If selection spills outside the cell boundaries, block to protect structure
|
|
2943
|
+
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
|
|
2944
|
+
evt.preventDefault();
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// Replacement text payload from beforeinput
|
|
2949
|
+
let insertedText = dataStr;
|
|
2950
|
+
if (!insertedText) {
|
|
2951
|
+
if (insertTextNull) {
|
|
2952
|
+
// Predictive text commit: Safari already mutated DOM ( <br>). Block and repair via Ace.
|
|
2953
|
+
evt.preventDefault();
|
|
2954
|
+
evt.stopPropagation();
|
|
2955
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2956
|
+
|
|
2957
|
+
setTimeout(() => {
|
|
2958
|
+
try {
|
|
2959
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2960
|
+
aceInstance.ace_fastIncorp(10);
|
|
2961
|
+
const freshRep = aceInstance.ace_getRep();
|
|
2962
|
+
const freshSelStart = freshRep.selStart;
|
|
2963
|
+
const freshSelEnd = freshRep.selEnd;
|
|
2964
|
+
// Insert a plain space
|
|
2965
|
+
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, ' ');
|
|
2966
|
+
|
|
2967
|
+
// Re-apply cell attribute to the single inserted char
|
|
2968
|
+
const afterRep = aceInstance.ace_getRep();
|
|
2969
|
+
const maxLen = Math.max(0, afterRep.lines.atIndex(lineNum)?.text?.length || 0);
|
|
2970
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
2971
|
+
const endCol = Math.min(startCol + 1, maxLen);
|
|
2972
|
+
if (endCol > startCol) {
|
|
2973
|
+
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
2974
|
+
[lineNum, startCol], [lineNum, endCol], [[ATTR_CELL, String(targetCellIndex)]]
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
// Re-apply table metadata line attribute
|
|
2979
|
+
ed.ep_data_tables_applyMeta(
|
|
2980
|
+
lineNum, tableMetadata.tblId, tableMetadata.row, tableMetadata.cols,
|
|
2981
|
+
afterRep, ed, null, docManager
|
|
2982
|
+
);
|
|
2983
|
+
|
|
2984
|
+
// Move caret to end of inserted text
|
|
2985
|
+
const newCaretPos = [lineNum, endCol];
|
|
2986
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2987
|
+
aceInstance.ace_fastIncorp(10);
|
|
2988
|
+
}, 'iosPredictiveCommit', true);
|
|
2989
|
+
} catch (e) {
|
|
2990
|
+
console.error(`${autoLogPrefix} ERROR fixing predictive commit:`, e);
|
|
2991
|
+
}
|
|
2992
|
+
}, 0);
|
|
2993
|
+
return;
|
|
2994
|
+
} else {
|
|
2995
|
+
// No payload and not special: if it's a replacement, block default to avoid DOM mutation
|
|
2996
|
+
if (INPUTTYPE_REPLACEMENT_TYPES.has(inputType) || hasSelection) {
|
|
2997
|
+
evt.preventDefault();
|
|
2998
|
+
evt.stopPropagation();
|
|
2999
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
3000
|
+
}
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
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, ' ');
|
|
3010
|
+
|
|
3011
|
+
// Intercept the browser default and perform the edit via Ace APIs
|
|
3012
|
+
evt.preventDefault();
|
|
3013
|
+
evt.stopPropagation();
|
|
3014
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
3015
|
+
|
|
3016
|
+
try {
|
|
3017
|
+
setTimeout(() => {
|
|
3018
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
3019
|
+
aceInstance.ace_fastIncorp(10);
|
|
3020
|
+
const freshRep = aceInstance.ace_getRep();
|
|
3021
|
+
const freshSelStart = freshRep.selStart;
|
|
3022
|
+
const freshSelEnd = freshRep.selEnd;
|
|
3023
|
+
|
|
3024
|
+
// Replace selection with sanitized text
|
|
3025
|
+
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
|
|
3026
|
+
|
|
3027
|
+
// Re-apply cell attribute to the newly inserted text (clamped to line length)
|
|
3028
|
+
const repAfterReplace = aceInstance.ace_getRep();
|
|
3029
|
+
const freshLineIndex = freshSelStart[0];
|
|
3030
|
+
const freshLineEntry = repAfterReplace.lines.atIndex(freshLineIndex);
|
|
3031
|
+
const maxLen = Math.max(0, (freshLineEntry && freshLineEntry.text) ? freshLineEntry.text.length : 0);
|
|
3032
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
3033
|
+
const endColRaw = startCol + insertedText.length;
|
|
3034
|
+
const endCol = Math.min(endColRaw, maxLen);
|
|
3035
|
+
if (endCol > startCol) {
|
|
3036
|
+
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
3037
|
+
[freshLineIndex, startCol], [freshLineIndex, endCol], [[ATTR_CELL, String(targetCellIndex)]]
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
// Re-apply table metadata line attribute
|
|
3042
|
+
ed.ep_data_tables_applyMeta(
|
|
3043
|
+
freshLineIndex,
|
|
3044
|
+
tableMetadata.tblId,
|
|
3045
|
+
tableMetadata.row,
|
|
3046
|
+
tableMetadata.cols,
|
|
3047
|
+
repAfterReplace,
|
|
3048
|
+
ed,
|
|
3049
|
+
null,
|
|
3050
|
+
docManager
|
|
3051
|
+
);
|
|
3052
|
+
|
|
3053
|
+
// Move caret to end of inserted text and update last-click state
|
|
3054
|
+
const newCaretCol = endCol;
|
|
3055
|
+
const newCaretPos = [freshLineIndex, newCaretCol];
|
|
3056
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
3057
|
+
aceInstance.ace_fastIncorp(10);
|
|
3058
|
+
|
|
3059
|
+
if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
3060
|
+
// Recompute cellStartCol for fresh line
|
|
3061
|
+
const freshLineText = (freshLineEntry && freshLineEntry.text) || '';
|
|
3062
|
+
const freshCells = freshLineText.split(DELIMITER);
|
|
3063
|
+
let freshOffset = 0;
|
|
3064
|
+
for (let i = 0; i < targetCellIndex; i++) {
|
|
3065
|
+
freshOffset += (freshCells[i]?.length ?? 0) + DELIMITER.length;
|
|
3066
|
+
}
|
|
3067
|
+
const newRelativePos = newCaretCol - freshOffset;
|
|
3068
|
+
editor.ep_data_tables_last_clicked = {
|
|
3069
|
+
lineNum: freshLineIndex,
|
|
3070
|
+
tblId: tableMetadata.tblId,
|
|
3071
|
+
cellIndex: targetCellIndex,
|
|
3072
|
+
relativePos: newRelativePos < 0 ? 0 : newRelativePos,
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
}, 'tableAutoReplaceTextOperations', true);
|
|
3076
|
+
}, 0);
|
|
3077
|
+
} catch (error) {
|
|
3078
|
+
console.error(`${autoLogPrefix} ERROR during auto-replace handling:`, error);
|
|
3079
|
+
}
|
|
3080
|
+
});
|
|
3081
|
+
|
|
2615
3082
|
// Composition start marker (Android Chrome/table only)
|
|
2616
3083
|
$inner.on('compositionstart', (evt) => {
|
|
2617
3084
|
if (!isAndroidUA()) return;
|
|
@@ -2626,7 +3093,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2626
3093
|
handledCurrentComposition = false;
|
|
2627
3094
|
suppressBeforeInputInsertTextDuringComposition = false;
|
|
2628
3095
|
});
|
|
2629
|
-
|
|
2630
3096
|
// Android Chrome composition handling for whitespace (space) to prevent DOM mutation breaking delimiters
|
|
2631
3097
|
$inner.on('compositionupdate', (evt) => {
|
|
2632
3098
|
const compLogPrefix = '[ep_data_tables:compositionHandler]';
|
|
@@ -4122,7 +4588,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4122
4588
|
return false;
|
|
4123
4589
|
}
|
|
4124
4590
|
}
|
|
4125
|
-
|
|
4126
4591
|
function deleteTableColumnWithText(tableLines, targetColIndex, editorInfo, docManager) {
|
|
4127
4592
|
try {
|
|
4128
4593
|
// Update text content for all table lines using precise character deletion
|