ep_data_tables 0.0.2 → 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/collectContentPre.js +4 -0
- package/ep.json +0 -2
- package/package.json +1 -1
- package/static/js/client_hooks.js +628 -295
- package/static/js/collector_hooks.js +0 -13
package/collectContentPre.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Ensure settings.toolbar exists early to avoid load-order error with ep_font_color
|
|
4
|
+
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
|
5
|
+
if (!settings.toolbar) settings.toolbar = {};
|
|
6
|
+
|
|
3
7
|
// Using console.log for server-side logging, similar to other Etherpad server-side files.
|
|
4
8
|
const log = (...m) => console.log('[ep_data_tables:collectContentPre_SERVER]', ...m);
|
|
5
9
|
|
package/ep.json
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
},
|
|
11
11
|
"client_hooks": {
|
|
12
12
|
"collectContentPre" : "ep_data_tables/static/js/client_hooks:collectContentPre",
|
|
13
|
-
"collectContentLineBreak": "ep_data_tables/static/js/collector_hooks:collectContentLineBreak",
|
|
14
|
-
"collectContentLineText" : "ep_data_tables/static/js/collector_hooks:collectContentLineText",
|
|
15
13
|
"aceKeyEvent" : "ep_data_tables/static/js/client_hooks:aceKeyEvent",
|
|
16
14
|
"aceStartLineAndCharForPoint" : "ep_data_tables/static/js/client_hooks:aceStartLineAndCharForPoint",
|
|
17
15
|
"aceEndLineAndCharForPoint" : "ep_data_tables/static/js/client_hooks:aceEndLineAndCharForPoint",
|
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
|
|
|
@@ -472,6 +487,12 @@ exports.collectContentPre = (hook, ctx) => {
|
|
|
472
487
|
// the span would be mistaken for a real cell boundary.
|
|
473
488
|
const hiddenDelimRegexPrimary = /<span class="ep-data_tables-delim"[^>]*>.*?<\/span>/ig;
|
|
474
489
|
segmentHTML = segmentHTML.replace(hiddenDelimRegexPrimary, '');
|
|
490
|
+
// Remove caret-anchor spans (invisible, non-semantic)
|
|
491
|
+
const caretAnchorRegex = /<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig;
|
|
492
|
+
segmentHTML = segmentHTML.replace(caretAnchorRegex, '');
|
|
493
|
+
// If, after stripping tags/entities, the content is empty, serialize as empty string
|
|
494
|
+
const textCheck = segmentHTML.replace(/<[^>]*>/g, '').replace(/ /ig, ' ').trim();
|
|
495
|
+
if (textCheck === '') segmentHTML = '';
|
|
475
496
|
|
|
476
497
|
const hidden = index === 0 ? '' :
|
|
477
498
|
/* keep the char in the DOM but make it visually disappear and non-editable */
|
|
@@ -482,7 +503,7 @@ exports.collectContentPre = (hook, ctx) => {
|
|
|
482
503
|
|
|
483
504
|
if (cellHTMLSegments.length !== existingMetadata.cols) {
|
|
484
505
|
// log(`${funcName}: WARNING Line ${lineNum}: Reconstructed cell count (${cellHTMLSegments.length}) does not match metadata cols (${existingMetadata.cols}). Padding/truncating.`);
|
|
485
|
-
while (cellHTMLSegments.length < existingMetadata.cols) cellHTMLSegments.push('
|
|
506
|
+
while (cellHTMLSegments.length < existingMetadata.cols) cellHTMLSegments.push('');
|
|
486
507
|
if (cellHTMLSegments.length > existingMetadata.cols) cellHTMLSegments.length = existingMetadata.cols;
|
|
487
508
|
}
|
|
488
509
|
|
|
@@ -520,15 +541,21 @@ exports.collectContentPre = (hook, ctx) => {
|
|
|
520
541
|
const resizeHandleRegex = /<div class="ep-data_tables-resize-handle"[^>]*><\/div>/ig;
|
|
521
542
|
segmentHTML = segmentHTML.replace(resizeHandleRegex, '');
|
|
522
543
|
if (index > 0) {
|
|
523
|
-
const hiddenDelimRegex = new RegExp(
|
|
544
|
+
const hiddenDelimRegex = new RegExp('^<span class="ep-data_tables-delim" contenteditable="false">' + DELIMITER + '(<\\/span>)?<\\/span>', 'i');
|
|
524
545
|
segmentHTML = segmentHTML.replace(hiddenDelimRegex, '');
|
|
525
546
|
}
|
|
547
|
+
// Remove caret-anchor spans (invisible, non-semantic)
|
|
548
|
+
const caretAnchorRegex = /<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig;
|
|
549
|
+
segmentHTML = segmentHTML.replace(caretAnchorRegex, '');
|
|
550
|
+
// If, after stripping tags/entities, the content is empty, serialize as empty string
|
|
551
|
+
const textCheck = segmentHTML.replace(/<[^>]*>/g, '').replace(/ /ig, ' ').trim();
|
|
552
|
+
if (textCheck === '') segmentHTML = '';
|
|
526
553
|
return segmentHTML;
|
|
527
554
|
});
|
|
528
555
|
|
|
529
556
|
if (cellHTMLSegments.length !== domCols) {
|
|
530
557
|
// log(`${funcName}: WARNING Line ${lineNum} (Fallback): Reconstructed cell count (${cellHTMLSegments.length}) does not match DOM cols (${domCols}).`);
|
|
531
|
-
while(cellHTMLSegments.length < domCols) cellHTMLSegments.push('
|
|
558
|
+
while(cellHTMLSegments.length < domCols) cellHTMLSegments.push('');
|
|
532
559
|
if(cellHTMLSegments.length > domCols) cellHTMLSegments.length = domCols;
|
|
533
560
|
}
|
|
534
561
|
|
|
@@ -672,24 +699,41 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
|
|
|
672
699
|
// Basic styling - can be moved to CSS later
|
|
673
700
|
const tdStyle = `padding: 5px 7px; word-wrap:break-word; vertical-align: top; border: 1px solid #000; position: relative;`; // Added position: relative
|
|
674
701
|
|
|
702
|
+
// Precompute encoded tbljson class so empty cells can carry the same marker
|
|
703
|
+
let encodedTbljsonClass = '';
|
|
704
|
+
try {
|
|
705
|
+
encodedTbljsonClass = `tbljson-${enc(JSON.stringify(metadata))}`;
|
|
706
|
+
} catch (_) { encodedTbljsonClass = ''; }
|
|
707
|
+
|
|
675
708
|
// Map the HTML segments directly into TD elements with column widths
|
|
676
709
|
const cellsHtml = innerHTMLSegments.map((segment, index) => {
|
|
677
710
|
// Build the hidden delimiter *inside* the first author span so the caret
|
|
678
|
-
// cannot sit between delimiter and text.
|
|
679
|
-
|
|
711
|
+
// cannot sit between delimiter and text. For empty cells, synthesize a span
|
|
712
|
+
// that carries tbljson and tblCell-N so caret anchoring remains stable.
|
|
713
|
+
const textOnly = (segment || '').replace(/<[^>]*>/g, '').replace(/ /ig, ' ').trim();
|
|
714
|
+
let modifiedSegment = segment || '';
|
|
715
|
+
const isEmpty = !segment || textOnly === '';
|
|
716
|
+
if (isEmpty) {
|
|
717
|
+
const cellClass = encodedTbljsonClass ? `${encodedTbljsonClass} tblCell-${index}` : `tblCell-${index}`;
|
|
718
|
+
modifiedSegment = `<span class="${cellClass}"> </span>`;
|
|
719
|
+
}
|
|
680
720
|
if (index > 0) {
|
|
681
721
|
const delimSpan = `<span class="ep-data_tables-delim" contenteditable="false">${HIDDEN_DELIM}</span>`;
|
|
682
722
|
// If the rendered segment already starts with a <span …> (which will be
|
|
683
723
|
// the usual author-colour wrapper) inject the delimiter right after that
|
|
684
724
|
// opening tag; otherwise just prefix it.
|
|
685
725
|
modifiedSegment = modifiedSegment.replace(/^(<span[^>]*>)/i, `$1${delimSpan}`);
|
|
686
|
-
if (modifiedSegment
|
|
726
|
+
if (!/^<span[^>]*>/i.test(modifiedSegment)) modifiedSegment = `${delimSpan}${modifiedSegment}`;
|
|
687
727
|
}
|
|
688
728
|
|
|
689
|
-
// --- NEW: Always embed the invisible caret-anchor as *last* child *within* the first
|
|
729
|
+
// --- NEW: Always embed the invisible caret-anchor as *last* child *within* the first span ---
|
|
690
730
|
const caretAnchorSpan = '<span class="ep-data_tables-caret-anchor" contenteditable="false"></span>';
|
|
691
731
|
const anchorInjected = modifiedSegment.replace(/<\/span>\s*$/i, `${caretAnchorSpan}</span>`);
|
|
692
|
-
modifiedSegment = (anchorInjected !== modifiedSegment)
|
|
732
|
+
modifiedSegment = (anchorInjected !== modifiedSegment)
|
|
733
|
+
? anchorInjected
|
|
734
|
+
: (isEmpty
|
|
735
|
+
? `<span class="${encodedTbljsonClass ? `${encodedTbljsonClass} ` : ''}tblCell-${index}">${modifiedSegment}${caretAnchorSpan}</span>`
|
|
736
|
+
: `${modifiedSegment}${caretAnchorSpan}`);
|
|
693
737
|
|
|
694
738
|
// Width & other decorations remain unchanged
|
|
695
739
|
const widthPercent = columnWidths[index] || (100 / numCols);
|
|
@@ -907,7 +951,7 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
907
951
|
const delimiterCount = (delimitedTextFromLine || '').split(DELIMITER).length - 1;
|
|
908
952
|
// log(`${logPrefix} NodeID#${nodeId}: Delimiter '${DELIMITER}' count in innerHTML: ${delimiterCount}`);
|
|
909
953
|
// log(`${logPrefix} NodeID#${nodeId}: Expected delimiters for ${rowMetadata.cols} columns: ${rowMetadata.cols - 1}`);
|
|
910
|
-
|
|
954
|
+
|
|
911
955
|
// log all delimiter positions
|
|
912
956
|
let pos = -1;
|
|
913
957
|
const delimiterPositions = [];
|
|
@@ -922,7 +966,10 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
922
966
|
// the embedded delimiter character they carry doesn't inflate or shrink
|
|
923
967
|
// the segment count.
|
|
924
968
|
const spanDelimRegex = new RegExp('<span class="ep-data_tables-delim"[^>]*>' + DELIMITER + '<\\/span>', 'ig');
|
|
925
|
-
const sanitizedHTMLForSplit = (delimitedTextFromLine || '')
|
|
969
|
+
const sanitizedHTMLForSplit = (delimitedTextFromLine || '')
|
|
970
|
+
.replace(spanDelimRegex, '')
|
|
971
|
+
// strip caret anchors from raw line html before split
|
|
972
|
+
.replace(/<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig, '');
|
|
926
973
|
const htmlSegments = sanitizedHTMLForSplit.split(DELIMITER);
|
|
927
974
|
|
|
928
975
|
// log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT ANALYSIS ***`);
|
|
@@ -949,57 +996,83 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
949
996
|
let finalHtmlSegments = htmlSegments;
|
|
950
997
|
|
|
951
998
|
if (htmlSegments.length !== rowMetadata.cols) {
|
|
952
|
-
// log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH DETECTED
|
|
953
|
-
// log(`${logPrefix} NodeID#${nodeId}: WARNING - Parsed segment count (${htmlSegments.length}) does not match metadata cols (${rowMetadata.cols}). Auto-reconstructing table structure.`);
|
|
954
|
-
console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Parsed segment count (${htmlSegments.length}) mismatch with metadata cols (${rowMetadata.cols}). Segments:`, htmlSegments);
|
|
955
|
-
|
|
956
|
-
// *** ENHANCED DEBUG: Analyze why we have a mismatch ***
|
|
957
|
-
// log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH ANALYSIS ***`);
|
|
958
|
-
// log(`${logPrefix} NodeID#${nodeId}: Expected columns: ${rowMetadata.cols}`);
|
|
959
|
-
// log(`${logPrefix} NodeID#${nodeId}: Actual segments: ${htmlSegments.length}`);
|
|
960
|
-
// log(`${logPrefix} NodeID#${nodeId}: Delimiter count found: ${delimiterCount}`);
|
|
961
|
-
// log(`${logPrefix} NodeID#${nodeId}: Expected delimiter count: ${rowMetadata.cols - 1}`);
|
|
999
|
+
// log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH DETECTED *** - Attempting reconstruction.`);
|
|
962
1000
|
|
|
963
1001
|
// Check if this is an image selection issue
|
|
964
1002
|
const hasImageSelected = delimitedTextFromLine.includes('currently-selected');
|
|
965
1003
|
const hasImageContent = delimitedTextFromLine.includes('image:');
|
|
966
|
-
// log(`${logPrefix} NodeID#${nodeId}: Has selected image: ${hasImageSelected}`);
|
|
967
|
-
// log(`${logPrefix} NodeID#${nodeId}: Has image content: ${hasImageContent}`);
|
|
968
|
-
|
|
969
1004
|
if (hasImageSelected) {
|
|
970
1005
|
// log(`${logPrefix} NodeID#${nodeId}: *** POTENTIAL CAUSE: Image selection state may be affecting segment parsing ***`);
|
|
971
1006
|
}
|
|
972
|
-
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1007
|
+
|
|
1008
|
+
// First attempt: reconstruct using DOM spans that carry tblCell-N classes
|
|
1009
|
+
let usedClassReconstruction = false;
|
|
1010
|
+
try {
|
|
1011
|
+
const cols = Math.max(0, Number(rowMetadata.cols) || 0);
|
|
1012
|
+
const grouped = Array.from({ length: cols }, () => '');
|
|
1013
|
+
const candidates = Array.from(node.querySelectorAll('[class*="tblCell-"]'));
|
|
1014
|
+
|
|
1015
|
+
const classNum = (el) => {
|
|
1016
|
+
if (!el || !el.classList) return -1;
|
|
1017
|
+
for (const cls of el.classList) {
|
|
1018
|
+
const m = /^tblCell-(\d+)$/.exec(cls);
|
|
1019
|
+
if (m) return parseInt(m[1], 10);
|
|
1020
|
+
}
|
|
1021
|
+
return -1;
|
|
1022
|
+
};
|
|
1023
|
+
const hasAncestorWithSameCell = (el, n) => {
|
|
1024
|
+
let p = el?.parentElement;
|
|
1025
|
+
while (p) {
|
|
1026
|
+
if (p.classList && p.classList.contains(`tblCell-${n}`)) return true;
|
|
1027
|
+
p = p.parentElement;
|
|
1028
|
+
}
|
|
1029
|
+
return false;
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
for (const el of candidates) {
|
|
1033
|
+
const n = classNum(el);
|
|
1034
|
+
if (n >= 0 && n < cols) {
|
|
1035
|
+
if (!hasAncestorWithSameCell(el, n)) {
|
|
1036
|
+
grouped[n] += el.outerHTML || '';
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const usable = grouped.some(s => s && s.trim() !== '');
|
|
1041
|
+
if (usable) {
|
|
1042
|
+
finalHtmlSegments = grouped.map(s => (s && s.trim() !== '') ? s : ' ');
|
|
1043
|
+
usedClassReconstruction = true;
|
|
1044
|
+
console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Reconstructed ${finalHtmlSegments.length} segments from tblCell-N classes.`);
|
|
1045
|
+
}
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
console.debug(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Class-based reconstruction error; falling back.`, e);
|
|
988
1048
|
}
|
|
989
|
-
|
|
1049
|
+
|
|
1050
|
+
// Fallback: reconstruct from string segments
|
|
1051
|
+
if (!usedClassReconstruction) {
|
|
1052
|
+
const reconstructedSegments = [];
|
|
1053
|
+
if (htmlSegments.length === 1 && rowMetadata.cols > 1) {
|
|
1054
|
+
reconstructedSegments.push(htmlSegments[0]);
|
|
1055
|
+
for (let i = 1; i < rowMetadata.cols; i++) {
|
|
1056
|
+
reconstructedSegments.push(' ');
|
|
1057
|
+
}
|
|
1058
|
+
} else if (htmlSegments.length > rowMetadata.cols) {
|
|
1059
|
+
for (let i = 0; i < rowMetadata.cols - 1; i++) {
|
|
1060
|
+
reconstructedSegments.push(htmlSegments[i] || ' ');
|
|
1061
|
+
}
|
|
990
1062
|
const remainingSegments = htmlSegments.slice(rowMetadata.cols - 1);
|
|
991
1063
|
reconstructedSegments.push(remainingSegments.join('|') || ' ');
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
for (let i = 0; i < rowMetadata.cols; i++) {
|
|
996
|
-
reconstructedSegments.push(htmlSegments[i] || ' ');
|
|
1064
|
+
} else {
|
|
1065
|
+
for (let i = 0; i < rowMetadata.cols; i++) {
|
|
1066
|
+
reconstructedSegments.push(htmlSegments[i] || ' ');
|
|
997
1067
|
}
|
|
1068
|
+
}
|
|
1069
|
+
finalHtmlSegments = reconstructedSegments;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Only warn if we still don't have the right number of segments
|
|
1073
|
+
if (finalHtmlSegments.length !== rowMetadata.cols) {
|
|
1074
|
+
console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Could not reconstruct to expected ${rowMetadata.cols} segments. Got ${finalHtmlSegments.length}.`);
|
|
998
1075
|
}
|
|
999
|
-
|
|
1000
|
-
// log(`${logPrefix} NodeID#${nodeId}: Reconstructed ${reconstructedSegments.length} segments to match expected ${rowMetadata.cols} columns.`);
|
|
1001
|
-
finalHtmlSegments = reconstructedSegments;
|
|
1002
|
-
|
|
1003
1076
|
} else {
|
|
1004
1077
|
// log(`${logPrefix} NodeID#${nodeId}: Segment count matches metadata cols (${rowMetadata.cols}). Using original segments.`);
|
|
1005
1078
|
}
|
|
@@ -1533,12 +1606,12 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1533
1606
|
// --- Check for Ctrl+X (Cut) key combination ---
|
|
1534
1607
|
const isCutKey = (evt.ctrlKey || evt.metaKey) && (evt.key === 'x' || evt.key === 'X' || evt.keyCode === 88);
|
|
1535
1608
|
if (isCutKey && hasSelection) {
|
|
1536
|
-
|
|
1609
|
+
log(`${logPrefix} Ctrl+X (Cut) detected with selection. Letting cut event handler manage this.`);
|
|
1537
1610
|
// Let the cut event handler handle this - we don't need to preventDefault here
|
|
1538
1611
|
// as the cut event will handle the operation and prevent default
|
|
1539
1612
|
return false; // Allow the cut event to be triggered
|
|
1540
1613
|
} else if (isCutKey && !hasSelection) {
|
|
1541
|
-
|
|
1614
|
+
log(`${logPrefix} Ctrl+X (Cut) detected but no selection. Allowing default.`);
|
|
1542
1615
|
return false; // Allow default - nothing to cut
|
|
1543
1616
|
}
|
|
1544
1617
|
|
|
@@ -1892,38 +1965,37 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
1892
1965
|
}
|
|
1893
1966
|
|
|
1894
1967
|
// *** CUT EVENT LISTENER ***
|
|
1895
|
-
|
|
1968
|
+
log(`${callWithAceLogPrefix} Attaching cut event listener to $inner (inner iframe body).`);
|
|
1896
1969
|
$inner.on('cut', (evt) => {
|
|
1897
1970
|
const cutLogPrefix = '[ep_data_tables:cutHandler]';
|
|
1898
|
-
|
|
1971
|
+
console.log(`${cutLogPrefix} CUT EVENT TRIGGERED. Event object:`, evt);
|
|
1899
1972
|
|
|
1900
|
-
|
|
1973
|
+
console.log(`${cutLogPrefix} Getting current editor representation (rep).`);
|
|
1901
1974
|
const rep = ed.ace_getRep();
|
|
1902
1975
|
if (!rep || !rep.selStart) {
|
|
1903
|
-
|
|
1976
|
+
console.warn(`${cutLogPrefix} WARNING: Could not get representation or selection. Allowing default cut.`);
|
|
1904
1977
|
console.warn(`${cutLogPrefix} Could not get rep or selStart.`);
|
|
1905
1978
|
return; // Allow default
|
|
1906
1979
|
}
|
|
1907
|
-
|
|
1980
|
+
console.log(`${cutLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
|
|
1908
1981
|
const selStart = rep.selStart;
|
|
1909
1982
|
const selEnd = rep.selEnd;
|
|
1910
1983
|
const lineNum = selStart[0];
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
if (
|
|
1915
|
-
|
|
1916
|
-
return; // Allow default - nothing to cut
|
|
1984
|
+
console.log(`${cutLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
|
|
1985
|
+
// Determine if there is a selection in the editor representation
|
|
1986
|
+
const hasSelectionInRep = !(selStart[0] === selEnd[0] && selStart[1] === selEnd[1]);
|
|
1987
|
+
if (!hasSelectionInRep) {
|
|
1988
|
+
console.log(`${cutLogPrefix} No selection detected in rep; deferring decision until table-line check.`);
|
|
1917
1989
|
}
|
|
1918
1990
|
|
|
1919
1991
|
// Check if selection spans multiple lines
|
|
1920
1992
|
if (selStart[0] !== selEnd[0]) {
|
|
1921
|
-
|
|
1993
|
+
console.warn(`${cutLogPrefix} WARNING: Selection spans multiple lines. Preventing cut to protect table structure.`);
|
|
1922
1994
|
evt.preventDefault();
|
|
1923
1995
|
return;
|
|
1924
1996
|
}
|
|
1925
1997
|
|
|
1926
|
-
|
|
1998
|
+
console.log(`${cutLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
|
|
1927
1999
|
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
1928
2000
|
let tableMetadata = null;
|
|
1929
2001
|
|
|
@@ -1940,11 +2012,18 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
1940
2012
|
}
|
|
1941
2013
|
|
|
1942
2014
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
|
|
1943
|
-
|
|
2015
|
+
console.log(`${cutLogPrefix} Line ${lineNum} is NOT a recognised table line. Allowing default cut.`);
|
|
1944
2016
|
return; // Not a table line
|
|
1945
2017
|
}
|
|
1946
2018
|
|
|
1947
|
-
|
|
2019
|
+
console.log(`${cutLogPrefix} Line ${lineNum} IS a table line. Metadata:`, tableMetadata);
|
|
2020
|
+
|
|
2021
|
+
// If inside a table line but the rep shows no selection, prevent default to protect structure
|
|
2022
|
+
if (!hasSelectionInRep) {
|
|
2023
|
+
console.log(`${cutLogPrefix} Preventing default CUT on table line with collapsed selection to protect delimiters.`);
|
|
2024
|
+
evt.preventDefault();
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
1948
2027
|
|
|
1949
2028
|
// Validate selection is within cell boundaries
|
|
1950
2029
|
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
@@ -1994,37 +2073,37 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
1994
2073
|
selEnd[1] = cellEndCol; // clamp
|
|
1995
2074
|
}
|
|
1996
2075
|
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
|
|
1997
|
-
|
|
2076
|
+
console.warn(`${cutLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing cut to protect table structure.`);
|
|
1998
2077
|
evt.preventDefault();
|
|
1999
2078
|
return;
|
|
2000
2079
|
}
|
|
2001
2080
|
|
|
2002
2081
|
// If we reach here, the selection is entirely within a single cell - allow cut and preserve table structure
|
|
2003
|
-
|
|
2082
|
+
console.log(`${cutLogPrefix} Selection is entirely within cell ${targetCellIndex}. Intercepting cut to preserve table structure.`);
|
|
2004
2083
|
evt.preventDefault();
|
|
2005
2084
|
|
|
2006
2085
|
try {
|
|
2007
2086
|
// Get the selected text to copy to clipboard
|
|
2008
2087
|
const selectedText = lineText.substring(selStart[1], selEnd[1]);
|
|
2009
|
-
|
|
2088
|
+
console.log(`${cutLogPrefix} Selected text to cut: "${selectedText}"`);
|
|
2010
2089
|
|
|
2011
2090
|
// Copy to clipboard manually
|
|
2012
2091
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2013
2092
|
navigator.clipboard.writeText(selectedText).then(() => {
|
|
2014
|
-
|
|
2093
|
+
console.log(`${cutLogPrefix} Successfully copied to clipboard via Navigator API.`);
|
|
2015
2094
|
}).catch((err) => {
|
|
2016
2095
|
console.warn(`${cutLogPrefix} Failed to copy to clipboard via Navigator API:`, err);
|
|
2017
2096
|
});
|
|
2018
2097
|
} else {
|
|
2019
2098
|
// Fallback for older browsers
|
|
2020
|
-
|
|
2099
|
+
console.log(`${cutLogPrefix} Using fallback clipboard method.`);
|
|
2021
2100
|
const textArea = document.createElement('textarea');
|
|
2022
2101
|
textArea.value = selectedText;
|
|
2023
2102
|
document.body.appendChild(textArea);
|
|
2024
2103
|
textArea.select();
|
|
2025
2104
|
try {
|
|
2026
2105
|
document.execCommand('copy');
|
|
2027
|
-
|
|
2106
|
+
console.log(`${cutLogPrefix} Successfully copied to clipboard via execCommand fallback.`);
|
|
2028
2107
|
} catch (err) {
|
|
2029
2108
|
console.warn(`${cutLogPrefix} Failed to copy to clipboard via fallback:`, err);
|
|
2030
2109
|
}
|
|
@@ -2032,14 +2111,14 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2032
2111
|
}
|
|
2033
2112
|
|
|
2034
2113
|
// Now perform the deletion within the cell using ace operations
|
|
2035
|
-
|
|
2114
|
+
console.log(`${cutLogPrefix} Performing deletion via ed.ace_callWithAce.`);
|
|
2036
2115
|
ed.ace_callWithAce((aceInstance) => {
|
|
2037
2116
|
const callAceLogPrefix = `${cutLogPrefix}[ace_callWithAceOps]`;
|
|
2038
|
-
|
|
2117
|
+
console.log(`${callAceLogPrefix} Entered ace_callWithAce for cut operations. selStart:`, selStart, `selEnd:`, selEnd);
|
|
2039
2118
|
|
|
2040
|
-
|
|
2119
|
+
console.log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to delete selected text.`);
|
|
2041
2120
|
aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, '');
|
|
2042
|
-
|
|
2121
|
+
console.log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
|
|
2043
2122
|
|
|
2044
2123
|
// --- Ensure cell is not left empty (zero-length) ---
|
|
2045
2124
|
const repAfterDeletion = aceInstance.ace_getRep();
|
|
@@ -2048,7 +2127,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2048
2127
|
const cellTextAfterDeletion = cellsAfterDeletion[targetCellIndex] || '';
|
|
2049
2128
|
|
|
2050
2129
|
if (cellTextAfterDeletion.length === 0) {
|
|
2051
|
-
|
|
2130
|
+
console.log(`${callAceLogPrefix} Cell ${targetCellIndex} became empty after cut – inserting single space to preserve structure.`);
|
|
2052
2131
|
const insertPos = [lineNum, selStart[1]]; // Start of the now-empty cell
|
|
2053
2132
|
aceInstance.ace_performDocumentReplaceRange(insertPos, insertPos, ' ');
|
|
2054
2133
|
|
|
@@ -2060,9 +2139,9 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2060
2139
|
);
|
|
2061
2140
|
}
|
|
2062
2141
|
|
|
2063
|
-
|
|
2142
|
+
console.log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
|
|
2064
2143
|
const repAfterCut = aceInstance.ace_getRep();
|
|
2065
|
-
|
|
2144
|
+
console.log(`${callAceLogPrefix} Fetched rep after cut for applyMeta. Line ${lineNum} text now: "${repAfterCut.lines.atIndex(lineNum).text}"`);
|
|
2066
2145
|
|
|
2067
2146
|
ed.ep_data_tables_applyMeta(
|
|
2068
2147
|
lineNum,
|
|
@@ -2074,20 +2153,20 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2074
2153
|
null,
|
|
2075
2154
|
docManager
|
|
2076
2155
|
);
|
|
2077
|
-
|
|
2156
|
+
console.log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
|
|
2078
2157
|
|
|
2079
2158
|
const newCaretPos = [lineNum, selStart[1]];
|
|
2080
|
-
|
|
2159
|
+
console.log(`${callAceLogPrefix} Setting caret position to: [${newCaretPos}].`);
|
|
2081
2160
|
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2082
|
-
|
|
2161
|
+
console.log(`${callAceLogPrefix} Selection change successful.`);
|
|
2083
2162
|
|
|
2084
|
-
|
|
2163
|
+
console.log(`${callAceLogPrefix} Cut operations within ace_callWithAce completed successfully.`);
|
|
2085
2164
|
}, 'tableCutTextOperations', true);
|
|
2086
2165
|
|
|
2087
|
-
|
|
2166
|
+
console.log(`${cutLogPrefix} Cut operation completed successfully.`);
|
|
2088
2167
|
} catch (error) {
|
|
2089
2168
|
console.error(`${cutLogPrefix} ERROR during cut operation:`, error);
|
|
2090
|
-
|
|
2169
|
+
console.log(`${cutLogPrefix} Cut operation failed. Error details:`, { message: error.message, stack: error.stack });
|
|
2091
2170
|
}
|
|
2092
2171
|
});
|
|
2093
2172
|
|
|
@@ -2116,10 +2195,99 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2116
2195
|
const lineNum = selStart[0];
|
|
2117
2196
|
// log(`${deleteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
|
|
2118
2197
|
|
|
2119
|
-
//
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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
|
|
2123
2291
|
}
|
|
2124
2292
|
|
|
2125
2293
|
// Check if selection spans multiple lines
|
|
@@ -2273,6 +2441,319 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2273
2441
|
}
|
|
2274
2442
|
});
|
|
2275
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
|
+
|
|
2276
2757
|
// *** DRAG AND DROP EVENT LISTENERS ***
|
|
2277
2758
|
// log(`${callWithAceLogPrefix} Attaching drag and drop event listeners to $inner (inner iframe body).`);
|
|
2278
2759
|
|
|
@@ -2281,6 +2762,18 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2281
2762
|
const dropLogPrefix = '[ep_data_tables:dropHandler]';
|
|
2282
2763
|
// log(`${dropLogPrefix} DROP EVENT TRIGGERED. Event object:`, evt);
|
|
2283
2764
|
|
|
2765
|
+
// Block drops directly targeted at a table element regardless of selection state
|
|
2766
|
+
const targetEl = evt.target;
|
|
2767
|
+
if (targetEl && typeof targetEl.closest === 'function' && targetEl.closest('table.dataTable')) {
|
|
2768
|
+
evt.preventDefault();
|
|
2769
|
+
evt.stopPropagation();
|
|
2770
|
+
if (evt.originalEvent && evt.originalEvent.dataTransfer) {
|
|
2771
|
+
try { evt.originalEvent.dataTransfer.dropEffect = 'none'; } catch (_) {}
|
|
2772
|
+
}
|
|
2773
|
+
console.warn('[ep_data_tables] Drop prevented on table to protect structure.');
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2284
2777
|
// log(`${dropLogPrefix} Getting current editor representation (rep).`);
|
|
2285
2778
|
const rep = ed.ace_getRep();
|
|
2286
2779
|
if (!rep || !rep.selStart) {
|
|
@@ -2314,6 +2807,17 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2314
2807
|
$inner.on('dragover', (evt) => {
|
|
2315
2808
|
const dragLogPrefix = '[ep_data_tables:dragoverHandler]';
|
|
2316
2809
|
|
|
2810
|
+
// If hovering over a table, signal not-allowed and block default
|
|
2811
|
+
const targetEl = evt.target;
|
|
2812
|
+
if (targetEl && typeof targetEl.closest === 'function' && targetEl.closest('table.dataTable')) {
|
|
2813
|
+
if (evt.originalEvent && evt.originalEvent.dataTransfer) {
|
|
2814
|
+
try { evt.originalEvent.dataTransfer.dropEffect = 'none'; } catch (_) {}
|
|
2815
|
+
}
|
|
2816
|
+
evt.preventDefault();
|
|
2817
|
+
evt.stopPropagation();
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2317
2821
|
const rep = ed.ace_getRep();
|
|
2318
2822
|
if (!rep || !rep.selStart) {
|
|
2319
2823
|
return; // Allow default
|
|
@@ -2337,6 +2841,18 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2337
2841
|
}
|
|
2338
2842
|
});
|
|
2339
2843
|
|
|
2844
|
+
// Guard against dragenter into table areas
|
|
2845
|
+
$inner.on('dragenter', (evt) => {
|
|
2846
|
+
const targetEl = evt.target;
|
|
2847
|
+
if (targetEl && typeof targetEl.closest === 'function' && targetEl.closest('table.dataTable')) {
|
|
2848
|
+
if (evt.originalEvent && evt.originalEvent.dataTransfer) {
|
|
2849
|
+
try { evt.originalEvent.dataTransfer.dropEffect = 'none'; } catch (_) {}
|
|
2850
|
+
}
|
|
2851
|
+
evt.preventDefault();
|
|
2852
|
+
evt.stopPropagation();
|
|
2853
|
+
}
|
|
2854
|
+
});
|
|
2855
|
+
|
|
2340
2856
|
// *** EXISTING PASTE LISTENER ***
|
|
2341
2857
|
// log(`${callWithAceLogPrefix} Attaching paste event listener to $inner (inner iframe body).`);
|
|
2342
2858
|
$inner.on('paste', (evt) => {
|
|
@@ -2791,14 +3307,15 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2791
3307
|
// *** DRAG PREVENTION FOR TABLE ELEMENTS ***
|
|
2792
3308
|
const preventTableDrag = (evt) => {
|
|
2793
3309
|
const target = evt.target;
|
|
2794
|
-
//
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
target.tagName === 'TBODY' && target.closest('table.dataTable')) {
|
|
2799
|
-
// log('[ep_data_tables:dragPrevention] Preventing drag operation on table element:', target.tagName);
|
|
3310
|
+
// Block ANY drag gesture that originates within a table (including spans/text inside cells)
|
|
3311
|
+
const inTable = target && typeof target.closest === 'function' && target.closest('table.dataTable');
|
|
3312
|
+
if (inTable) {
|
|
3313
|
+
// log('[ep_data_tables:dragPrevention] Preventing drag operation originating from inside table');
|
|
2800
3314
|
evt.preventDefault();
|
|
2801
3315
|
evt.stopPropagation();
|
|
3316
|
+
if (evt.originalEvent && evt.originalEvent.dataTransfer) {
|
|
3317
|
+
try { evt.originalEvent.dataTransfer.effectAllowed = 'none'; } catch (_) {}
|
|
3318
|
+
}
|
|
2802
3319
|
return false;
|
|
2803
3320
|
}
|
|
2804
3321
|
};
|
|
@@ -2808,6 +3325,21 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2808
3325
|
$inner.on('drag', preventTableDrag);
|
|
2809
3326
|
$inner.on('dragend', preventTableDrag);
|
|
2810
3327
|
// log(`${callWithAceLogPrefix} Attached drag prevention handlers to inner body`);
|
|
3328
|
+
|
|
3329
|
+
// Attach drag prevention broadly to cover iframe boundaries and the host document
|
|
3330
|
+
if (innerDoc.length > 0) {
|
|
3331
|
+
innerDoc.on('dragstart', preventTableDrag);
|
|
3332
|
+
innerDoc.on('drag', preventTableDrag);
|
|
3333
|
+
innerDoc.on('dragend', preventTableDrag);
|
|
3334
|
+
}
|
|
3335
|
+
if (outerDoc.length > 0) {
|
|
3336
|
+
outerDoc.on('dragstart', preventTableDrag);
|
|
3337
|
+
outerDoc.on('drag', preventTableDrag);
|
|
3338
|
+
outerDoc.on('dragend', preventTableDrag);
|
|
3339
|
+
}
|
|
3340
|
+
$(document).on('dragstart', preventTableDrag);
|
|
3341
|
+
$(document).on('drag', preventTableDrag);
|
|
3342
|
+
$(document).on('dragend', preventTableDrag);
|
|
2811
3343
|
};
|
|
2812
3344
|
|
|
2813
3345
|
// Setup the global handlers
|
|
@@ -4239,203 +4771,4 @@ exports.aceUndoRedo = (hook, ctx) => {
|
|
|
4239
4771
|
console.error(`${logPrefix} Error during undo/redo validation:`, e);
|
|
4240
4772
|
// log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
|
|
4241
4773
|
}
|
|
4242
|
-
};
|
|
4243
|
-
|
|
4244
|
-
// *** ADDED: postAceInit hook for attaching listeners ***
|
|
4245
|
-
exports.postAceInit = (hookName, ctx) => {
|
|
4246
|
-
const func = '[ep_data_tables:postAceInit]';
|
|
4247
|
-
// log(`${func} START`);
|
|
4248
|
-
const editorInfo = ctx.ace; // Get editorInfo from context
|
|
4249
|
-
|
|
4250
|
-
if (!editorInfo) {
|
|
4251
|
-
console.error(`${func} ERROR: editorInfo (ctx.ace) is not available.`);
|
|
4252
|
-
return;
|
|
4253
|
-
}
|
|
4254
|
-
|
|
4255
|
-
const attachReconnectHandler = () => {
|
|
4256
|
-
try {
|
|
4257
|
-
const padObj = window.pad;
|
|
4258
|
-
const socket = padObj && padObj.socket;
|
|
4259
|
-
if (!socket) return false; // Not ready yet
|
|
4260
|
-
|
|
4261
|
-
if (socket.ep_data_tables_reconnect_listener_attached) return true;
|
|
4262
|
-
|
|
4263
|
-
let triggered = false;
|
|
4264
|
-
const triggerHardReconnect = (evtName) => {
|
|
4265
|
-
if (triggered) return;
|
|
4266
|
-
triggered = true;
|
|
4267
|
-
console.log(`[ep_data_tables] Socket.IO event '${evtName}' – invoking pad.forceReconnect()`);
|
|
4268
|
-
if (window.pad && typeof window.pad.forceReconnect === 'function') {
|
|
4269
|
-
try { window.pad.forceReconnect(); } catch(e) { console.error('[ep_data_tables] pad.forceReconnect() failed', e); window.location.reload(); }
|
|
4270
|
-
} else {
|
|
4271
|
-
window.location.reload();
|
|
4272
|
-
}
|
|
4273
|
-
};
|
|
4274
|
-
|
|
4275
|
-
socket.on('reconnect_attempt', () => triggerHardReconnect('reconnect_attempt'));
|
|
4276
|
-
socket.on('reconnect', () => triggerHardReconnect('reconnect'));
|
|
4277
|
-
socket.on('connect', () => { if (socket.disconnectedPreviously) triggerHardReconnect('connect'); });
|
|
4278
|
-
socket.on('disconnect', () => { socket.disconnectedPreviously = true; });
|
|
4279
|
-
|
|
4280
|
-
socket.ep_data_tables_reconnect_listener_attached = true;
|
|
4281
|
-
console.log('[ep_data_tables] Reconnect handler fully attached to pad.socket');
|
|
4282
|
-
return true;
|
|
4283
|
-
} catch (e) {
|
|
4284
|
-
console.error('[ep_data_tables] Error attaching reconnect listener:', e);
|
|
4285
|
-
return false;
|
|
4286
|
-
}
|
|
4287
|
-
};
|
|
4288
|
-
|
|
4289
|
-
// Keep trying until it attaches (no max attempts)
|
|
4290
|
-
if (!attachReconnectHandler()) {
|
|
4291
|
-
const intervalId = setInterval(() => {
|
|
4292
|
-
if (attachReconnectHandler()) clearInterval(intervalId);
|
|
4293
|
-
}, 500);
|
|
4294
|
-
}
|
|
4295
|
-
|
|
4296
|
-
// Setup mousedown listener via callWithAce
|
|
4297
|
-
editorInfo.ace_callWithAce((ace) => {
|
|
4298
|
-
const editor = ace.editor;
|
|
4299
|
-
const inner = ace.editor.container; // Use the main container
|
|
4300
|
-
|
|
4301
|
-
if (!editor || !inner) {
|
|
4302
|
-
console.error(`${func} ERROR: ace.editor or ace.editor.container not found within ace_callWithAce.`);
|
|
4303
|
-
return;
|
|
4304
|
-
}
|
|
4305
|
-
|
|
4306
|
-
// log(`${func} Inside callWithAce for attaching mousedown listeners.`);
|
|
4307
|
-
|
|
4308
|
-
// Initialize shared state on the editor object
|
|
4309
|
-
if (!editor.ep_data_tables_last_clicked) {
|
|
4310
|
-
editor.ep_data_tables_last_clicked = null;
|
|
4311
|
-
// log(`${func} Initialized ace.editor.ep_data_tables_last_clicked`);
|
|
4312
|
-
}
|
|
4313
|
-
|
|
4314
|
-
// log(`${func} Attempting to attach mousedown listener to editor container for cell selection...`);
|
|
4315
|
-
|
|
4316
|
-
inner.addEventListener('mousedown', (evt) => {
|
|
4317
|
-
const target = evt.target;
|
|
4318
|
-
const mousedownFuncName = '[ep_data_tables mousedown]';
|
|
4319
|
-
// log(`${mousedownFuncName} RAW MOUSE DOWN detected. Target:`, target);
|
|
4320
|
-
// log(`${mousedownFuncName} Target tagName: ${target.tagName}`);
|
|
4321
|
-
// log(`${mousedownFuncName} Target className: ${target.className}`);
|
|
4322
|
-
// log(`${mousedownFuncName} Target ID: ${target.id}`);
|
|
4323
|
-
|
|
4324
|
-
// Don't interfere with resize handle clicks
|
|
4325
|
-
if (target.classList && target.classList.contains('ep-data_tables-resize-handle')) {
|
|
4326
|
-
// log(`${mousedownFuncName} Click on resize handle, skipping cell selection logic.`);
|
|
4327
|
-
return;
|
|
4328
|
-
}
|
|
4329
|
-
|
|
4330
|
-
// *** ENHANCED DEBUG: Check for image-related elements ***
|
|
4331
|
-
const $target = $(target);
|
|
4332
|
-
const isImageElement = $target.closest('.inline-image, .image-placeholder, .image-inner, .image-resize-handle').length > 0;
|
|
4333
|
-
// log(`${mousedownFuncName} Is target or ancestor image-related?`, isImageElement);
|
|
4334
|
-
|
|
4335
|
-
if (isImageElement) {
|
|
4336
|
-
// log(`${mousedownFuncName} *** IMAGE ELEMENT DETECTED ***`);
|
|
4337
|
-
// log(`${mousedownFuncName} Closest image container:`, $target.closest('.inline-image, .image-placeholder')[0]);
|
|
4338
|
-
// log(`${mousedownFuncName} Is .inline-image:`, $target.hasClass('inline-image') || $target.closest('.inline-image').length > 0);
|
|
4339
|
-
// log(`${mousedownFuncName} Is .image-placeholder:`, $target.hasClass('image-placeholder') || $target.closest('.image-placeholder').length > 0);
|
|
4340
|
-
// log(`${mousedownFuncName} Is .image-inner:`, $target.hasClass('image-inner') || $target.closest('.image-inner').length > 0);
|
|
4341
|
-
// log(`${mousedownFuncName} Is .image-resize-handle:`, $target.hasClass('image-resize-handle') || $target.closest('.image-resize-handle').length > 0);
|
|
4342
|
-
}
|
|
4343
|
-
|
|
4344
|
-
// Check if the click is on an image or image-related element - if so, completely skip
|
|
4345
|
-
if (isImageElement) {
|
|
4346
|
-
// log(`${mousedownFuncName} Click detected on image element within table cell. Completely skipping table processing to avoid interference.`);
|
|
4347
|
-
return;
|
|
4348
|
-
}
|
|
4349
|
-
|
|
4350
|
-
// *** ENHANCED DEBUG: Check table context ***
|
|
4351
|
-
const clickedTD = target.closest('td');
|
|
4352
|
-
const clickedTR = target.closest('tr');
|
|
4353
|
-
const clickedTable = target.closest('table.dataTable');
|
|
4354
|
-
|
|
4355
|
-
// log(`${mousedownFuncName} Table context analysis:`);
|
|
4356
|
-
// log(`${mousedownFuncName} - Clicked TD:`, !!clickedTD);
|
|
4357
|
-
// log(`${mousedownFuncName} - Clicked TR:`, !!clickedTR);
|
|
4358
|
-
// log(`${mousedownFuncName} - Clicked table.dataTable:`, !!clickedTable);
|
|
4359
|
-
|
|
4360
|
-
if (clickedTable) {
|
|
4361
|
-
// log(`${mousedownFuncName} - Table tblId:`, clickedTable.getAttribute('data-tblId'));
|
|
4362
|
-
// log(`${mousedownFuncName} - Table row:`, clickedTable.getAttribute('data-row'));
|
|
4363
|
-
}
|
|
4364
|
-
if (clickedTD) {
|
|
4365
|
-
// log(`${mousedownFuncName} - TD data-column:`, clickedTD.getAttribute('data-column'));
|
|
4366
|
-
// log(`${mousedownFuncName} - TD innerHTML length:`, clickedTD.innerHTML?.length || 0);
|
|
4367
|
-
// log(`${mousedownFuncName} - TD contains images:`, clickedTD.querySelector('.inline-image, .image-placeholder') ? 'YES' : 'NO');
|
|
4368
|
-
}
|
|
4369
|
-
|
|
4370
|
-
// Clear previous selection state regardless of where click happened
|
|
4371
|
-
if (editor.ep_data_tables_last_clicked) {
|
|
4372
|
-
// log(`${mousedownFuncName} Clearing previous selection info.`);
|
|
4373
|
-
// TODO: Add visual class removal if needed
|
|
4374
|
-
}
|
|
4375
|
-
editor.ep_data_tables_last_clicked = null; // Clear state first
|
|
4376
|
-
|
|
4377
|
-
if (clickedTD && clickedTR && clickedTable) {
|
|
4378
|
-
// log(`${mousedownFuncName} Click detected inside table.dataTable td.`);
|
|
4379
|
-
try {
|
|
4380
|
-
const cellIndex = Array.from(clickedTR.children).indexOf(clickedTD);
|
|
4381
|
-
const lineNode = clickedTable.closest('div.ace-line');
|
|
4382
|
-
const tblId = clickedTable.getAttribute('data-tblId');
|
|
4383
|
-
|
|
4384
|
-
// log(`${mousedownFuncName} Cell analysis:`);
|
|
4385
|
-
// log(`${mousedownFuncName} - Cell index:`, cellIndex);
|
|
4386
|
-
// log(`${mousedownFuncName} - Line node:`, !!lineNode);
|
|
4387
|
-
// log(`${mousedownFuncName} - Line node ID:`, lineNode?.id);
|
|
4388
|
-
// log(`${mousedownFuncName} - Table ID:`, tblId);
|
|
4389
|
-
|
|
4390
|
-
// Ensure ace.rep and ace.rep.lines are available
|
|
4391
|
-
if (!ace.rep || !ace.rep.lines) {
|
|
4392
|
-
console.error(`${mousedownFuncName} ERROR: ace.rep or ace.rep.lines not available inside mousedown listener.`);
|
|
4393
|
-
return;
|
|
4394
|
-
}
|
|
4395
|
-
|
|
4396
|
-
if (lineNode && lineNode.id && tblId !== null && cellIndex !== -1) {
|
|
4397
|
-
const lineNum = ace.rep.lines.indexOfKey(lineNode.id);
|
|
4398
|
-
if (lineNum !== -1) {
|
|
4399
|
-
// Store the accurately determined cell info
|
|
4400
|
-
// Initialize relative position - might be refined later if needed
|
|
4401
|
-
const clickInfo = { lineNum, tblId, cellIndex, relativePos: 0 }; // Set initial relativePos to 0
|
|
4402
|
-
editor.ep_data_tables_last_clicked = clickInfo;
|
|
4403
|
-
// log(`${mousedownFuncName} Clicked cell (SUCCESS): Line=${lineNum}, TblId=${tblId}, CellIndex=${cellIndex}. Stored click info:`, clickInfo);
|
|
4404
|
-
|
|
4405
|
-
// --- NEW: Jump caret immediately for snappier UX ---
|
|
4406
|
-
try {
|
|
4407
|
-
const docMgr = ace.ep_data_tables_docManager;
|
|
4408
|
-
if (docMgr && typeof navigateToCell === 'function') {
|
|
4409
|
-
const navOk = navigateToCell(lineNum, cellIndex, ace, docMgr);
|
|
4410
|
-
// log(`${mousedownFuncName} Immediate navigateToCell result: ${navOk}`);
|
|
4411
|
-
}
|
|
4412
|
-
} catch (navErr) {
|
|
4413
|
-
console.error(`${mousedownFuncName} Error during immediate caret navigation:`, navErr);
|
|
4414
|
-
}
|
|
4415
|
-
|
|
4416
|
-
// TODO: Add visual class for selection if desired
|
|
4417
|
-
// log(`${mousedownFuncName} TEST: Skipped adding/removing selected-table-cell class`);
|
|
4418
|
-
|
|
4419
|
-
} else {
|
|
4420
|
-
// log(`${mousedownFuncName} Clicked cell (ERROR): Could not find line number for node ID: ${lineNode.id}`);
|
|
4421
|
-
}
|
|
4422
|
-
} else {
|
|
4423
|
-
// log(`${mousedownFuncName} Clicked cell (ERROR): Missing required info (lineNode, lineNode.id, tblId, or valid cellIndex).`, { lineNode, tblId, cellIndex });
|
|
4424
|
-
}
|
|
4425
|
-
} catch (e) {
|
|
4426
|
-
console.error(`${mousedownFuncName} Error processing table cell click:`, e);
|
|
4427
|
-
// log(`${mousedownFuncName} Error details:`, { message: e.message, stack: e.stack });
|
|
4428
|
-
editor.ep_data_tables_last_clicked = null; // Ensure state is clear on error
|
|
4429
|
-
}
|
|
4430
|
-
} else {
|
|
4431
|
-
// log(`${mousedownFuncName} Click was outside a table.dataTable td.`);
|
|
4432
|
-
}
|
|
4433
|
-
});
|
|
4434
|
-
// log(`${func} Mousedown listeners for cell selection attached successfully (inside callWithAce).`);
|
|
4435
|
-
|
|
4436
|
-
}, 'tableCellSelectionPostAce', true); // Unique name for callstack
|
|
4437
|
-
|
|
4438
|
-
// log(`${func} END`);
|
|
4439
|
-
};
|
|
4440
|
-
|
|
4441
|
-
// END OF FILE
|
|
4774
|
+
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const log = (...m) => console.debug('[ep_data_tables:collector_hooks]', ...m);
|
|
2
|
-
|
|
3
|
-
exports.collectContentPre = (hook, ctx) => {
|
|
4
|
-
return;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
exports.collectContentLineBreak = (hook, ctx) => {
|
|
8
|
-
return true;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
exports.collectContentLineText = (_hook, ctx) => {
|
|
12
|
-
return ctx.text || '';
|
|
13
|
-
};
|