ep_data_tables 0.0.7 → 0.0.9
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/css/datatables-editor.css +69 -108
- package/static/js/client_hooks.js +1096 -977
|
@@ -1,41 +1,20 @@
|
|
|
1
|
-
/* ep_data_tables – attribute‑based tables (line‑class + PostWrite renderer)
|
|
2
|
-
* -----------------------------------------------------------------
|
|
3
|
-
* Strategy
|
|
4
|
-
* • One line attribute tbljson = JSON({tblId,row,cells:[{txt:"…"},…]})
|
|
5
|
-
* • One char‑range attr td = column‑index (string)
|
|
6
|
-
* • `aceLineAttribsToClasses` puts class `tbl-line` on the **line div** so
|
|
7
|
-
* we can catch it once per line in `acePostWriteDomLineHTML`.
|
|
8
|
-
* • Renderer accumulates rows that share the same tblId in a buffer on
|
|
9
|
-
* innerdocbody, flushes to a single <table> when the run ends.
|
|
10
|
-
* • No raw JSON text is ever visible to the user.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/* eslint-env browser */
|
|
14
|
-
|
|
15
|
-
// ────────────────────────────── constants ──────────────────────────────
|
|
16
1
|
const ATTR_TABLE_JSON = 'tbljson';
|
|
17
2
|
const ATTR_CELL = 'td';
|
|
18
|
-
const ATTR_CLASS_PREFIX = 'tbljson-';
|
|
3
|
+
const ATTR_CLASS_PREFIX = 'tbljson-';
|
|
19
4
|
const log = (...m) => console.debug('[ep_data_tables:client_hooks]', ...m);
|
|
20
|
-
const DELIMITER = '\u241F';
|
|
21
|
-
// Use the same rare character inside the hidden span so acePostWriteDomLineHTML can
|
|
22
|
-
// still find delimiters when it splits node.innerHTML.
|
|
23
|
-
// Users never see this because the span is contenteditable=false and styled away.
|
|
5
|
+
const DELIMITER = '\u241F';
|
|
24
6
|
const HIDDEN_DELIM = DELIMITER;
|
|
7
|
+
const INPUTTYPE_REPLACEMENT_TYPES = new Set(['insertReplacementText', 'insertFromComposition']);
|
|
25
8
|
|
|
26
|
-
// helper for stable random ids
|
|
27
9
|
const rand = () => Math.random().toString(36).slice(2, 8);
|
|
28
10
|
|
|
29
|
-
// encode/decode so JSON can survive as a CSS class token if ever needed
|
|
30
11
|
const enc = s => btoa(s).replace(/\+/g, '-').replace(/\//g, '_');
|
|
31
12
|
const dec = s => {
|
|
32
|
-
// Revert to simpler decode, assuming enc provides valid padding
|
|
33
13
|
const str = s.replace(/-/g, '+').replace(/_/g, '/');
|
|
34
14
|
try {
|
|
35
15
|
if (typeof atob === 'function') {
|
|
36
|
-
return atob(str);
|
|
16
|
+
return atob(str);
|
|
37
17
|
} else if (typeof Buffer === 'function') {
|
|
38
|
-
// Node.js environment
|
|
39
18
|
return Buffer.from(str, 'base64').toString('utf8');
|
|
40
19
|
} else {
|
|
41
20
|
console.error('[ep_data_tables] Base64 decoding function (atob or Buffer) not found.');
|
|
@@ -47,45 +26,48 @@ const dec = s => {
|
|
|
47
26
|
}
|
|
48
27
|
};
|
|
49
28
|
|
|
50
|
-
|
|
51
|
-
let lastClickedCellInfo = null; // { lineNum: number, cellIndex: number, tblId: string }
|
|
52
|
-
|
|
53
|
-
// NEW: Module-level state for column resizing (similar to images plugin)
|
|
29
|
+
let lastClickedCellInfo = null;
|
|
54
30
|
let isResizing = false;
|
|
55
31
|
let resizeStartX = 0;
|
|
56
|
-
let resizeCurrentX = 0;
|
|
32
|
+
let resizeCurrentX = 0;
|
|
57
33
|
let resizeTargetTable = null;
|
|
58
34
|
let resizeTargetColumn = -1;
|
|
59
35
|
let resizeOriginalWidths = [];
|
|
60
36
|
let resizeTableMetadata = null;
|
|
61
37
|
let resizeLineNum = -1;
|
|
62
|
-
let resizeOverlay = null;
|
|
63
|
-
// Android Chrome composition handling state
|
|
38
|
+
let resizeOverlay = null;
|
|
64
39
|
let suppressNextBeforeInputInsertTextOnce = false;
|
|
65
40
|
let isAndroidChromeComposition = false;
|
|
66
41
|
let handledCurrentComposition = false;
|
|
67
|
-
// Suppress all beforeinput insertText events during an Android Chrome IME composition
|
|
68
42
|
let suppressBeforeInputInsertTextDuringComposition = false;
|
|
69
|
-
// Helper to detect any Android browser (exclude iOS/Safari)
|
|
70
43
|
function isAndroidUA() {
|
|
71
44
|
const ua = (navigator.userAgent || '').toLowerCase();
|
|
72
45
|
const isAndroid = ua.includes('android');
|
|
46
|
+
// Treat Chrome OS (Chromebooks) similarly because touch-screen Chromebooks exhibit the same
|
|
47
|
+
// duplicate beforeinput / composition quirks we patch for Android.
|
|
48
|
+
const isChromeOS = ua.includes('cros'); // "CrOS" token present in Chromebook UAs
|
|
73
49
|
const isIOS = ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod') || ua.includes('crios');
|
|
74
|
-
|
|
75
|
-
return isAndroid && !isIOS;
|
|
50
|
+
return (isAndroid || isChromeOS) && !isIOS;
|
|
76
51
|
}
|
|
77
52
|
|
|
78
|
-
|
|
53
|
+
function isIOSUA() {
|
|
54
|
+
const ua = (navigator.userAgent || '').toLowerCase();
|
|
55
|
+
return ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod') || ua.includes('ios') || ua.includes('crios') || ua.includes('fxios') || ua.includes('edgios');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Normalise soft whitespace (NBSP, newline, carriage return, tab) to ASCII space and collapse runs.
|
|
59
|
+
const normalizeSoftWhitespace = (str) => (
|
|
60
|
+
(str || '')
|
|
61
|
+
.replace(/[\u00A0\r\n\t]/g, ' ')
|
|
62
|
+
.replace(/\s+/g, ' ')
|
|
63
|
+
);
|
|
79
64
|
|
|
80
65
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* @param {HTMLElement} element - The root element to start searching from.
|
|
84
|
-
* @returns {HTMLElement|null} - The found element or null.
|
|
66
|
+
* @param {HTMLElement} element
|
|
67
|
+
* @returns {HTMLElement|null}
|
|
85
68
|
*/
|
|
86
69
|
function findTbljsonElement(element) {
|
|
87
70
|
if (!element) return null;
|
|
88
|
-
// Check if this element has the tbljson class
|
|
89
71
|
if (element.classList) {
|
|
90
72
|
for (const cls of element.classList) {
|
|
91
73
|
if (cls.startsWith(ATTR_CLASS_PREFIX)) {
|
|
@@ -93,7 +75,6 @@ function findTbljsonElement(element) {
|
|
|
93
75
|
}
|
|
94
76
|
}
|
|
95
77
|
}
|
|
96
|
-
// Recursively check children
|
|
97
78
|
if (element.children) {
|
|
98
79
|
for (const child of element.children) {
|
|
99
80
|
const found = findTbljsonElement(child);
|
|
@@ -114,7 +95,6 @@ function findTbljsonElement(element) {
|
|
|
114
95
|
function getTableLineMetadata(lineNum, editorInfo, docManager) {
|
|
115
96
|
const funcName = 'getTableLineMetadata';
|
|
116
97
|
try {
|
|
117
|
-
// First, try the fast path: getting the attribute directly from the line.
|
|
118
98
|
const attribs = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
119
99
|
if (attribs) {
|
|
120
100
|
try {
|
|
@@ -128,12 +108,9 @@ function getTableLineMetadata(lineNum, editorInfo, docManager) {
|
|
|
128
108
|
}
|
|
129
109
|
}
|
|
130
110
|
|
|
131
|
-
// Fallback for block-styled lines.
|
|
132
111
|
// log(`${funcName}: No valid attribute on line ${lineNum}, checking DOM.`);
|
|
133
112
|
const rep = editorInfo.ace_getRep();
|
|
134
|
-
|
|
135
|
-
// This is the fix: Get the lineNode directly from the rep. It's more reliable
|
|
136
|
-
// than querying the DOM and avoids the ace_getOuterDoc() call which was failing.
|
|
113
|
+
|
|
137
114
|
const lineEntry = rep.lines.atIndex(lineNum);
|
|
138
115
|
const lineNode = lineEntry?.lineNode;
|
|
139
116
|
|
|
@@ -168,7 +145,6 @@ function getTableLineMetadata(lineNum, editorInfo, docManager) {
|
|
|
168
145
|
}
|
|
169
146
|
}
|
|
170
147
|
|
|
171
|
-
// ─────────────────── Cell Navigation Helper Functions ───────────────────
|
|
172
148
|
/**
|
|
173
149
|
* Navigate to the next cell in the table (Tab key behavior)
|
|
174
150
|
* @param {number} currentLineNum - Current line number
|
|
@@ -182,41 +158,35 @@ function getTableLineMetadata(lineNum, editorInfo, docManager) {
|
|
|
182
158
|
function navigateToNextCell(currentLineNum, currentCellIndex, tableMetadata, shiftKey, editorInfo, docManager) {
|
|
183
159
|
const funcName = 'navigateToNextCell';
|
|
184
160
|
// log(`${funcName}: START - Current: Line=${currentLineNum}, Cell=${currentCellIndex}, Shift=${shiftKey}`);
|
|
185
|
-
|
|
161
|
+
|
|
186
162
|
try {
|
|
187
163
|
let targetRow = tableMetadata.row;
|
|
188
164
|
let targetCol = currentCellIndex;
|
|
189
165
|
|
|
190
166
|
if (shiftKey) {
|
|
191
|
-
// Shift+Tab: Move to previous cell
|
|
192
167
|
targetCol--;
|
|
193
168
|
if (targetCol < 0) {
|
|
194
|
-
// Move to last cell of previous row
|
|
195
169
|
targetRow--;
|
|
196
170
|
targetCol = tableMetadata.cols - 1;
|
|
197
171
|
}
|
|
198
172
|
} else {
|
|
199
|
-
// Tab: Move to next cell
|
|
200
173
|
targetCol++;
|
|
201
174
|
if (targetCol >= tableMetadata.cols) {
|
|
202
|
-
// Move to first cell of next row
|
|
203
175
|
targetRow++;
|
|
204
176
|
targetCol = 0;
|
|
205
177
|
}
|
|
206
178
|
}
|
|
207
179
|
|
|
208
180
|
// log(`${funcName}: Target coordinates - Row=${targetRow}, Col=${targetCol}`);
|
|
209
|
-
|
|
210
|
-
// Find the line number for the target row
|
|
181
|
+
|
|
211
182
|
const targetLineNum = findLineForTableRow(tableMetadata.tblId, targetRow, editorInfo, docManager);
|
|
212
183
|
if (targetLineNum === -1) {
|
|
213
184
|
// log(`${funcName}: Could not find line for target row ${targetRow}`);
|
|
214
185
|
return false;
|
|
215
186
|
}
|
|
216
187
|
|
|
217
|
-
// Navigate to the target cell
|
|
218
188
|
return navigateToCell(targetLineNum, targetCol, editorInfo, docManager);
|
|
219
|
-
|
|
189
|
+
|
|
220
190
|
} catch (e) {
|
|
221
191
|
console.error(`[ep_data_tables] ${funcName}: Error during navigation:`, e);
|
|
222
192
|
return false;
|
|
@@ -242,37 +212,28 @@ function navigateToCellBelow(currentLineNum, currentCellIndex, tableMetadata, ed
|
|
|
242
212
|
|
|
243
213
|
// log(`${funcName}: Target coordinates - Row=${targetRow}, Col=${targetCol}`);
|
|
244
214
|
|
|
245
|
-
// Find the line number for the target row
|
|
246
215
|
const targetLineNum = findLineForTableRow(tableMetadata.tblId, targetRow, editorInfo, docManager);
|
|
247
216
|
|
|
248
217
|
if (targetLineNum !== -1) {
|
|
249
|
-
// Found the row below, navigate to it.
|
|
250
218
|
// log(`${funcName}: Found line for target row ${targetRow}, navigating.`);
|
|
251
219
|
return navigateToCell(targetLineNum, targetCol, editorInfo, docManager);
|
|
252
220
|
} else {
|
|
253
|
-
// Could not find the row below, we must be on the last line.
|
|
254
|
-
// Create a new, empty line after the table.
|
|
255
221
|
// log(`${funcName}: Could not find next row. Creating new line after table.`);
|
|
256
222
|
const rep = editorInfo.ace_getRep();
|
|
257
223
|
const lineTextLength = rep.lines.atIndex(currentLineNum).text.length;
|
|
258
224
|
const endOfLinePos = [currentLineNum, lineTextLength];
|
|
259
225
|
|
|
260
|
-
// Move caret to end of the current line...
|
|
261
226
|
editorInfo.ace_performSelectionChange(endOfLinePos, endOfLinePos, false);
|
|
262
|
-
// ...and insert a newline character. This creates a new line below.
|
|
263
227
|
editorInfo.ace_performDocumentReplaceRange(endOfLinePos, endOfLinePos, '\n');
|
|
264
228
|
|
|
265
|
-
// The caret is automatically moved to the new line by the operation above,
|
|
266
|
-
// but we ensure the visual selection is synced and the editor is focused.
|
|
267
229
|
editorInfo.ace_updateBrowserSelectionFromRep();
|
|
268
230
|
editorInfo.ace_focus();
|
|
269
231
|
|
|
270
|
-
// We've now exited the table, so clear the last-clicked state.
|
|
271
232
|
const editor = editorInfo.editor;
|
|
272
233
|
if (editor) editor.ep_data_tables_last_clicked = null;
|
|
273
234
|
// log(`${funcName}: Cleared last click info as we have exited the table.`);
|
|
274
235
|
|
|
275
|
-
return true;
|
|
236
|
+
return true;
|
|
276
237
|
}
|
|
277
238
|
} catch (e) {
|
|
278
239
|
console.error(`[ep_data_tables] ${funcName}: Error during navigation:`, e);
|
|
@@ -291,20 +252,19 @@ function navigateToCellBelow(currentLineNum, currentCellIndex, tableMetadata, ed
|
|
|
291
252
|
function findLineForTableRow(tblId, targetRow, editorInfo, docManager) {
|
|
292
253
|
const funcName = 'findLineForTableRow';
|
|
293
254
|
// log(`${funcName}: Searching for tblId=${tblId}, row=${targetRow}`);
|
|
294
|
-
|
|
255
|
+
|
|
295
256
|
try {
|
|
296
257
|
const rep = editorInfo.ace_getRep();
|
|
297
258
|
if (!rep || !rep.lines) {
|
|
298
259
|
// log(`${funcName}: Could not get rep or rep.lines`);
|
|
299
260
|
return -1;
|
|
300
261
|
}
|
|
301
|
-
|
|
262
|
+
|
|
302
263
|
const totalLines = rep.lines.length();
|
|
303
264
|
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
|
|
304
265
|
try {
|
|
305
266
|
let lineAttrString = docManager.getAttributeOnLine(lineIndex, ATTR_TABLE_JSON);
|
|
306
|
-
|
|
307
|
-
// If no attribute found directly, check DOM (same logic as acePostWriteDomLineHTML)
|
|
267
|
+
|
|
308
268
|
if (!lineAttrString) {
|
|
309
269
|
const lineEntry = rep.lines.atIndex(lineIndex);
|
|
310
270
|
if (lineEntry && lineEntry.lineNode) {
|
|
@@ -319,7 +279,7 @@ function findLineForTableRow(tblId, targetRow, editorInfo, docManager) {
|
|
|
319
279
|
}
|
|
320
280
|
}
|
|
321
281
|
}
|
|
322
|
-
|
|
282
|
+
|
|
323
283
|
if (lineAttrString) {
|
|
324
284
|
const lineMetadata = JSON.parse(lineAttrString);
|
|
325
285
|
if (lineMetadata.tblId === tblId && lineMetadata.row === targetRow) {
|
|
@@ -328,10 +288,10 @@ function findLineForTableRow(tblId, targetRow, editorInfo, docManager) {
|
|
|
328
288
|
}
|
|
329
289
|
}
|
|
330
290
|
} catch (e) {
|
|
331
|
-
continue;
|
|
291
|
+
continue;
|
|
332
292
|
}
|
|
333
293
|
}
|
|
334
|
-
|
|
294
|
+
|
|
335
295
|
// log(`${funcName}: Target row not found`);
|
|
336
296
|
return -1;
|
|
337
297
|
} catch (e) {
|
|
@@ -359,7 +319,7 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
|
|
|
359
319
|
// log(`${funcName}: Could not get rep or rep.lines`);
|
|
360
320
|
return false;
|
|
361
321
|
}
|
|
362
|
-
|
|
322
|
+
|
|
363
323
|
const lineEntry = rep.lines.atIndex(targetLineNum);
|
|
364
324
|
if (!lineEntry) {
|
|
365
325
|
// log(`${funcName}: Could not get line entry for line ${targetLineNum}`);
|
|
@@ -368,7 +328,7 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
|
|
|
368
328
|
|
|
369
329
|
const lineText = lineEntry.text || '';
|
|
370
330
|
const cells = lineText.split(DELIMITER);
|
|
371
|
-
|
|
331
|
+
|
|
372
332
|
if (targetCellIndex >= cells.length) {
|
|
373
333
|
// log(`${funcName}: Target cell ${targetCellIndex} doesn't exist (only ${cells.length} cells)`);
|
|
374
334
|
return false;
|
|
@@ -384,10 +344,8 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
|
|
|
384
344
|
const clampedTargetCol = Math.min(targetCol, lineText.length);
|
|
385
345
|
targetPos = [targetLineNum, clampedTargetCol];
|
|
386
346
|
|
|
387
|
-
// --- NEW: Update plugin state BEFORE performing the UI action ---
|
|
388
347
|
try {
|
|
389
348
|
const editor = editorInfo.ep_data_tables_editor;
|
|
390
|
-
// Use the new robust helper to get metadata, which handles block-styled lines.
|
|
391
349
|
const tableMetadata = getTableLineMetadata(targetLineNum, editorInfo, docManager);
|
|
392
350
|
|
|
393
351
|
if (editor && tableMetadata) {
|
|
@@ -404,23 +362,14 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
|
|
|
404
362
|
} catch (e) {
|
|
405
363
|
// log(`${funcName}: Could not update stored click info before navigation:`, e.message);
|
|
406
364
|
}
|
|
407
|
-
|
|
408
|
-
// The previous attempts involving wrappers and poking the renderer have all
|
|
409
|
-
// proven to be unstable. The correct approach is to directly update the
|
|
410
|
-
// internal model and then tell the browser to sync its visual selection to
|
|
411
|
-
// that model.
|
|
365
|
+
|
|
412
366
|
try {
|
|
413
|
-
// 1. Update the internal representation of the selection.
|
|
414
367
|
editorInfo.ace_performSelectionChange(targetPos, targetPos, false);
|
|
415
368
|
// log(`${funcName}: Updated internal selection to [${targetPos}]`);
|
|
416
369
|
|
|
417
|
-
// 2. Explicitly tell the editor to update the browser's visual selection
|
|
418
|
-
// to match the new internal representation. This is the correct way to
|
|
419
|
-
// make the caret appear in the new location without causing a race condition.
|
|
420
370
|
editorInfo.ace_updateBrowserSelectionFromRep();
|
|
421
371
|
// log(`${funcName}: Called updateBrowserSelectionFromRep to sync visual caret.`);
|
|
422
|
-
|
|
423
|
-
// 3. Ensure the editor has focus.
|
|
372
|
+
|
|
424
373
|
editorInfo.ace_focus();
|
|
425
374
|
// log(`${funcName}: Editor focused.`);
|
|
426
375
|
|
|
@@ -428,9 +377,8 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
|
|
|
428
377
|
console.error(`[ep_data_tables] ${funcName}: Error during direct navigation update:`, e);
|
|
429
378
|
return false;
|
|
430
379
|
}
|
|
431
|
-
|
|
380
|
+
|
|
432
381
|
} catch (e) {
|
|
433
|
-
// This synchronous catch is a fallback, though the error was happening asynchronously.
|
|
434
382
|
console.error(`[ep_data_tables] ${funcName}: Error during cell navigation:`, e);
|
|
435
383
|
return false;
|
|
436
384
|
}
|
|
@@ -439,16 +387,14 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
|
|
|
439
387
|
return true;
|
|
440
388
|
}
|
|
441
389
|
|
|
442
|
-
// ────────────────────── collectContentPre (DOM → atext) ─────────────────────
|
|
443
390
|
exports.collectContentPre = (hook, ctx) => {
|
|
444
391
|
const funcName = 'collectContentPre';
|
|
445
|
-
const node = ctx.domNode;
|
|
392
|
+
const node = ctx.domNode;
|
|
446
393
|
const state = ctx.state;
|
|
447
|
-
const cc = ctx.cc;
|
|
394
|
+
const cc = ctx.cc;
|
|
448
395
|
|
|
449
396
|
// log(`${funcName}: *** ENTRY POINT *** Hook: ${hook}, Node: ${node?.tagName}.${node?.className}`);
|
|
450
397
|
|
|
451
|
-
// ***** START Primary Path: Reconstruct from rendered table *****
|
|
452
398
|
if (node?.classList?.contains('ace-line')) {
|
|
453
399
|
const tableNode = node.querySelector('table.dataTable[data-tblId]');
|
|
454
400
|
if (tableNode) {
|
|
@@ -476,20 +422,13 @@ exports.collectContentPre = (hook, ctx) => {
|
|
|
476
422
|
let cellHTMLSegments = Array.from(trNode.children).map((td, index) => {
|
|
477
423
|
let segmentHTML = td.innerHTML || '';
|
|
478
424
|
// log(`${funcName}: Line ${lineNum} TD[${index}] raw innerHTML (first 100): "${segmentHTML.substring(0,100)}"`);
|
|
479
|
-
|
|
425
|
+
|
|
480
426
|
const resizeHandleRegex = /<div class="ep-data_tables-resize-handle"[^>]*><\/div>/ig;
|
|
481
427
|
segmentHTML = segmentHTML.replace(resizeHandleRegex, '');
|
|
482
|
-
// NEW: Also remove any previously injected hidden delimiter span so we do
|
|
483
|
-
// not serialise it back into the atext. Leaving it in would duplicate the
|
|
484
|
-
// hidden span on every save-reload cycle and, more importantly, confuse the
|
|
485
|
-
// later HTML-to-table reconstruction because the delimiter that lives *inside*
|
|
486
|
-
// the span would be mistaken for a real cell boundary.
|
|
487
428
|
const hiddenDelimRegexPrimary = /<span class="ep-data_tables-delim"[^>]*>.*?<\/span>/ig;
|
|
488
429
|
segmentHTML = segmentHTML.replace(hiddenDelimRegexPrimary, '');
|
|
489
|
-
// Remove caret-anchor spans (invisible, non-semantic)
|
|
490
430
|
const caretAnchorRegex = /<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig;
|
|
491
431
|
segmentHTML = segmentHTML.replace(caretAnchorRegex, '');
|
|
492
|
-
// If, after stripping tags/entities, the content is empty, serialize as empty string
|
|
493
432
|
const textCheck = segmentHTML.replace(/<[^>]*>/g, '').replace(/ /ig, ' ').trim();
|
|
494
433
|
if (textCheck === '') segmentHTML = '';
|
|
495
434
|
|
|
@@ -514,7 +453,7 @@ exports.collectContentPre = (hook, ctx) => {
|
|
|
514
453
|
state.lineAttributes = state.lineAttributes.filter(attr => attr[0] !== ATTR_TABLE_JSON);
|
|
515
454
|
state.lineAttributes.push([ATTR_TABLE_JSON, existingAttrString]);
|
|
516
455
|
// log(`${funcName}: Line ${lineNum} ensured ${ATTR_TABLE_JSON} attribute is in state.lineAttributes.`);
|
|
517
|
-
|
|
456
|
+
|
|
518
457
|
// log(`${funcName}: Line ${lineNum} reconstruction complete. Returning undefined to prevent default DOM collection.`);
|
|
519
458
|
return undefined;
|
|
520
459
|
} else {
|
|
@@ -534,7 +473,7 @@ exports.collectContentPre = (hook, ctx) => {
|
|
|
534
473
|
const tempMetadata = {tblId: domTblId, row: parseInt(domRow, 10), cols: domCols};
|
|
535
474
|
const tempAttrString = JSON.stringify(tempMetadata);
|
|
536
475
|
// log(`${funcName}: Line ${lineNum} FALLBACK: Constructed temporary metadata: ${tempAttrString}`);
|
|
537
|
-
|
|
476
|
+
|
|
538
477
|
let cellHTMLSegments = Array.from(trNode.children).map((td, index) => {
|
|
539
478
|
let segmentHTML = td.innerHTML || '';
|
|
540
479
|
const resizeHandleRegex = /<div class="ep-data_tables-resize-handle"[^>]*><\/div>/ig;
|
|
@@ -543,15 +482,13 @@ exports.collectContentPre = (hook, ctx) => {
|
|
|
543
482
|
const hiddenDelimRegex = new RegExp('^<span class="ep-data_tables-delim" contenteditable="false">' + DELIMITER + '(<\\/span>)?<\\/span>', 'i');
|
|
544
483
|
segmentHTML = segmentHTML.replace(hiddenDelimRegex, '');
|
|
545
484
|
}
|
|
546
|
-
// Remove caret-anchor spans (invisible, non-semantic)
|
|
547
485
|
const caretAnchorRegex = /<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig;
|
|
548
486
|
segmentHTML = segmentHTML.replace(caretAnchorRegex, '');
|
|
549
|
-
// If, after stripping tags/entities, the content is empty, serialize as empty string
|
|
550
487
|
const textCheck = segmentHTML.replace(/<[^>]*>/g, '').replace(/ /ig, ' ').trim();
|
|
551
488
|
if (textCheck === '') segmentHTML = '';
|
|
552
489
|
return segmentHTML;
|
|
553
490
|
});
|
|
554
|
-
|
|
491
|
+
|
|
555
492
|
if (cellHTMLSegments.length !== domCols) {
|
|
556
493
|
// log(`${funcName}: WARNING Line ${lineNum} (Fallback): Reconstructed cell count (${cellHTMLSegments.length}) does not match DOM cols (${domCols}).`);
|
|
557
494
|
while(cellHTMLSegments.length < domCols) cellHTMLSegments.push('');
|
|
@@ -582,10 +519,8 @@ exports.collectContentPre = (hook, ctx) => {
|
|
|
582
519
|
} else {
|
|
583
520
|
// log(`${funcName}: Node is not an ace-line (or node is null). Node: ${node?.tagName}.${node?.className}. Allowing normal processing.`);
|
|
584
521
|
}
|
|
585
|
-
// ***** END Primary Path *****
|
|
586
522
|
|
|
587
523
|
|
|
588
|
-
// ***** Secondary Path: Apply attributes from tbljson-* class on spans (for initial creation/pasting) *****
|
|
589
524
|
const classes = ctx.cls ? ctx.cls.split(' ') : [];
|
|
590
525
|
let appliedAttribFromClass = false;
|
|
591
526
|
if (classes.length > 0) {
|
|
@@ -617,38 +552,31 @@ exports.collectContentPre = (hook, ctx) => {
|
|
|
617
552
|
} else {
|
|
618
553
|
// log(`${funcName}: Secondary path - Node ${node?.tagName}.${node?.className} has no ctx.cls or classes array is empty.`);
|
|
619
554
|
}
|
|
620
|
-
|
|
555
|
+
|
|
621
556
|
// log(`${funcName}: *** EXIT POINT *** For Node: ${node?.tagName}.${node?.className}. Applied from class: ${appliedAttribFromClass}`);
|
|
622
557
|
};
|
|
623
558
|
|
|
624
|
-
// ───────────── attribute → span‑class mapping (linestylefilter hook) ─────────
|
|
625
559
|
exports.aceAttribsToClasses = (hook, ctx) => {
|
|
626
560
|
const funcName = 'aceAttribsToClasses';
|
|
627
561
|
// log(`>>>> ${funcName}: Called with key: ${ctx.key}`); // log entry
|
|
628
562
|
if (ctx.key === ATTR_TABLE_JSON) {
|
|
629
563
|
// log(`${funcName}: Processing ATTR_TABLE_JSON.`);
|
|
630
|
-
// ctx.value is the raw JSON string from Etherpad's attribute pool
|
|
631
564
|
const rawJsonValue = ctx.value;
|
|
632
565
|
// log(`${funcName}: Received raw attribute value (ctx.value):`, rawJsonValue);
|
|
633
566
|
|
|
634
|
-
// Attempt to parse for logging purposes
|
|
635
567
|
let parsedMetadataForLog = '[JSON Parse Error]';
|
|
636
568
|
try {
|
|
637
569
|
parsedMetadataForLog = JSON.parse(rawJsonValue);
|
|
638
570
|
// log(`${funcName}: Value parsed for logging:`, parsedMetadataForLog);
|
|
639
571
|
} catch(e) {
|
|
640
572
|
// log(`${funcName}: Error parsing raw JSON value for logging:`, e);
|
|
641
|
-
// Continue anyway, enc() might still work if it's just a string
|
|
642
573
|
}
|
|
643
574
|
|
|
644
|
-
// Generate the class name by base64 encoding the raw JSON string.
|
|
645
|
-
// This ensures acePostWriteDomLineHTML receives the expected encoded format.
|
|
646
575
|
const className = `tbljson-${enc(rawJsonValue)}`;
|
|
647
576
|
// log(`${funcName}: Generated class name by encoding raw JSON: ${className}`);
|
|
648
577
|
return [className];
|
|
649
578
|
}
|
|
650
579
|
if (ctx.key === ATTR_CELL) {
|
|
651
|
-
// Keep this in case we want cell-specific styling later
|
|
652
580
|
//// log(`${funcName}: Processing ATTR_CELL: ${ctx.value}`); // Optional: Uncomment if needed
|
|
653
581
|
return [`tblCell-${ctx.value}`];
|
|
654
582
|
}
|
|
@@ -656,14 +584,8 @@ exports.aceAttribsToClasses = (hook, ctx) => {
|
|
|
656
584
|
return [];
|
|
657
585
|
};
|
|
658
586
|
|
|
659
|
-
// ───────────── line‑class mapping (REMOVE - superseded by aceAttribsToClasses) ─────────
|
|
660
|
-
// exports.aceLineAttribsToClasses = ... (Removed as aceAttribsToClasses adds table-line-data now)
|
|
661
587
|
|
|
662
|
-
// ─────────────────── Create Initial DOM Structure ────────────────────
|
|
663
|
-
// REMOVED - This hook doesn't reliably trigger on attribute changes during creation.
|
|
664
|
-
// exports.aceCreateDomLine = (hookName, args, cb) => { ... };
|
|
665
588
|
|
|
666
|
-
// Helper function to escape HTML (Keep this helper)
|
|
667
589
|
function escapeHtml(text = '') {
|
|
668
590
|
const strText = String(text);
|
|
669
591
|
var map = {
|
|
@@ -671,8 +593,6 @@ function escapeHtml(text = '') {
|
|
|
671
593
|
};
|
|
672
594
|
return strText.replace(/[&<>"'']/g, function(m) { return map[m]; });
|
|
673
595
|
}
|
|
674
|
-
|
|
675
|
-
// NEW Helper function to build table HTML from pre-rendered delimited content with resize handles
|
|
676
596
|
function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
|
|
677
597
|
const funcName = 'buildTableFromDelimitedHTML';
|
|
678
598
|
// log(`${funcName}: START`, { metadata, innerHTMLSegments });
|
|
@@ -680,14 +600,12 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
|
|
|
680
600
|
if (!metadata || typeof metadata.tblId === 'undefined' || typeof metadata.row === 'undefined') {
|
|
681
601
|
console.error(`[ep_data_tables] ${funcName}: Invalid or missing metadata. Aborting.`);
|
|
682
602
|
// log(`${funcName}: END - Error`);
|
|
683
|
-
return '<table class="dataTable dataTable-error"><tbody><tr><td>Error: Missing table metadata</td></tr></tbody></table>';
|
|
603
|
+
return '<table class="dataTable dataTable-error"><tbody><tr><td>Error: Missing table metadata</td></tr></tbody></table>';
|
|
684
604
|
}
|
|
685
605
|
|
|
686
|
-
// Get column widths from metadata, or use equal distribution if not set
|
|
687
606
|
const numCols = innerHTMLSegments.length;
|
|
688
607
|
const columnWidths = metadata.columnWidths || Array(numCols).fill(100 / numCols);
|
|
689
|
-
|
|
690
|
-
// Ensure we have the right number of column widths
|
|
608
|
+
|
|
691
609
|
while (columnWidths.length < numCols) {
|
|
692
610
|
columnWidths.push(100 / numCols);
|
|
693
611
|
}
|
|
@@ -695,20 +613,14 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
|
|
|
695
613
|
columnWidths.splice(numCols);
|
|
696
614
|
}
|
|
697
615
|
|
|
698
|
-
|
|
699
|
-
const tdStyle = `padding: 5px 7px; word-wrap:break-word; vertical-align: top; border: 1px solid #000; position: relative;`; // Added position: relative
|
|
616
|
+
const tdStyle = `padding: 5px 7px; word-wrap:break-word; vertical-align: top; border: 1px solid #000; position: relative;`;
|
|
700
617
|
|
|
701
|
-
// Precompute encoded tbljson class so empty cells can carry the same marker
|
|
702
618
|
let encodedTbljsonClass = '';
|
|
703
619
|
try {
|
|
704
620
|
encodedTbljsonClass = `tbljson-${enc(JSON.stringify(metadata))}`;
|
|
705
621
|
} catch (_) { encodedTbljsonClass = ''; }
|
|
706
622
|
|
|
707
|
-
// Map the HTML segments directly into TD elements with column widths
|
|
708
623
|
const cellsHtml = innerHTMLSegments.map((segment, index) => {
|
|
709
|
-
// Build the hidden delimiter *inside* the first author span so the caret
|
|
710
|
-
// cannot sit between delimiter and text. For empty cells, synthesize a span
|
|
711
|
-
// that carries tbljson and tblCell-N so caret anchoring remains stable.
|
|
712
624
|
const textOnly = (segment || '').replace(/<[^>]*>/g, '').replace(/ /ig, ' ').trim();
|
|
713
625
|
let modifiedSegment = segment || '';
|
|
714
626
|
const isEmpty = !segment || textOnly === '';
|
|
@@ -718,14 +630,10 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
|
|
|
718
630
|
}
|
|
719
631
|
if (index > 0) {
|
|
720
632
|
const delimSpan = `<span class="ep-data_tables-delim" contenteditable="false">${HIDDEN_DELIM}</span>`;
|
|
721
|
-
// If the rendered segment already starts with a <span …> (which will be
|
|
722
|
-
// the usual author-colour wrapper) inject the delimiter right after that
|
|
723
|
-
// opening tag; otherwise just prefix it.
|
|
724
633
|
modifiedSegment = modifiedSegment.replace(/^(<span[^>]*>)/i, `$1${delimSpan}`);
|
|
725
634
|
if (!/^<span[^>]*>/i.test(modifiedSegment)) modifiedSegment = `${delimSpan}${modifiedSegment}`;
|
|
726
635
|
}
|
|
727
636
|
|
|
728
|
-
// --- NEW: Always embed the invisible caret-anchor as *last* child *within* the first span ---
|
|
729
637
|
const caretAnchorSpan = '<span class="ep-data_tables-caret-anchor" contenteditable="false"></span>';
|
|
730
638
|
const anchorInjected = modifiedSegment.replace(/<\/span>\s*$/i, `${caretAnchorSpan}</span>`);
|
|
731
639
|
modifiedSegment = (anchorInjected !== modifiedSegment)
|
|
@@ -734,7 +642,36 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
|
|
|
734
642
|
? `<span class="${encodedTbljsonClass ? `${encodedTbljsonClass} ` : ''}tblCell-${index}">${modifiedSegment}${caretAnchorSpan}</span>`
|
|
735
643
|
: `${modifiedSegment}${caretAnchorSpan}`);
|
|
736
644
|
|
|
737
|
-
|
|
645
|
+
try {
|
|
646
|
+
const requiredCellClass = `tblCell-${index}`;
|
|
647
|
+
const leadingDelimMatch = modifiedSegment.match(/^\s*<span[^>]*\bep-data_tables-delim\b[^>]*>[\s\S]*?<\/span>\s*/i);
|
|
648
|
+
const head = leadingDelimMatch ? leadingDelimMatch[0] : '';
|
|
649
|
+
const tail = leadingDelimMatch ? modifiedSegment.slice(head.length) : modifiedSegment;
|
|
650
|
+
|
|
651
|
+
const openSpanMatch = tail.match(/^\s*<span([^>]*)>/i);
|
|
652
|
+
if (!openSpanMatch) {
|
|
653
|
+
const baseClasses = `${encodedTbljsonClass ? `${encodedTbljsonClass} ` : ''}${requiredCellClass}`;
|
|
654
|
+
modifiedSegment = `${head}<span class="${baseClasses}">${tail}</span>`;
|
|
655
|
+
} else {
|
|
656
|
+
const fullOpen = openSpanMatch[0];
|
|
657
|
+
const attrs = openSpanMatch[1] || '';
|
|
658
|
+
const classMatch = /\bclass\s*=\s*"([^"]*)"/i.exec(attrs);
|
|
659
|
+
let classList = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
|
|
660
|
+
classList = classList.filter(c => !/^tblCell-\d+$/.test(c));
|
|
661
|
+
classList.push(requiredCellClass);
|
|
662
|
+
const unique = Array.from(new Set(classList));
|
|
663
|
+
const newClassAttr = ` class="${unique.join(' ')}"`;
|
|
664
|
+
const attrsWithoutClass = classMatch ? attrs.replace(/\s*class\s*=\s*"[^"]*"/i, '') : attrs;
|
|
665
|
+
const rebuiltOpen = `<span${newClassAttr}${attrsWithoutClass}>`;
|
|
666
|
+
const afterOpen = tail.slice(fullOpen.length);
|
|
667
|
+
const cleanedTail = afterOpen.replace(/(<span[^>]*class=")([^"]*)(")/ig, (m, p1, classes, p3) => {
|
|
668
|
+
const filtered = classes.split(/\s+/).filter(c => c && !/^tblCell-\d+$/.test(c)).join(' ');
|
|
669
|
+
return p1 + filtered + p3;
|
|
670
|
+
});
|
|
671
|
+
modifiedSegment = head + rebuiltOpen + cleanedTail;
|
|
672
|
+
}
|
|
673
|
+
} catch (_) { /* ignore normalization errors */ }
|
|
674
|
+
|
|
738
675
|
const widthPercent = columnWidths[index] || (100 / numCols);
|
|
739
676
|
const cellStyle = `${tdStyle} width: ${widthPercent}%;`;
|
|
740
677
|
|
|
@@ -747,27 +684,22 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
|
|
|
747
684
|
}).join('');
|
|
748
685
|
// log(`${funcName}: Joined all cellsHtml:`, cellsHtml);
|
|
749
686
|
|
|
750
|
-
// Add 'dataTable-first-row' class if it's the logical first row (row index 0)
|
|
751
687
|
const firstRowClass = metadata.row === 0 ? ' dataTable-first-row' : '';
|
|
752
688
|
// log(`${funcName}: First row class applied: '${firstRowClass}'`);
|
|
753
689
|
|
|
754
|
-
// Construct the final table HTML
|
|
755
|
-
// Rely on CSS for border-collapse, width etc. Add data attributes from metadata.
|
|
756
690
|
const tableHtml = `<table class="dataTable${firstRowClass}" writingsuggestions="false" data-tblId="${metadata.tblId}" data-row="${metadata.row}" style="width:100%; border-collapse: collapse; table-layout: fixed;" draggable="false"><tbody><tr>${cellsHtml}</tr></tbody></table>`;
|
|
757
691
|
// log(`${funcName}: Generated final table HTML:`, tableHtml);
|
|
758
692
|
// log(`${funcName}: END - Success`);
|
|
759
693
|
return tableHtml;
|
|
760
694
|
}
|
|
761
695
|
|
|
762
|
-
// ───────────────── Populate Table Cells / Render (PostWrite) ──────────────────
|
|
763
696
|
exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
764
697
|
const funcName = 'acePostWriteDomLineHTML';
|
|
765
698
|
const node = args?.node;
|
|
766
699
|
const nodeId = node?.id;
|
|
767
|
-
const lineNum = args?.lineNumber;
|
|
768
|
-
const logPrefix = '[ep_data_tables:acePostWriteDomLineHTML]';
|
|
700
|
+
const lineNum = args?.lineNumber;
|
|
701
|
+
const logPrefix = '[ep_data_tables:acePostWriteDomLineHTML]';
|
|
769
702
|
|
|
770
|
-
// *** STARTUP LOGGING ***
|
|
771
703
|
// log(`${logPrefix} ----- START ----- NodeID: ${nodeId} LineNum: ${lineNum}`);
|
|
772
704
|
if (!node || !nodeId) {
|
|
773
705
|
// log(`${logPrefix} ERROR - Received invalid node or node without ID. Aborting.`);
|
|
@@ -775,15 +707,13 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
775
707
|
return cb();
|
|
776
708
|
}
|
|
777
709
|
|
|
778
|
-
// *** ENHANCED DEBUG: Log complete DOM state ***
|
|
779
710
|
// log(`${logPrefix} NodeID#${nodeId}: COMPLETE DOM STRUCTURE DEBUG:`);
|
|
780
711
|
// log(`${logPrefix} NodeID#${nodeId}: Node tagName: ${node.tagName}`);
|
|
781
712
|
// log(`${logPrefix} NodeID#${nodeId}: Node className: ${node.className}`);
|
|
782
713
|
// log(`${logPrefix} NodeID#${nodeId}: Node innerHTML length: ${node.innerHTML?.length || 0}`);
|
|
783
714
|
// log(`${logPrefix} NodeID#${nodeId}: Node innerHTML (first 500 chars): "${(node.innerHTML || '').substring(0, 500)}"`);
|
|
784
715
|
// log(`${logPrefix} NodeID#${nodeId}: Node children count: ${node.children?.length || 0}`);
|
|
785
|
-
|
|
786
|
-
// log all child elements and their classes
|
|
716
|
+
|
|
787
717
|
if (node.children) {
|
|
788
718
|
for (let i = 0; i < Math.min(node.children.length, 10); i++) {
|
|
789
719
|
const child = node.children[i];
|
|
@@ -797,15 +727,12 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
797
727
|
let rowMetadata = null;
|
|
798
728
|
let encodedJsonString = null;
|
|
799
729
|
|
|
800
|
-
// --- 1. Find and Parse Metadata Attribute ---
|
|
801
730
|
// log(`${logPrefix} NodeID#${nodeId}: Searching for tbljson-* class...`);
|
|
802
|
-
|
|
803
|
-
// ENHANCED Helper function to recursively search for tbljson class in all descendants
|
|
731
|
+
|
|
804
732
|
function findTbljsonClass(element, depth = 0, path = '') {
|
|
805
733
|
const indent = ' '.repeat(depth);
|
|
806
734
|
// log(`${logPrefix} NodeID#${nodeId}: ${indent}Searching element: ${element.tagName || 'unknown'}, path: ${path}`);
|
|
807
|
-
|
|
808
|
-
// Check the element itself
|
|
735
|
+
|
|
809
736
|
if (element.classList) {
|
|
810
737
|
// log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has ${element.classList.length} classes: [${Array.from(element.classList).join(', ')}]`);
|
|
811
738
|
for (const cls of element.classList) {
|
|
@@ -817,8 +744,7 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
817
744
|
} else {
|
|
818
745
|
// log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has no classList`);
|
|
819
746
|
}
|
|
820
|
-
|
|
821
|
-
// Recursively check all descendants
|
|
747
|
+
|
|
822
748
|
if (element.children) {
|
|
823
749
|
// log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has ${element.children.length} children`);
|
|
824
750
|
for (let i = 0; i < element.children.length; i++) {
|
|
@@ -833,54 +759,45 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
833
759
|
} else {
|
|
834
760
|
// log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has no children`);
|
|
835
761
|
}
|
|
836
|
-
|
|
762
|
+
|
|
837
763
|
// log(`${logPrefix} NodeID#${nodeId}: ${indent}No tbljson class found in this element or its children`);
|
|
838
764
|
return null;
|
|
839
765
|
}
|
|
840
766
|
|
|
841
|
-
// Search for tbljson class starting from the node
|
|
842
767
|
// log(`${logPrefix} NodeID#${nodeId}: Starting recursive search for tbljson class...`);
|
|
843
768
|
encodedJsonString = findTbljsonClass(node, 0, 'ROOT');
|
|
844
|
-
|
|
769
|
+
|
|
845
770
|
if (encodedJsonString) {
|
|
846
771
|
// log(`${logPrefix} NodeID#${nodeId}: *** SUCCESS: Found encoded tbljson class: ${encodedJsonString} ***`);
|
|
847
772
|
} else {
|
|
848
773
|
// log(`${logPrefix} NodeID#${nodeId}: *** NO TBLJSON CLASS FOUND ***`);
|
|
849
774
|
}
|
|
850
775
|
|
|
851
|
-
// If no attribute found, it's not a table line managed by us
|
|
852
776
|
if (!encodedJsonString) {
|
|
853
777
|
// log(`${logPrefix} NodeID#${nodeId}: No tbljson-* class found. Assuming not a table line. END.`);
|
|
854
|
-
|
|
855
|
-
// DEBUG: Add detailed logging to understand why tbljson class is missing
|
|
778
|
+
|
|
856
779
|
// log(`${logPrefix} NodeID#${nodeId}: DEBUG - Node tag: ${node.tagName}, Node classes:`, Array.from(node.classList || []));
|
|
857
780
|
// log(`${logPrefix} NodeID#${nodeId}: DEBUG - Node innerHTML (first 200 chars): "${(node.innerHTML || '').substring(0, 200)}"`);
|
|
858
|
-
|
|
859
|
-
// Check if there are any child elements with classes
|
|
781
|
+
|
|
860
782
|
if (node.children && node.children.length > 0) {
|
|
861
783
|
for (let i = 0; i < Math.min(node.children.length, 5); i++) {
|
|
862
784
|
const child = node.children[i];
|
|
863
785
|
// log(`${logPrefix} NodeID#${nodeId}: DEBUG - Child ${i} tag: ${child.tagName}, classes:`, Array.from(child.classList || []));
|
|
864
786
|
}
|
|
865
787
|
}
|
|
866
|
-
|
|
867
|
-
// Check if there's already a table in this node (orphaned table)
|
|
788
|
+
|
|
868
789
|
const existingTable = node.querySelector('table.dataTable[data-tblId]');
|
|
869
790
|
if (existingTable) {
|
|
870
791
|
const existingTblId = existingTable.getAttribute('data-tblId');
|
|
871
792
|
const existingRow = existingTable.getAttribute('data-row');
|
|
872
793
|
// log(`${logPrefix} NodeID#${nodeId}: DEBUG - Found orphaned table! TblId: ${existingTblId}, Row: ${existingRow}`);
|
|
873
|
-
|
|
874
|
-
// This suggests the table exists but the tbljson class was lost
|
|
875
|
-
// Check if we're in a post-resize situation
|
|
794
|
+
|
|
876
795
|
if (existingTblId && existingRow !== null) {
|
|
877
796
|
// log(`${logPrefix} NodeID#${nodeId}: POTENTIAL ISSUE - Table exists but no tbljson class. This may be a post-resize issue.`);
|
|
878
|
-
|
|
879
|
-
// Try to look up what the metadata should be based on the table attributes
|
|
797
|
+
|
|
880
798
|
const tableCells = existingTable.querySelectorAll('td');
|
|
881
799
|
// log(`${logPrefix} NodeID#${nodeId}: Table has ${tableCells.length} cells`);
|
|
882
|
-
|
|
883
|
-
// log the current line's attribute state if we can get line number
|
|
800
|
+
|
|
884
801
|
if (lineNum !== undefined && args?.documentAttributeManager) {
|
|
885
802
|
try {
|
|
886
803
|
const currentLineAttr = args.documentAttributeManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
@@ -891,22 +808,14 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
891
808
|
}
|
|
892
809
|
}
|
|
893
810
|
}
|
|
894
|
-
|
|
811
|
+
|
|
895
812
|
return cb();
|
|
896
813
|
}
|
|
897
814
|
|
|
898
|
-
// *** NEW CHECK: If table already rendered, skip regeneration ***
|
|
899
815
|
const existingTable = node.querySelector('table.dataTable[data-tblId]');
|
|
900
816
|
if (existingTable) {
|
|
901
817
|
// log(`${logPrefix} NodeID#${nodeId}: Table already exists in DOM. Skipping innerHTML replacement.`);
|
|
902
|
-
|
|
903
|
-
// const existingTblId = existingTable.getAttribute('data-tblId');
|
|
904
|
-
// try {
|
|
905
|
-
// const decoded = dec(encodedJsonString);
|
|
906
|
-
// const currentMetadata = JSON.parse(decoded);
|
|
907
|
-
// if (existingTblId === currentMetadata?.tblId) { ... }
|
|
908
|
-
// } catch(e) { /* ignore validation error */ }
|
|
909
|
-
return cb(); // Do nothing further
|
|
818
|
+
return cb();
|
|
910
819
|
}
|
|
911
820
|
|
|
912
821
|
// log(`${logPrefix} NodeID#${nodeId}: Decoding and parsing metadata...`);
|
|
@@ -917,7 +826,6 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
917
826
|
rowMetadata = JSON.parse(decoded);
|
|
918
827
|
// log(`${logPrefix} NodeID#${nodeId}: Parsed rowMetadata:`, rowMetadata);
|
|
919
828
|
|
|
920
|
-
// Validate essential metadata
|
|
921
829
|
if (!rowMetadata || typeof rowMetadata.tblId === 'undefined' || typeof rowMetadata.row === 'undefined' || typeof rowMetadata.cols !== 'number') {
|
|
922
830
|
throw new Error('Invalid or incomplete metadata (missing tblId, row, or cols).');
|
|
923
831
|
}
|
|
@@ -926,32 +834,20 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
926
834
|
} catch(e) {
|
|
927
835
|
// log(`${logPrefix} NodeID#${nodeId}: FATAL ERROR - Failed to decode/parse/validate tbljson metadata. Rendering cannot proceed.`, e);
|
|
928
836
|
console.error(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Failed to decode/parse/validate tbljson.`, encodedJsonString, e);
|
|
929
|
-
// Optionally render an error state in the node?
|
|
930
837
|
node.innerHTML = '<div style="color:red; border: 1px solid red; padding: 5px;">[ep_data_tables] Error: Invalid table metadata attribute found.</div>';
|
|
931
838
|
// log(`${logPrefix} NodeID#${nodeId}: Rendered error message in node. END.`);
|
|
932
839
|
return cb();
|
|
933
840
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
// --- 2. Get and Parse Line Content ---
|
|
937
|
-
// ALWAYS get the innerHTML of the line div itself to preserve all styling spans and attributes.
|
|
938
|
-
// This innerHTML is set by Etherpad based on the line's current text in atext and includes
|
|
939
|
-
// all the span elements with author colors, bold, italic, and other styling.
|
|
940
|
-
// For an imported line's first render, atext is "Cell1|Cell2", so node.innerHTML will be "Cell1|Cell2".
|
|
941
|
-
// For a natively created line, node.innerHTML is also "Cell1|Cell2".
|
|
942
|
-
// After an edit, aceKeyEvent updates atext, and node.innerHTML reflects that new "EditedCell1|Cell2" string.
|
|
943
|
-
// When styling is applied, it will include spans like: <span class="author-xxx bold">Cell1</span>|<span class="author-yyy italic">Cell2</span>
|
|
841
|
+
|
|
944
842
|
const delimitedTextFromLine = node.innerHTML;
|
|
945
843
|
// log(`${logPrefix} NodeID#${nodeId}: Using node.innerHTML for delimited text to preserve styling.`);
|
|
946
844
|
// log(`${logPrefix} NodeID#${nodeId}: Raw innerHTML length: ${delimitedTextFromLine?.length || 0}`);
|
|
947
845
|
// log(`${logPrefix} NodeID#${nodeId}: Raw innerHTML (first 1000 chars): "${(delimitedTextFromLine || '').substring(0, 1000)}"`);
|
|
948
|
-
|
|
949
|
-
// *** ENHANCED DEBUG: Analyze delimiter presence ***
|
|
846
|
+
|
|
950
847
|
const delimiterCount = (delimitedTextFromLine || '').split(DELIMITER).length - 1;
|
|
951
848
|
// log(`${logPrefix} NodeID#${nodeId}: Delimiter '${DELIMITER}' count in innerHTML: ${delimiterCount}`);
|
|
952
849
|
// log(`${logPrefix} NodeID#${nodeId}: Expected delimiters for ${rowMetadata.cols} columns: ${rowMetadata.cols - 1}`);
|
|
953
850
|
|
|
954
|
-
// log all delimiter positions
|
|
955
851
|
let pos = -1;
|
|
956
852
|
const delimiterPositions = [];
|
|
957
853
|
while ((pos = delimitedTextFromLine.indexOf(DELIMITER, pos + 1)) !== -1) {
|
|
@@ -959,18 +855,18 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
959
855
|
// log(`${logPrefix} NodeID#${nodeId}: Delimiter found at position ${pos}, context: "${delimitedTextFromLine.substring(Math.max(0, pos - 20), pos + 21)}"`);
|
|
960
856
|
}
|
|
961
857
|
// log(`${logPrefix} NodeID#${nodeId}: All delimiter positions: [${delimiterPositions.join(', ')}]`);
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
// NEW: Remove all hidden-delimiter <span> wrappers **before** we split so
|
|
965
|
-
// the embedded delimiter character they carry doesn't inflate or shrink
|
|
966
|
-
// the segment count.
|
|
967
|
-
const spanDelimRegex = new RegExp('<span class="ep-data_tables-delim"[^>]*>' + DELIMITER + '<\\/span>', 'ig');
|
|
858
|
+
|
|
859
|
+
const spanDelimRegex = /<span class="ep-data_tables-delim"[^>]*>[\s\S]*?<\/span>/ig;
|
|
968
860
|
const sanitizedHTMLForSplit = (delimitedTextFromLine || '')
|
|
969
|
-
.replace(spanDelimRegex,
|
|
970
|
-
|
|
971
|
-
.replace(
|
|
861
|
+
.replace(spanDelimRegex, DELIMITER)
|
|
862
|
+
.replace(/<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig, '')
|
|
863
|
+
.replace(/\r?\n/g, ' ')
|
|
864
|
+
.replace(/<br\s*\/?>/gi, ' ')
|
|
865
|
+
.replace(/\u00A0/gu, ' ')
|
|
866
|
+
.replace(/[\u200B\u200C\u200D\uFEFF]/g, '')
|
|
867
|
+
.replace(/\s+/g, ' ');
|
|
972
868
|
const htmlSegments = sanitizedHTMLForSplit.split(DELIMITER);
|
|
973
|
-
|
|
869
|
+
|
|
974
870
|
// log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT ANALYSIS ***`);
|
|
975
871
|
// log(`${logPrefix} NodeID#${nodeId}: Split resulted in ${htmlSegments.length} segments`);
|
|
976
872
|
for (let i = 0; i < htmlSegments.length; i++) {
|
|
@@ -983,70 +879,35 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
983
879
|
if (segment.length > 400) {
|
|
984
880
|
// log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (chars 400-600): "${segment.substring(400, 600)}"`);
|
|
985
881
|
}
|
|
986
|
-
// Check if segment contains image-related content
|
|
987
882
|
if (segment.includes('image:') || segment.includes('image-placeholder') || segment.includes('currently-selected')) {
|
|
988
883
|
// log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT[${i}] CONTAINS IMAGE CONTENT ***`);
|
|
989
884
|
}
|
|
885
|
+
try {
|
|
886
|
+
const tblCellMatches = segment.match(/\btblCell-(\d+)\b/g) || [];
|
|
887
|
+
const tbljsonMatches = segment.match(/\btbljson-[A-Za-z0-9_-]+\b/g) || [];
|
|
888
|
+
const uniqueCells = Array.from(new Set(tblCellMatches));
|
|
889
|
+
if (uniqueCells.length > 1) {
|
|
890
|
+
console.warn('[ep_data_tables][diag] segment contains multiple tblCell-* markers', { segIndex: i, uniqueCells });
|
|
891
|
+
}
|
|
892
|
+
} catch (_) {}
|
|
990
893
|
}
|
|
991
894
|
|
|
992
895
|
// log(`${logPrefix} NodeID#${nodeId}: Parsed HTML segments (${htmlSegments.length}):`, htmlSegments.map(s => (s || '').substring(0,50) + (s && s.length > 50 ? '...' : '')));
|
|
993
896
|
|
|
994
|
-
// --- Enhanced Validation with Automatic Structure Reconstruction ---
|
|
995
897
|
let finalHtmlSegments = htmlSegments;
|
|
996
|
-
|
|
898
|
+
|
|
997
899
|
if (htmlSegments.length !== rowMetadata.cols) {
|
|
998
900
|
// log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH DETECTED *** - Attempting reconstruction.`);
|
|
999
|
-
|
|
1000
|
-
|
|
901
|
+
console.warn('[ep_data_tables][diag] Segment/column mismatch', { nodeId, lineNum, segs: htmlSegments.length, cols: rowMetadata.cols, tblId: rowMetadata.tblId, row: rowMetadata.row });
|
|
902
|
+
|
|
1001
903
|
const hasImageSelected = delimitedTextFromLine.includes('currently-selected');
|
|
1002
904
|
const hasImageContent = delimitedTextFromLine.includes('image:');
|
|
1003
905
|
if (hasImageSelected) {
|
|
1004
906
|
// log(`${logPrefix} NodeID#${nodeId}: *** POTENTIAL CAUSE: Image selection state may be affecting segment parsing ***`);
|
|
1005
907
|
}
|
|
1006
908
|
|
|
1007
|
-
// First attempt: reconstruct using DOM spans that carry tblCell-N classes
|
|
1008
909
|
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
910
|
|
|
1049
|
-
// Fallback: reconstruct from string segments
|
|
1050
911
|
if (!usedClassReconstruction) {
|
|
1051
912
|
const reconstructedSegments = [];
|
|
1052
913
|
if (htmlSegments.length === 1 && rowMetadata.cols > 1) {
|
|
@@ -1068,7 +929,6 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
1068
929
|
finalHtmlSegments = reconstructedSegments;
|
|
1069
930
|
}
|
|
1070
931
|
|
|
1071
|
-
// Only warn if we still don't have the right number of segments
|
|
1072
932
|
if (finalHtmlSegments.length !== rowMetadata.cols) {
|
|
1073
933
|
console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Could not reconstruct to expected ${rowMetadata.cols} segments. Got ${finalHtmlSegments.length}.`);
|
|
1074
934
|
}
|
|
@@ -1076,22 +936,17 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
1076
936
|
// log(`${logPrefix} NodeID#${nodeId}: Segment count matches metadata cols (${rowMetadata.cols}). Using original segments.`);
|
|
1077
937
|
}
|
|
1078
938
|
|
|
1079
|
-
// --- 3. Build and Render Table ---
|
|
1080
939
|
// log(`${logPrefix} NodeID#${nodeId}: Calling buildTableFromDelimitedHTML...`);
|
|
1081
940
|
try {
|
|
1082
941
|
const newTableHTML = buildTableFromDelimitedHTML(rowMetadata, finalHtmlSegments);
|
|
1083
942
|
// log(`${logPrefix} NodeID#${nodeId}: Received new table HTML from helper. Replacing content.`);
|
|
1084
|
-
|
|
1085
|
-
// The old local findTbljsonElement is removed from here. We use the global one now.
|
|
943
|
+
|
|
1086
944
|
const tbljsonElement = findTbljsonElement(node);
|
|
1087
|
-
|
|
1088
|
-
// If we found a tbljson element and it's nested in a block element,
|
|
1089
|
-
// we need to preserve the block wrapper while replacing the content
|
|
945
|
+
|
|
1090
946
|
if (tbljsonElement && tbljsonElement.parentElement && tbljsonElement.parentElement !== node) {
|
|
1091
|
-
// Check if the parent is a block-level element that should be preserved
|
|
1092
947
|
const parentTag = tbljsonElement.parentElement.tagName.toLowerCase();
|
|
1093
948
|
const blockElements = ['center', 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'right', 'left', 'ul', 'ol', 'li', 'code'];
|
|
1094
|
-
|
|
949
|
+
|
|
1095
950
|
if (blockElements.includes(parentTag)) {
|
|
1096
951
|
// log(`${logPrefix} NodeID#${nodeId}: Preserving block element ${parentTag} and replacing its content with table.`);
|
|
1097
952
|
tbljsonElement.parentElement.innerHTML = newTableHTML;
|
|
@@ -1100,11 +955,10 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
1100
955
|
node.innerHTML = newTableHTML;
|
|
1101
956
|
}
|
|
1102
957
|
} else {
|
|
1103
|
-
// Replace the node's content entirely with the generated table
|
|
1104
958
|
// log(`${logPrefix} NodeID#${nodeId}: No nested block element found, replacing entire node content.`);
|
|
1105
959
|
node.innerHTML = newTableHTML;
|
|
1106
960
|
}
|
|
1107
|
-
|
|
961
|
+
|
|
1108
962
|
// log(`${logPrefix} NodeID#${nodeId}: Successfully replaced content with new table structure.`);
|
|
1109
963
|
} catch (renderError) {
|
|
1110
964
|
// log(`${logPrefix} NodeID#${nodeId}: ERROR during table building or rendering.`, renderError);
|
|
@@ -1113,19 +967,13 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
1113
967
|
// log(`${logPrefix} NodeID#${nodeId}: Rendered build/render error message in node. END.`);
|
|
1114
968
|
return cb();
|
|
1115
969
|
}
|
|
1116
|
-
// --- End Table Building ---
|
|
1117
970
|
|
|
1118
|
-
// *** REMOVED CACHING LOGIC ***
|
|
1119
|
-
// The old logic based on tableRowNodes cache is completely removed.
|
|
1120
971
|
|
|
1121
972
|
// log(`${logPrefix}: ----- END ----- NodeID: ${nodeId}`);
|
|
1122
973
|
return cb();
|
|
1123
974
|
};
|
|
1124
975
|
|
|
1125
|
-
// NEW: Helper function to get line number (adapted from ep_image_insert)
|
|
1126
|
-
// Ensure this is defined before it's used in postAceInit
|
|
1127
976
|
function _getLineNumberOfElement(element) {
|
|
1128
|
-
// Implementation similar to ep_image_insert
|
|
1129
977
|
let currentElement = element;
|
|
1130
978
|
let count = 0;
|
|
1131
979
|
while (currentElement = currentElement.previousElementSibling) {
|
|
@@ -1133,8 +981,6 @@ function _getLineNumberOfElement(element) {
|
|
|
1133
981
|
}
|
|
1134
982
|
return count;
|
|
1135
983
|
}
|
|
1136
|
-
|
|
1137
|
-
// ───────────────────── Handle Key Events ─────────────────────
|
|
1138
984
|
exports.aceKeyEvent = (h, ctx) => {
|
|
1139
985
|
const funcName = 'aceKeyEvent';
|
|
1140
986
|
const evt = ctx.evt;
|
|
@@ -1151,21 +997,17 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1151
997
|
return false;
|
|
1152
998
|
}
|
|
1153
999
|
|
|
1154
|
-
// Get caret info from event context - may be stale
|
|
1155
1000
|
const reportedLineNum = rep.selStart[0];
|
|
1156
1001
|
const reportedCol = rep.selStart[1];
|
|
1157
1002
|
// log(`${logPrefix} Reported caret from rep: Line=${reportedLineNum}, Col=${reportedCol}`);
|
|
1158
1003
|
|
|
1159
|
-
// --- Get Table Metadata for the reported line ---
|
|
1160
1004
|
let tableMetadata = null;
|
|
1161
|
-
let lineAttrString = null;
|
|
1005
|
+
let lineAttrString = null;
|
|
1162
1006
|
try {
|
|
1163
|
-
// Add debugging to see what's happening with attribute retrieval
|
|
1164
1007
|
// log(`${logPrefix} DEBUG: Attempting to get ${ATTR_TABLE_JSON} attribute from line ${reportedLineNum}`);
|
|
1165
1008
|
lineAttrString = docManager.getAttributeOnLine(reportedLineNum, ATTR_TABLE_JSON);
|
|
1166
1009
|
// log(`${logPrefix} DEBUG: getAttributeOnLine returned: ${lineAttrString ? `"${lineAttrString}"` : 'null/undefined'}`);
|
|
1167
|
-
|
|
1168
|
-
// Also check if there are any attributes on this line at all
|
|
1010
|
+
|
|
1169
1011
|
if (typeof docManager.getAttributesOnLine === 'function') {
|
|
1170
1012
|
try {
|
|
1171
1013
|
const allAttribs = docManager.getAttributesOnLine(reportedLineNum);
|
|
@@ -1174,8 +1016,7 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1174
1016
|
// log(`${logPrefix} DEBUG: Error getting all attributes:`, e);
|
|
1175
1017
|
}
|
|
1176
1018
|
}
|
|
1177
|
-
|
|
1178
|
-
// NEW: Check if there's a table in the DOM even though attribute is missing
|
|
1019
|
+
|
|
1179
1020
|
if (!lineAttrString) {
|
|
1180
1021
|
try {
|
|
1181
1022
|
const rep = editorInfo.ace_getRep();
|
|
@@ -1186,7 +1027,6 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1186
1027
|
const domTblId = tableInDOM.getAttribute('data-tblId');
|
|
1187
1028
|
const domRow = tableInDOM.getAttribute('data-row');
|
|
1188
1029
|
// log(`${logPrefix} DEBUG: Found table in DOM without attribute! TblId=${domTblId}, Row=${domRow}`);
|
|
1189
|
-
// Try to reconstruct the metadata from DOM
|
|
1190
1030
|
const domCells = tableInDOM.querySelectorAll('td');
|
|
1191
1031
|
if (domTblId && domRow !== null && domCells.length > 0) {
|
|
1192
1032
|
// log(`${logPrefix} DEBUG: Attempting to reconstruct metadata from DOM...`);
|
|
@@ -1204,28 +1044,25 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1204
1044
|
// log(`${logPrefix} DEBUG: Error checking DOM for table:`, e);
|
|
1205
1045
|
}
|
|
1206
1046
|
}
|
|
1207
|
-
|
|
1047
|
+
|
|
1208
1048
|
if (lineAttrString) {
|
|
1209
1049
|
tableMetadata = JSON.parse(lineAttrString);
|
|
1210
1050
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
|
|
1211
1051
|
// log(`${logPrefix} Line ${reportedLineNum} has attribute, but metadata invalid/missing cols.`);
|
|
1212
|
-
tableMetadata = null;
|
|
1052
|
+
tableMetadata = null;
|
|
1213
1053
|
}
|
|
1214
1054
|
} else {
|
|
1215
1055
|
// log(`${logPrefix} DEBUG: No ${ATTR_TABLE_JSON} attribute found on line ${reportedLineNum}`);
|
|
1216
|
-
// Not a table line based on reported caret line
|
|
1217
1056
|
}
|
|
1218
1057
|
} catch(e) {
|
|
1219
1058
|
console.error(`${logPrefix} Error checking/parsing line attribute for line ${reportedLineNum}.`, e);
|
|
1220
|
-
tableMetadata = null;
|
|
1059
|
+
tableMetadata = null;
|
|
1221
1060
|
}
|
|
1222
1061
|
|
|
1223
|
-
|
|
1224
|
-
const
|
|
1225
|
-
const lastClick = editor?.ep_data_tables_last_clicked; // Read shared state
|
|
1062
|
+
const editor = editorInfo.editor;
|
|
1063
|
+
const lastClick = editor?.ep_data_tables_last_clicked;
|
|
1226
1064
|
// log(`${logPrefix} Reading stored click/caret info:`, lastClick);
|
|
1227
1065
|
|
|
1228
|
-
// --- Determine the TRUE target line, cell, and caret position ---
|
|
1229
1066
|
let currentLineNum = -1;
|
|
1230
1067
|
let targetCellIndex = -1;
|
|
1231
1068
|
let relativeCaretPos = -1;
|
|
@@ -1234,9 +1071,8 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1234
1071
|
let lineText = '';
|
|
1235
1072
|
let cellTexts = [];
|
|
1236
1073
|
let metadataForTargetLine = null;
|
|
1237
|
-
let trustedLastClick = false;
|
|
1074
|
+
let trustedLastClick = false;
|
|
1238
1075
|
|
|
1239
|
-
// ** Scenario 1: Try to trust lastClick info **
|
|
1240
1076
|
if (lastClick) {
|
|
1241
1077
|
// log(`${logPrefix} Attempting to validate stored click info for Line=${lastClick.lineNum}...`);
|
|
1242
1078
|
let storedLineAttrString = null;
|
|
@@ -1245,21 +1081,20 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1245
1081
|
// log(`${logPrefix} DEBUG: Getting ${ATTR_TABLE_JSON} attribute from stored line ${lastClick.lineNum}`);
|
|
1246
1082
|
storedLineAttrString = docManager.getAttributeOnLine(lastClick.lineNum, ATTR_TABLE_JSON);
|
|
1247
1083
|
// log(`${logPrefix} DEBUG: Stored line attribute result: ${storedLineAttrString ? `"${storedLineAttrString}"` : 'null/undefined'}`);
|
|
1248
|
-
|
|
1084
|
+
|
|
1249
1085
|
if (storedLineAttrString) {
|
|
1250
1086
|
storedLineMetadata = JSON.parse(storedLineAttrString);
|
|
1251
1087
|
// log(`${logPrefix} DEBUG: Parsed stored metadata:`, storedLineMetadata);
|
|
1252
1088
|
}
|
|
1253
|
-
|
|
1254
|
-
// Check if metadata is valid and tblId matches
|
|
1089
|
+
|
|
1255
1090
|
if (storedLineMetadata && typeof storedLineMetadata.cols === 'number' && storedLineMetadata.tblId === lastClick.tblId) {
|
|
1256
1091
|
// log(`${logPrefix} Stored click info VALIDATED (Metadata OK and tblId matches). Trusting stored state.`);
|
|
1257
1092
|
trustedLastClick = true;
|
|
1258
1093
|
currentLineNum = lastClick.lineNum;
|
|
1259
1094
|
targetCellIndex = lastClick.cellIndex;
|
|
1260
1095
|
metadataForTargetLine = storedLineMetadata;
|
|
1261
|
-
lineAttrString = storedLineAttrString;
|
|
1262
|
-
|
|
1096
|
+
lineAttrString = storedLineAttrString;
|
|
1097
|
+
|
|
1263
1098
|
lineText = rep.lines.atIndex(currentLineNum)?.text || '';
|
|
1264
1099
|
cellTexts = lineText.split(DELIMITER);
|
|
1265
1100
|
// log(`${logPrefix} Using Line=${currentLineNum}, CellIndex=${targetCellIndex}. Text: "${lineText}"`);
|
|
@@ -1280,7 +1115,7 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1280
1115
|
relativeCaretPos = Math.max(0, Math.min(lastClick.relativePos, currentCellTextLength));
|
|
1281
1116
|
// log(`${logPrefix} Using and validated stored relative position: ${relativeCaretPos}.`);
|
|
1282
1117
|
} else {
|
|
1283
|
-
relativeCaretPos = reportedCol - cellStartCol;
|
|
1118
|
+
relativeCaretPos = reportedCol - cellStartCol;
|
|
1284
1119
|
const currentCellTextLength = cellTexts[targetCellIndex]?.length ?? 0;
|
|
1285
1120
|
relativeCaretPos = Math.max(0, Math.min(relativeCaretPos, currentCellTextLength));
|
|
1286
1121
|
// log(`${logPrefix} Stored relativePos missing, calculated from reportedCol (${reportedCol}): ${relativeCaretPos}`);
|
|
@@ -1291,20 +1126,17 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1291
1126
|
}
|
|
1292
1127
|
} catch (e) {
|
|
1293
1128
|
console.error(`${logPrefix} Error validating stored click info for line ${lastClick.lineNum}.`, e);
|
|
1294
|
-
if (editor) editor.ep_data_tables_last_clicked = null;
|
|
1129
|
+
if (editor) editor.ep_data_tables_last_clicked = null;
|
|
1295
1130
|
}
|
|
1296
1131
|
}
|
|
1297
|
-
|
|
1298
|
-
// ** Scenario 2: Fallback - Use reported line/col ONLY if stored info wasn't trusted **
|
|
1132
|
+
|
|
1299
1133
|
if (!trustedLastClick) {
|
|
1300
1134
|
// log(`${logPrefix} Fallback: Using reported caret position Line=${reportedLineNum}, Col=${reportedCol}.`);
|
|
1301
|
-
// Fetch metadata for the reported line again, in case it wasn't fetched or was invalid earlier
|
|
1302
1135
|
try {
|
|
1303
1136
|
lineAttrString = docManager.getAttributeOnLine(reportedLineNum, ATTR_TABLE_JSON);
|
|
1304
1137
|
if (lineAttrString) tableMetadata = JSON.parse(lineAttrString);
|
|
1305
1138
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number') tableMetadata = null;
|
|
1306
|
-
|
|
1307
|
-
// If no attribute found directly, check if there's a table in the DOM even though attribute is missing (block styles)
|
|
1139
|
+
|
|
1308
1140
|
if (!lineAttrString) {
|
|
1309
1141
|
try {
|
|
1310
1142
|
const rep = editorInfo.ace_getRep();
|
|
@@ -1315,7 +1147,6 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1315
1147
|
const domTblId = tableInDOM.getAttribute('data-tblId');
|
|
1316
1148
|
const domRow = tableInDOM.getAttribute('data-row');
|
|
1317
1149
|
// log(`${logPrefix} Fallback: Found table in DOM without attribute! TblId=${domTblId}, Row=${domRow}`);
|
|
1318
|
-
// Try to reconstruct the metadata from DOM
|
|
1319
1150
|
const domCells = tableInDOM.querySelectorAll('td');
|
|
1320
1151
|
if (domTblId && domRow !== null && domCells.length > 0) {
|
|
1321
1152
|
// log(`${logPrefix} Fallback: Attempting to reconstruct metadata from DOM...`);
|
|
@@ -1334,17 +1165,17 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1334
1165
|
// log(`${logPrefix} Fallback: Error checking DOM for table:`, e);
|
|
1335
1166
|
}
|
|
1336
1167
|
}
|
|
1337
|
-
} catch(e) { tableMetadata = null; }
|
|
1168
|
+
} catch(e) { tableMetadata = null; }
|
|
1338
1169
|
|
|
1339
1170
|
if (!tableMetadata) {
|
|
1340
1171
|
// log(`${logPrefix} Fallback: Reported line ${reportedLineNum} is not a valid table line. Allowing default.`);
|
|
1341
1172
|
return false;
|
|
1342
1173
|
}
|
|
1343
|
-
|
|
1174
|
+
|
|
1344
1175
|
currentLineNum = reportedLineNum;
|
|
1345
1176
|
metadataForTargetLine = tableMetadata;
|
|
1346
1177
|
// log(`${logPrefix} Fallback: Processing based on reported line ${currentLineNum}.`);
|
|
1347
|
-
|
|
1178
|
+
|
|
1348
1179
|
lineText = rep.lines.atIndex(currentLineNum)?.text || '';
|
|
1349
1180
|
cellTexts = lineText.split(DELIMITER);
|
|
1350
1181
|
// log(`${logPrefix} Fallback: Fetched text for reported line ${currentLineNum}: "${lineText}"`);
|
|
@@ -1353,7 +1184,6 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1353
1184
|
// log(`${logPrefix} WARNING (Fallback): Cell count mismatch for reported line ${currentLineNum}.`);
|
|
1354
1185
|
}
|
|
1355
1186
|
|
|
1356
|
-
// Calculate target cell based on reportedCol
|
|
1357
1187
|
let currentOffset = 0;
|
|
1358
1188
|
let foundIndex = -1;
|
|
1359
1189
|
for (let i = 0; i < cellTexts.length; i++) {
|
|
@@ -1394,7 +1224,6 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1394
1224
|
targetCellIndex = foundIndex;
|
|
1395
1225
|
}
|
|
1396
1226
|
|
|
1397
|
-
// --- Final Validation ---
|
|
1398
1227
|
if (currentLineNum < 0 || targetCellIndex < 0 || !metadataForTargetLine || targetCellIndex >= metadataForTargetLine.cols) {
|
|
1399
1228
|
// log(`${logPrefix} FAILED final validation: Line=${currentLineNum}, Cell=${targetCellIndex}, Metadata=${!!metadataForTargetLine}. Allowing default.`);
|
|
1400
1229
|
if (editor) editor.ep_data_tables_last_clicked = null;
|
|
@@ -1402,9 +1231,7 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1402
1231
|
}
|
|
1403
1232
|
|
|
1404
1233
|
// log(`${logPrefix} --> Final Target: Line=${currentLineNum}, CellIndex=${targetCellIndex}, RelativePos=${relativeCaretPos}`);
|
|
1405
|
-
// --- End Cell/Position Determination ---
|
|
1406
1234
|
|
|
1407
|
-
// --- START NEW: Handle Highlight Deletion/Replacement ---
|
|
1408
1235
|
const selStartActual = rep.selStart;
|
|
1409
1236
|
const selEndActual = rep.selEnd;
|
|
1410
1237
|
const hasSelection = selStartActual[0] !== selEndActual[0] || selStartActual[1] !== selEndActual[1];
|
|
@@ -1419,11 +1246,10 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1419
1246
|
return true;
|
|
1420
1247
|
}
|
|
1421
1248
|
|
|
1422
|
-
let selectionStartColInLine = selStartActual[1];
|
|
1423
|
-
let selectionEndColInLine = selEndActual[1];
|
|
1249
|
+
let selectionStartColInLine = selStartActual[1];
|
|
1250
|
+
let selectionEndColInLine = selEndActual[1];
|
|
1424
1251
|
|
|
1425
1252
|
const currentCellFullText = cellTexts[targetCellIndex] || '';
|
|
1426
|
-
// cellStartCol is already defined and calculated based on trustedLastClick or fallback
|
|
1427
1253
|
const cellContentStartColInLine = cellStartCol;
|
|
1428
1254
|
const cellContentEndColInLine = cellStartCol + currentCellFullText.length;
|
|
1429
1255
|
|
|
@@ -1432,11 +1258,11 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1432
1258
|
const hasTrailingDelim =
|
|
1433
1259
|
targetCellIndex < metadataForTargetLine.cols - 1 &&
|
|
1434
1260
|
selectionEndColInLine === cellContentEndColInLine + DELIMITER.length;
|
|
1435
|
-
|
|
1261
|
+
|
|
1436
1262
|
const hasLeadingDelim =
|
|
1437
1263
|
targetCellIndex > 0 &&
|
|
1438
1264
|
selectionStartColInLine === cellContentStartColInLine - DELIMITER.length;
|
|
1439
|
-
|
|
1265
|
+
|
|
1440
1266
|
console.log(`[ep_data_tables:highlight-deletion] Selection analysis:`, {
|
|
1441
1267
|
targetCellIndex,
|
|
1442
1268
|
totalCols: metadataForTargetLine.cols,
|
|
@@ -1451,12 +1277,12 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1451
1277
|
hasLeadingDelim,
|
|
1452
1278
|
cellText: currentCellFullText
|
|
1453
1279
|
});
|
|
1454
|
-
|
|
1280
|
+
|
|
1455
1281
|
if (hasLeadingDelim) {
|
|
1456
1282
|
console.log(`[ep_data_tables:highlight-deletion] CLAMPING selection start from ${selectionStartColInLine} to ${cellContentStartColInLine}`);
|
|
1457
1283
|
selectionStartColInLine = cellContentStartColInLine;
|
|
1458
1284
|
}
|
|
1459
|
-
|
|
1285
|
+
|
|
1460
1286
|
if (hasTrailingDelim) {
|
|
1461
1287
|
console.log(`[ep_data_tables:highlight-deletion] CLAMPING selection end from ${selectionEndColInLine} to ${cellContentEndColInLine}`);
|
|
1462
1288
|
selectionEndColInLine = cellContentEndColInLine;
|
|
@@ -1468,34 +1294,23 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1468
1294
|
selectionStartColInLine >= cellContentStartColInLine &&
|
|
1469
1295
|
selectionEndColInLine <= cellContentEndColInLine;
|
|
1470
1296
|
|
|
1471
|
-
// Allow selection even if it starts at the very first char, but be ready to restore
|
|
1472
1297
|
|
|
1473
1298
|
if (isSelectionEntirelyWithinCell) {
|
|
1474
|
-
// Pure selection (no key pressed yet) – allow browser shortcuts such as
|
|
1475
|
-
// Ctrl-C / Ctrl-X / Cmd-C / Cmd-X to work. We only take control for
|
|
1476
|
-
// real keydown events that would modify the cell (handled further below).
|
|
1477
1299
|
|
|
1478
|
-
// 1. Non-keydown events → let them bubble (copy/cut command happens on
|
|
1479
|
-
// the subsequent "copy"/"cut" event).
|
|
1480
1300
|
if (evt.type !== 'keydown') return false;
|
|
1481
1301
|
|
|
1482
|
-
// 2. Keydown that involves modifiers (Ctrl/Cmd/Alt) → we are not going
|
|
1483
|
-
// to change the cell text, so let the browser handle it.
|
|
1484
1302
|
if (evt.ctrlKey || evt.metaKey || evt.altKey) return false;
|
|
1485
1303
|
|
|
1486
|
-
// 3. For destructive or printable keys we fall through so the specialised
|
|
1487
|
-
// highlight-deletion logic that follows can run.
|
|
1488
1304
|
}
|
|
1489
1305
|
|
|
1490
1306
|
const isCurrentKeyDelete = evt.key === 'Delete' || evt.keyCode === 46;
|
|
1491
1307
|
const isCurrentKeyBackspace = evt.key === 'Backspace' || evt.keyCode === 8;
|
|
1492
|
-
// Check if it's a printable character, not a modifier
|
|
1493
1308
|
const isCurrentKeyTyping = evt.key && evt.key.length === 1 && !evt.ctrlKey && !evt.metaKey && !evt.altKey;
|
|
1494
1309
|
|
|
1495
1310
|
|
|
1496
1311
|
if (isSelectionEntirelyWithinCell && (isCurrentKeyDelete || isCurrentKeyBackspace || isCurrentKeyTyping)) {
|
|
1497
1312
|
// log(`${logPrefix} [selection] Handling key='${evt.key}' (Type: ${evt.type}) for valid intra-cell selection.`);
|
|
1498
|
-
|
|
1313
|
+
|
|
1499
1314
|
if (evt.type !== 'keydown') {
|
|
1500
1315
|
// log(`${logPrefix} [selection] Ignoring non-keydown event type ('${evt.type}') for selection handling. Allowing default.`);
|
|
1501
1316
|
return false;
|
|
@@ -1506,16 +1321,15 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1506
1321
|
const rangeEnd = [currentLineNum, selectionEndColInLine];
|
|
1507
1322
|
let replacementText = '';
|
|
1508
1323
|
let newAbsoluteCaretCol = selectionStartColInLine;
|
|
1509
|
-
const repBeforeEdit = editorInfo.ace_getRep();
|
|
1324
|
+
const repBeforeEdit = editorInfo.ace_getRep();
|
|
1510
1325
|
// log(`${logPrefix} [caretTrace] [selection] rep.selStart before ace_performDocumentReplaceRange: Line=${repBeforeEdit.selStart[0]}, Col=${repBeforeEdit.selStart[1]}`);
|
|
1511
1326
|
|
|
1512
1327
|
if (isCurrentKeyTyping) {
|
|
1513
1328
|
replacementText = evt.key;
|
|
1514
1329
|
newAbsoluteCaretCol = selectionStartColInLine + replacementText.length;
|
|
1515
1330
|
// log(`${logPrefix} [selection] -> Replacing selected range [[${rangeStart[0]},${rangeStart[1]}],[${rangeEnd[0]},${rangeEnd[1]}]] with text '${replacementText}'`);
|
|
1516
|
-
} else {
|
|
1331
|
+
} else {
|
|
1517
1332
|
// log(`${logPrefix} [selection] -> Deleting selected range [[${rangeStart[0]},${rangeStart[1]}],[${rangeEnd[0]},${rangeEnd[1]}]]`);
|
|
1518
|
-
// If whole cell is being wiped, keep a single space so cell isn't empty
|
|
1519
1333
|
const isWholeCell = selectionStartColInLine <= cellContentStartColInLine && selectionEndColInLine >= cellContentEndColInLine;
|
|
1520
1334
|
if (isWholeCell) {
|
|
1521
1335
|
replacementText = ' ';
|
|
@@ -1525,11 +1339,8 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1525
1339
|
}
|
|
1526
1340
|
|
|
1527
1341
|
try {
|
|
1528
|
-
// const repBeforeEdit = editorInfo.ace_getRep(); // Get rep before edit for attribute helper - MOVED UP
|
|
1529
1342
|
editorInfo.ace_performDocumentReplaceRange(rangeStart, rangeEnd, replacementText);
|
|
1530
1343
|
|
|
1531
|
-
// NEW: ensure the replacement text inherits the cell attribute so the
|
|
1532
|
-
// author-span (& tblCell-N) comes back immediately
|
|
1533
1344
|
if (replacementText.length > 0) {
|
|
1534
1345
|
const attrStart = [currentLineNum, selectionStartColInLine];
|
|
1535
1346
|
const attrEnd = [currentLineNum, selectionStartColInLine + replacementText.length];
|
|
@@ -1565,14 +1376,12 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1565
1376
|
editorInfo.ace_performSelectionChange([currentLineNum, newAbsoluteCaretCol], [currentLineNum, newAbsoluteCaretCol], false);
|
|
1566
1377
|
const repAfterSelectionChange = editorInfo.ace_getRep();
|
|
1567
1378
|
// log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performSelectionChange: Line=${repAfterSelectionChange.selStart[0]}, Col=${repAfterSelectionChange.selStart[1]}`);
|
|
1568
|
-
|
|
1569
|
-
// Add sync hint AFTER setting selection
|
|
1379
|
+
|
|
1570
1380
|
editorInfo.ace_fastIncorp(1);
|
|
1571
1381
|
const repAfterFastIncorp = editorInfo.ace_getRep();
|
|
1572
1382
|
// log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_fastIncorp: Line=${repAfterFastIncorp.selStart[0]}, Col=${repAfterFastIncorp.selStart[1]}`);
|
|
1573
1383
|
// log(`${logPrefix} [selection] -> Requested sync hint (fastIncorp 1).`);
|
|
1574
1384
|
|
|
1575
|
-
// --- Re-assert selection ---
|
|
1576
1385
|
// log(`${logPrefix} [caretTrace] [selection] Attempting to re-assert selection post-fastIncorp to [${currentLineNum}, ${newAbsoluteCaretCol}]`);
|
|
1577
1386
|
editorInfo.ace_performSelectionChange([currentLineNum, newAbsoluteCaretCol], [currentLineNum, newAbsoluteCaretCol], false);
|
|
1578
1387
|
const repAfterReassert = editorInfo.ace_getRep();
|
|
@@ -1590,31 +1399,26 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1590
1399
|
} else {
|
|
1591
1400
|
// log(`${logPrefix} [selection] -> Editor instance not found, cannot update ep_data_tables_last_clicked.`);
|
|
1592
1401
|
}
|
|
1593
|
-
|
|
1402
|
+
|
|
1594
1403
|
// log(`${logPrefix} END [selection] (Handled highlight modification) Key='${evt.key}' Type='${evt.type}'. Duration: ${Date.now() - startLogTime}ms`);
|
|
1595
1404
|
return true;
|
|
1596
1405
|
} catch (error) {
|
|
1597
1406
|
// log(`${logPrefix} [selection] ERROR during highlight modification:`, error);
|
|
1598
1407
|
console.error('[ep_data_tables] Error processing highlight modification:', error);
|
|
1599
|
-
return true;
|
|
1408
|
+
return true;
|
|
1600
1409
|
}
|
|
1601
1410
|
}
|
|
1602
1411
|
}
|
|
1603
|
-
// --- END NEW: Handle Highlight Deletion/Replacement ---
|
|
1604
1412
|
|
|
1605
|
-
// --- Check for Ctrl+X (Cut) key combination ---
|
|
1606
1413
|
const isCutKey = (evt.ctrlKey || evt.metaKey) && (evt.key === 'x' || evt.key === 'X' || evt.keyCode === 88);
|
|
1607
1414
|
if (isCutKey && hasSelection) {
|
|
1608
1415
|
// log(`${logPrefix} Ctrl+X (Cut) detected with selection. Letting cut event handler manage this.`);
|
|
1609
|
-
|
|
1610
|
-
// as the cut event will handle the operation and prevent default
|
|
1611
|
-
return false; // Allow the cut event to be triggered
|
|
1416
|
+
return false;
|
|
1612
1417
|
} else if (isCutKey && !hasSelection) {
|
|
1613
1418
|
// log(`${logPrefix} Ctrl+X (Cut) detected but no selection. Allowing default.`);
|
|
1614
|
-
return false;
|
|
1419
|
+
return false;
|
|
1615
1420
|
}
|
|
1616
1421
|
|
|
1617
|
-
// --- Define Key Types ---
|
|
1618
1422
|
const isTypingKey = evt.key && evt.key.length === 1 && !evt.ctrlKey && !evt.metaKey && !evt.altKey;
|
|
1619
1423
|
const isDeleteKey = evt.key === 'Delete' || evt.keyCode === 46;
|
|
1620
1424
|
const isBackspaceKey = evt.key === 'Backspace' || evt.keyCode === 8;
|
|
@@ -1623,18 +1427,9 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1623
1427
|
const isEnterKey = evt.key === 'Enter';
|
|
1624
1428
|
// log(`${logPrefix} Key classification: Typing=${isTypingKey}, Backspace=${isBackspaceKey}, Delete=${isDeleteKey}, Nav=${isNavigationKey}, Tab=${isTabKey}, Enter=${isEnterKey}, Cut=${isCutKey}`);
|
|
1625
1429
|
|
|
1626
|
-
/*
|
|
1627
|
-
* Prevent caret placement *after* the invisible caret-anchor.
|
|
1628
|
-
* – RIGHT (→) pressed at the end of a cell jumps to the next cell.
|
|
1629
|
-
* – LEFT (←) pressed at the start of a cell jumps to the previous cell.
|
|
1630
|
-
* This avoids the narrow dead-zone that lives between the anchor and the
|
|
1631
|
-
* resize handle where typing previously caused content to drift into the
|
|
1632
|
-
* neighbouring column.
|
|
1633
|
-
*/
|
|
1634
1430
|
const currentCellTextLengthEarly = cellTexts[targetCellIndex]?.length ?? 0;
|
|
1635
1431
|
|
|
1636
1432
|
if (evt.type === 'keydown' && !evt.ctrlKey && !evt.metaKey && !evt.altKey) {
|
|
1637
|
-
// Right-arrow – if at the very end of a cell, move to the next cell.
|
|
1638
1433
|
if (evt.keyCode === 39 && relativeCaretPos >= currentCellTextLengthEarly && targetCellIndex < metadataForTargetLine.cols - 1) {
|
|
1639
1434
|
// log(`${logPrefix} ArrowRight at cell boundary – navigating to next cell to avoid anchor zone.`);
|
|
1640
1435
|
evt.preventDefault();
|
|
@@ -1642,7 +1437,6 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1642
1437
|
return true;
|
|
1643
1438
|
}
|
|
1644
1439
|
|
|
1645
|
-
// Left-arrow – if at the very start of a cell, move to the previous cell.
|
|
1646
1440
|
if (evt.keyCode === 37 && relativeCaretPos === 0 && targetCellIndex > 0) {
|
|
1647
1441
|
// log(`${logPrefix} ArrowLeft at cell boundary – navigating to previous cell to avoid anchor zone.`);
|
|
1648
1442
|
evt.preventDefault();
|
|
@@ -1651,21 +1445,17 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1651
1445
|
}
|
|
1652
1446
|
}
|
|
1653
1447
|
|
|
1654
|
-
// --- Handle Keys ---
|
|
1655
1448
|
|
|
1656
|
-
// 1. Allow non-Tab navigation keys immediately
|
|
1657
1449
|
if (isNavigationKey && !isTabKey) {
|
|
1658
1450
|
// log(`${logPrefix} Allowing navigation key: ${evt.key}. Clearing click state.`);
|
|
1659
|
-
if (editor) editor.ep_data_tables_last_clicked = null;
|
|
1451
|
+
if (editor) editor.ep_data_tables_last_clicked = null;
|
|
1660
1452
|
return false;
|
|
1661
1453
|
}
|
|
1662
1454
|
|
|
1663
|
-
// 2. Handle Tab - Navigate to next cell (only on keydown to avoid double navigation)
|
|
1664
1455
|
if (isTabKey) {
|
|
1665
1456
|
// log(`${logPrefix} Tab key pressed. Event type: ${evt.type}`);
|
|
1666
1457
|
evt.preventDefault();
|
|
1667
|
-
|
|
1668
|
-
// Only process keydown events for navigation to avoid double navigation
|
|
1458
|
+
|
|
1669
1459
|
if (evt.type !== 'keydown') {
|
|
1670
1460
|
// log(`${logPrefix} Ignoring Tab ${evt.type} event to prevent double navigation.`);
|
|
1671
1461
|
return true;
|
|
@@ -1679,12 +1469,10 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1679
1469
|
return true;
|
|
1680
1470
|
}
|
|
1681
1471
|
|
|
1682
|
-
// 3. Handle Enter - Navigate to cell below (only on keydown to avoid double navigation)
|
|
1683
1472
|
if (isEnterKey) {
|
|
1684
1473
|
// log(`${logPrefix} Enter key pressed. Event type: ${evt.type}`);
|
|
1685
1474
|
evt.preventDefault();
|
|
1686
|
-
|
|
1687
|
-
// Only process keydown events for navigation to avoid double navigation
|
|
1475
|
+
|
|
1688
1476
|
if (evt.type !== 'keydown') {
|
|
1689
1477
|
// log(`${logPrefix} Ignoring Enter ${evt.type} event to prevent double navigation.`);
|
|
1690
1478
|
return true;
|
|
@@ -1698,38 +1486,31 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1698
1486
|
return true;
|
|
1699
1487
|
}
|
|
1700
1488
|
|
|
1701
|
-
// 4. Intercept destructive keys ONLY at cell boundaries to protect delimiters
|
|
1702
1489
|
const currentCellTextLength = cellTexts[targetCellIndex]?.length ?? 0;
|
|
1703
|
-
// Backspace at the very beginning of cell > 0
|
|
1704
1490
|
if (isBackspaceKey && relativeCaretPos === 0 && targetCellIndex > 0) {
|
|
1705
1491
|
// log(`${logPrefix} Intercepted Backspace at start of cell ${targetCellIndex}. Preventing default.`);
|
|
1706
1492
|
evt.preventDefault();
|
|
1707
1493
|
return true;
|
|
1708
1494
|
}
|
|
1709
|
-
// NEW: Backspace at very beginning of first cell – would merge with previous line
|
|
1710
1495
|
if (isBackspaceKey && relativeCaretPos === 0 && targetCellIndex === 0) {
|
|
1711
1496
|
// log(`${logPrefix} Intercepted Backspace at start of first cell (line boundary). Preventing merge.`);
|
|
1712
1497
|
evt.preventDefault();
|
|
1713
1498
|
return true;
|
|
1714
1499
|
}
|
|
1715
|
-
// Delete at the very end of cell < last cell
|
|
1716
1500
|
if (isDeleteKey && relativeCaretPos === currentCellTextLength && targetCellIndex < metadataForTargetLine.cols - 1) {
|
|
1717
1501
|
// log(`${logPrefix} Intercepted Delete at end of cell ${targetCellIndex}. Preventing default.`);
|
|
1718
1502
|
evt.preventDefault();
|
|
1719
1503
|
return true;
|
|
1720
1504
|
}
|
|
1721
|
-
// NEW: Delete at very end of last cell – would merge with next line
|
|
1722
1505
|
if (isDeleteKey && relativeCaretPos === currentCellTextLength && targetCellIndex === metadataForTargetLine.cols - 1) {
|
|
1723
1506
|
// log(`${logPrefix} Intercepted Delete at end of last cell (line boundary). Preventing merge.`);
|
|
1724
1507
|
evt.preventDefault();
|
|
1725
1508
|
return true;
|
|
1726
1509
|
}
|
|
1727
1510
|
|
|
1728
|
-
// 5. Handle Typing/Backspace/Delete WITHIN a cell via manual modification
|
|
1729
1511
|
const isInternalBackspace = isBackspaceKey && relativeCaretPos > 0;
|
|
1730
1512
|
const isInternalDelete = isDeleteKey && relativeCaretPos < currentCellTextLength;
|
|
1731
1513
|
|
|
1732
|
-
// Guard: internal Backspace at relativePos 1 (would delete delimiter) & Delete at relativePos 0
|
|
1733
1514
|
if ((isInternalBackspace && relativeCaretPos === 1 && targetCellIndex > 0) ||
|
|
1734
1515
|
(isInternalDelete && relativeCaretPos === 0 && targetCellIndex > 0)) {
|
|
1735
1516
|
// log(`${logPrefix} Attempt to erase protected delimiter – operation blocked.`);
|
|
@@ -1738,7 +1519,6 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1738
1519
|
}
|
|
1739
1520
|
|
|
1740
1521
|
if (isTypingKey || isInternalBackspace || isInternalDelete) {
|
|
1741
|
-
// --- PREVENT TYPING DIRECTLY AFTER DELIMITER (relativeCaretPos===0) ---
|
|
1742
1522
|
if (isTypingKey && relativeCaretPos === 0 && targetCellIndex > 0) {
|
|
1743
1523
|
// log(`${logPrefix} Caret at forbidden position 0 (just after delimiter). Auto-advancing to position 1.`);
|
|
1744
1524
|
const safePosAbs = cellStartCol + 1;
|
|
@@ -1747,12 +1527,10 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1747
1527
|
relativeCaretPos = 1;
|
|
1748
1528
|
// log(`${logPrefix} Caret moved to safe position. New relativeCaretPos=${relativeCaretPos}`);
|
|
1749
1529
|
}
|
|
1750
|
-
// *** Use the validated currentLineNum and currentCol derived from relativeCaretPos ***
|
|
1751
1530
|
const currentCol = cellStartCol + relativeCaretPos;
|
|
1752
1531
|
// log(`${logPrefix} Handling INTERNAL key='${evt.key}' Type='${evt.type}' at Line=${currentLineNum}, Col=${currentCol} (CellIndex=${targetCellIndex}, RelativePos=${relativeCaretPos}).`);
|
|
1753
1532
|
// log(`${logPrefix} [caretTrace] Initial rep.selStart for internal edit: Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
|
|
1754
1533
|
|
|
1755
|
-
// Only process keydown events for modifications
|
|
1756
1534
|
if (evt.type !== 'keydown') {
|
|
1757
1535
|
// log(`${logPrefix} Ignoring non-keydown event type ('${evt.type}') for handled key.`);
|
|
1758
1536
|
return false;
|
|
@@ -1762,10 +1540,10 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1762
1540
|
evt.preventDefault();
|
|
1763
1541
|
|
|
1764
1542
|
let newAbsoluteCaretCol = -1;
|
|
1765
|
-
let repBeforeEdit = null;
|
|
1543
|
+
let repBeforeEdit = null;
|
|
1766
1544
|
|
|
1767
1545
|
try {
|
|
1768
|
-
repBeforeEdit = editorInfo.ace_getRep();
|
|
1546
|
+
repBeforeEdit = editorInfo.ace_getRep();
|
|
1769
1547
|
// log(`${logPrefix} [caretTrace] rep.selStart before ace_performDocumentReplaceRange: Line=${repBeforeEdit.selStart[0]}, Col=${repBeforeEdit.selStart[1]}`);
|
|
1770
1548
|
|
|
1771
1549
|
if (isTypingKey) {
|
|
@@ -1786,27 +1564,24 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1786
1564
|
const delRangeEnd = [currentLineNum, currentCol + 1];
|
|
1787
1565
|
// log(`${logPrefix} -> Deleting (Delete) range [${delRangeStart}]-[${delRangeEnd}]`);
|
|
1788
1566
|
editorInfo.ace_performDocumentReplaceRange(delRangeStart, delRangeEnd, '');
|
|
1789
|
-
newAbsoluteCaretCol = currentCol;
|
|
1567
|
+
newAbsoluteCaretCol = currentCol;
|
|
1790
1568
|
}
|
|
1791
1569
|
const repAfterReplace = editorInfo.ace_getRep();
|
|
1792
1570
|
// log(`${logPrefix} [caretTrace] rep.selStart after ace_performDocumentReplaceRange: Line=${repAfterReplace.selStart[0]}, Col=${repAfterReplace.selStart[1]}`);
|
|
1793
1571
|
|
|
1794
1572
|
|
|
1795
|
-
// *** CRITICAL: Re-apply the line attribute after ANY modification ***
|
|
1796
1573
|
// log(`${logPrefix} -> Re-applying tbljson line attribute...`);
|
|
1797
|
-
|
|
1798
|
-
// DEBUG: Log the values before calculating attrStringToApply
|
|
1574
|
+
|
|
1799
1575
|
// log(`${logPrefix} DEBUG: Before calculating attrStringToApply - trustedLastClick=${trustedLastClick}, reportedLineNum=${reportedLineNum}, currentLineNum=${currentLineNum}`);
|
|
1800
1576
|
// log(`${logPrefix} DEBUG: lineAttrString value:`, lineAttrString ? `"${lineAttrString}"` : 'null/undefined');
|
|
1801
|
-
|
|
1577
|
+
|
|
1802
1578
|
const applyHelper = editorInfo.ep_data_tables_applyMeta;
|
|
1803
1579
|
if (applyHelper && typeof applyHelper === 'function' && repBeforeEdit) {
|
|
1804
|
-
// Pass the original lineAttrString if available AND if it belongs to the currentLineNum
|
|
1805
1580
|
const attrStringToApply = (trustedLastClick || reportedLineNum === currentLineNum) ? lineAttrString : null;
|
|
1806
|
-
|
|
1581
|
+
|
|
1807
1582
|
// log(`${logPrefix} DEBUG: Calculated attrStringToApply:`, attrStringToApply ? `"${attrStringToApply}"` : 'null/undefined');
|
|
1808
1583
|
// log(`${logPrefix} DEBUG: Condition result: (${trustedLastClick} || ${reportedLineNum} === ${currentLineNum}) = ${trustedLastClick || reportedLineNum === currentLineNum}`);
|
|
1809
|
-
|
|
1584
|
+
|
|
1810
1585
|
applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, repBeforeEdit, editorInfo, attrStringToApply, docManager);
|
|
1811
1586
|
// log(`${logPrefix} -> tbljson line attribute re-applied (using rep before edit).`);
|
|
1812
1587
|
} else {
|
|
@@ -1814,16 +1589,15 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1814
1589
|
const currentRepFallback = editorInfo.ace_getRep();
|
|
1815
1590
|
if (applyHelper && typeof applyHelper === 'function' && currentRepFallback) {
|
|
1816
1591
|
// log(`${logPrefix} -> Retrying attribute application with current rep...`);
|
|
1817
|
-
applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, currentRepFallback, editorInfo, null, docManager);
|
|
1592
|
+
applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, currentRepFallback, editorInfo, null, docManager);
|
|
1818
1593
|
// log(`${logPrefix} -> tbljson line attribute re-applied (using current rep fallback).`);
|
|
1819
1594
|
} else {
|
|
1820
1595
|
console.error(`${logPrefix} -> FAILED to re-apply tbljson attribute even with fallback rep.`);
|
|
1821
1596
|
}
|
|
1822
1597
|
}
|
|
1823
|
-
|
|
1824
|
-
// Set caret position immediately
|
|
1598
|
+
|
|
1825
1599
|
if (newAbsoluteCaretCol >= 0) {
|
|
1826
|
-
const newCaretPos = [currentLineNum, newAbsoluteCaretCol];
|
|
1600
|
+
const newCaretPos = [currentLineNum, newAbsoluteCaretCol];
|
|
1827
1601
|
// log(`${logPrefix} -> Setting selection immediately to:`, newCaretPos);
|
|
1828
1602
|
// log(`${logPrefix} [caretTrace] rep.selStart before ace_performSelectionChange: Line=${editorInfo.ace_getRep().selStart[0]}, Col=${editorInfo.ace_getRep().selStart[1]}`);
|
|
1829
1603
|
try {
|
|
@@ -1832,20 +1606,17 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1832
1606
|
// log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performSelectionChange: Line=${repAfterSelectionChange.selStart[0]}, Col=${repAfterSelectionChange.selStart[1]}`);
|
|
1833
1607
|
// log(`${logPrefix} -> Selection set immediately.`);
|
|
1834
1608
|
|
|
1835
|
-
// Add sync hint AFTER setting selection
|
|
1836
1609
|
editorInfo.ace_fastIncorp(1);
|
|
1837
1610
|
const repAfterFastIncorp = editorInfo.ace_getRep();
|
|
1838
1611
|
// log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_fastIncorp: Line=${repAfterFastIncorp.selStart[0]}, Col=${repAfterFastIncorp.selStart[1]}`);
|
|
1839
1612
|
// log(`${logPrefix} -> Requested sync hint (fastIncorp 1).`);
|
|
1840
1613
|
|
|
1841
|
-
// --- Re-assert selection ---
|
|
1842
1614
|
const targetCaretPosForReassert = [currentLineNum, newAbsoluteCaretCol];
|
|
1843
1615
|
// log(`${logPrefix} [caretTrace] Attempting to re-assert selection post-fastIncorp to [${targetCaretPosForReassert[0]}, ${targetCaretPosForReassert[1]}]`);
|
|
1844
1616
|
editorInfo.ace_performSelectionChange(targetCaretPosForReassert, targetCaretPosForReassert, false);
|
|
1845
1617
|
const repAfterReassert = editorInfo.ace_getRep();
|
|
1846
1618
|
// log(`${logPrefix} [caretTrace] [selection] rep.selStart after re-asserting selection: Line=${repAfterReassert.selStart[0]}, Col=${repAfterReassert.selStart[1]}`);
|
|
1847
1619
|
|
|
1848
|
-
// Store the updated caret info for the next event
|
|
1849
1620
|
const newRelativePos = newAbsoluteCaretCol - cellStartCol;
|
|
1850
1621
|
editor.ep_data_tables_last_clicked = {
|
|
1851
1622
|
lineNum: currentLineNum,
|
|
@@ -1867,44 +1638,43 @@ exports.aceKeyEvent = (h, ctx) => {
|
|
|
1867
1638
|
} catch (error) {
|
|
1868
1639
|
// log(`${logPrefix} ERROR during manual key handling:`, error);
|
|
1869
1640
|
console.error('[ep_data_tables] Error processing key event update:', error);
|
|
1870
|
-
// Maybe return false to allow default as a fallback on error?
|
|
1871
|
-
// For now, return true as we prevented default.
|
|
1872
1641
|
return true;
|
|
1873
1642
|
}
|
|
1874
1643
|
|
|
1875
1644
|
const endLogTime = Date.now();
|
|
1876
1645
|
// log(`${logPrefix} END (Handled Internal Edit Manually) Key='${evt.key}' Type='${evt.type}' -> Returned true. Duration: ${endLogTime - startLogTime}ms`);
|
|
1877
|
-
return true;
|
|
1646
|
+
return true;
|
|
1878
1647
|
|
|
1879
|
-
}
|
|
1648
|
+
}
|
|
1880
1649
|
|
|
1881
1650
|
|
|
1882
|
-
// Fallback for any other keys or edge cases not handled above
|
|
1883
1651
|
const endLogTimeFinal = Date.now();
|
|
1884
1652
|
// log(`${logPrefix} END (Fell Through / Unhandled Case) Key='${evt.key}' Type='${evt.type}'. Allowing default. Duration: ${endLogTimeFinal - startLogTime}ms`);
|
|
1885
|
-
// Clear click state if it wasn't handled?
|
|
1886
|
-
// if (editor?.ep_data_tables_last_clicked) editor.ep_data_tables_last_clicked = null;
|
|
1887
1653
|
// log(`${logPrefix} [caretTrace] Final rep.selStart at end of aceKeyEvent (if unhandled): Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
|
|
1888
|
-
return false;
|
|
1654
|
+
return false;
|
|
1889
1655
|
};
|
|
1890
|
-
|
|
1891
|
-
// ───────────────────── ace init + public helpers ─────────────────────
|
|
1892
1656
|
exports.aceInitialized = (h, ctx) => {
|
|
1893
1657
|
const logPrefix = '[ep_data_tables:aceInitialized]';
|
|
1894
1658
|
// log(`${logPrefix} START`, { hook_name: h, context: ctx });
|
|
1895
1659
|
const ed = ctx.editorInfo;
|
|
1896
1660
|
const docManager = ctx.documentAttributeManager;
|
|
1897
1661
|
|
|
1662
|
+
try {
|
|
1663
|
+
if (typeof window !== 'undefined') {
|
|
1664
|
+
window.__epDataTablesReady = true;
|
|
1665
|
+
const guard = document.getElementById('ep-data-tables-guard');
|
|
1666
|
+
if (guard && guard.parentNode) guard.parentNode.removeChild(guard);
|
|
1667
|
+
}
|
|
1668
|
+
} catch (_) {}
|
|
1669
|
+
|
|
1898
1670
|
// log(`${logPrefix} Attaching ep_data_tables_applyMeta helper to editorInfo.`);
|
|
1899
1671
|
ed.ep_data_tables_applyMeta = applyTableLineMetadataAttribute;
|
|
1900
1672
|
// log(`${logPrefix}: Attached applyTableLineMetadataAttribute helper to ed.ep_data_tables_applyMeta successfully.`);
|
|
1901
1673
|
|
|
1902
|
-
// Store the documentAttributeManager reference for later use
|
|
1903
1674
|
// log(`${logPrefix} Storing documentAttributeManager reference on editorInfo.`);
|
|
1904
1675
|
ed.ep_data_tables_docManager = docManager;
|
|
1905
1676
|
// log(`${logPrefix}: Stored documentAttributeManager reference as ed.ep_data_tables_docManager.`);
|
|
1906
1677
|
|
|
1907
|
-
// *** ENHANCED: Paste event listener + Column resize listeners ***
|
|
1908
1678
|
// log(`${logPrefix} Preparing to attach paste and resize listeners via ace_callWithAce.`);
|
|
1909
1679
|
ed.ace_callWithAce((ace) => {
|
|
1910
1680
|
const callWithAceLogPrefix = '[ep_data_tables:aceInitialized:callWithAceForListeners]';
|
|
@@ -1918,12 +1688,10 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
1918
1688
|
const editor = ace.editor;
|
|
1919
1689
|
// log(`${callWithAceLogPrefix} ace.editor obtained successfully.`);
|
|
1920
1690
|
|
|
1921
|
-
// Store editor reference for later use in table operations
|
|
1922
1691
|
// log(`${logPrefix} Storing editor reference on editorInfo.`);
|
|
1923
1692
|
ed.ep_data_tables_editor = editor;
|
|
1924
1693
|
// log(`${logPrefix}: Stored editor reference as ed.ep_data_tables_editor.`);
|
|
1925
1694
|
|
|
1926
|
-
// Attempt to find the inner iframe body, similar to ep_image_insert
|
|
1927
1695
|
let $inner;
|
|
1928
1696
|
try {
|
|
1929
1697
|
// log(`${callWithAceLogPrefix} Attempting to find inner iframe body for listener attachment.`);
|
|
@@ -1949,8 +1717,294 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
1949
1717
|
// log(`${callWithAceLogPrefix} Failed to find body in ace_inner.`);
|
|
1950
1718
|
return;
|
|
1951
1719
|
}
|
|
1952
|
-
$inner = $(innerDocBody[0]);
|
|
1720
|
+
$inner = $(innerDocBody[0]);
|
|
1953
1721
|
// log(`${callWithAceLogPrefix} Successfully found inner iframe body:`, $inner);
|
|
1722
|
+
|
|
1723
|
+
const mobileSuggestionBlocker = (evt) => {
|
|
1724
|
+
const t = evt && evt.inputType || '';
|
|
1725
|
+
const dataStr = (evt && typeof evt.data === 'string') ? evt.data : '';
|
|
1726
|
+
const isProblem = (
|
|
1727
|
+
t === 'insertReplacementText' ||
|
|
1728
|
+
t === 'insertFromComposition' ||
|
|
1729
|
+
(t === 'insertText' && !evt.isComposing && (!dataStr || dataStr.length > 1))
|
|
1730
|
+
);
|
|
1731
|
+
if (!isProblem) return;
|
|
1732
|
+
|
|
1733
|
+
try {
|
|
1734
|
+
const repQuick = ed.ace_getRep && ed.ace_getRep();
|
|
1735
|
+
if (!repQuick || !repQuick.selStart) return;
|
|
1736
|
+
const lineNumQuick = repQuick.selStart[0];
|
|
1737
|
+
let metaStrQuick = docManager && docManager.getAttributeOnLine
|
|
1738
|
+
? docManager.getAttributeOnLine(lineNumQuick, ATTR_TABLE_JSON)
|
|
1739
|
+
: null;
|
|
1740
|
+
let metaQuick = null;
|
|
1741
|
+
if (metaStrQuick) { try { metaQuick = JSON.parse(metaStrQuick); } catch (_) {} }
|
|
1742
|
+
if (!metaQuick) metaQuick = getTableLineMetadata(lineNumQuick, ed, docManager);
|
|
1743
|
+
if (!metaQuick || typeof metaQuick.cols !== 'number') return;
|
|
1744
|
+
} catch (_) { return; }
|
|
1745
|
+
|
|
1746
|
+
evt._epDataTablesHandled = true;
|
|
1747
|
+
if (evt.originalEvent) evt.originalEvent._epDataTablesHandled = true;
|
|
1748
|
+
evt.preventDefault();
|
|
1749
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
1750
|
+
|
|
1751
|
+
const capturedInputType = t;
|
|
1752
|
+
const capturedData = dataStr;
|
|
1753
|
+
|
|
1754
|
+
setTimeout(() => {
|
|
1755
|
+
try {
|
|
1756
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
1757
|
+
aceInstance.ace_fastIncorp(10);
|
|
1758
|
+
const rep = aceInstance.ace_getRep();
|
|
1759
|
+
if (!rep || !rep.selStart || !rep.selEnd) return;
|
|
1760
|
+
|
|
1761
|
+
const selStart = [...rep.selStart];
|
|
1762
|
+
const selEnd = [...rep.selEnd];
|
|
1763
|
+
const lineNum = selStart[0];
|
|
1764
|
+
|
|
1765
|
+
let metaStr = docManager && docManager.getAttributeOnLine
|
|
1766
|
+
? docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON)
|
|
1767
|
+
: null;
|
|
1768
|
+
let tableMetadata = null;
|
|
1769
|
+
if (metaStr) { try { tableMetadata = JSON.parse(metaStr); } catch (_) {} }
|
|
1770
|
+
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
1771
|
+
if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined') {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const initialHasSelection = !(selStart[0] === selEnd[0] && selStart[1] === selEnd[1]);
|
|
1776
|
+
|
|
1777
|
+
let replacement = typeof capturedData === 'string' ? capturedData : '';
|
|
1778
|
+
if (!replacement) {
|
|
1779
|
+
if (capturedInputType === 'insertText' && !initialHasSelection) {
|
|
1780
|
+
replacement = ' ';
|
|
1781
|
+
} else if (capturedInputType === 'insertReplacementText' || capturedInputType === 'insertFromComposition' || initialHasSelection) {
|
|
1782
|
+
replacement = ' ';
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
replacement = normalizeSoftWhitespace(
|
|
1787
|
+
(replacement || '')
|
|
1788
|
+
.replace(new RegExp(DELIMITER, 'g'), ' ')
|
|
1789
|
+
.replace(/[\u200B\u200C\u200D\uFEFF]/g, '')
|
|
1790
|
+
);
|
|
1791
|
+
|
|
1792
|
+
if (!replacement) replacement = ' ';
|
|
1793
|
+
|
|
1794
|
+
const lineEntry = rep.lines.atIndex(lineNum);
|
|
1795
|
+
const lineText = lineEntry?.text || '';
|
|
1796
|
+
const cells = lineText.split(DELIMITER);
|
|
1797
|
+
let currentOffset = 0;
|
|
1798
|
+
let targetCellIndex = -1;
|
|
1799
|
+
let cellStartCol = 0;
|
|
1800
|
+
let cellEndCol = 0;
|
|
1801
|
+
for (let i = 0; i < cells.length; i++) {
|
|
1802
|
+
const cellLen = cells[i]?.length ?? 0;
|
|
1803
|
+
const cellEndThis = currentOffset + cellLen;
|
|
1804
|
+
if (selStart[1] >= currentOffset && selStart[1] <= cellEndThis) {
|
|
1805
|
+
targetCellIndex = i;
|
|
1806
|
+
cellStartCol = currentOffset;
|
|
1807
|
+
cellEndCol = cellEndThis;
|
|
1808
|
+
break;
|
|
1809
|
+
}
|
|
1810
|
+
currentOffset += cellLen + DELIMITER.length;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
if (targetCellIndex === -1) {
|
|
1814
|
+
aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, replacement);
|
|
1815
|
+
const repAfterFallback = aceInstance.ace_getRep();
|
|
1816
|
+
ed.ep_data_tables_applyMeta(
|
|
1817
|
+
lineNum,
|
|
1818
|
+
tableMetadata.tblId,
|
|
1819
|
+
tableMetadata.row,
|
|
1820
|
+
tableMetadata.cols,
|
|
1821
|
+
repAfterFallback,
|
|
1822
|
+
ed,
|
|
1823
|
+
null,
|
|
1824
|
+
docManager
|
|
1825
|
+
);
|
|
1826
|
+
const fallbackLineEntry = repAfterFallback.lines.atIndex(lineNum);
|
|
1827
|
+
const fallbackMaxLen = fallbackLineEntry ? fallbackLineEntry.text.length : 0;
|
|
1828
|
+
const fallbackStartCol = Math.min(Math.max(selStart[1], 0), fallbackMaxLen);
|
|
1829
|
+
const fallbackEndCol = Math.min(fallbackStartCol + replacement.length, fallbackMaxLen);
|
|
1830
|
+
const fallbackCaretPos = [lineNum, fallbackEndCol];
|
|
1831
|
+
aceInstance.ace_performSelectionChange(fallbackCaretPos, fallbackCaretPos, false);
|
|
1832
|
+
aceInstance.ace_fastIncorp(10);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
if (selEnd[0] !== selStart[0]) {
|
|
1837
|
+
selEnd[0] = selStart[0];
|
|
1838
|
+
selEnd[1] = cellEndCol;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
if (selEnd[1] > cellEndCol) {
|
|
1842
|
+
selEnd[1] = Math.min(selEnd[1], cellEndCol);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
if (selEnd[1] < selStart[1]) selEnd[1] = selStart[1];
|
|
1846
|
+
|
|
1847
|
+
aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, replacement);
|
|
1848
|
+
|
|
1849
|
+
const repAfter = aceInstance.ace_getRep();
|
|
1850
|
+
const lineEntryAfter = repAfter.lines.atIndex(lineNum);
|
|
1851
|
+
const maxLen = lineEntryAfter ? lineEntryAfter.text.length : 0;
|
|
1852
|
+
const startCol = Math.min(Math.max(selStart[1], 0), maxLen);
|
|
1853
|
+
const endCol = Math.min(startCol + replacement.length, maxLen);
|
|
1854
|
+
|
|
1855
|
+
if (endCol > startCol) {
|
|
1856
|
+
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
1857
|
+
[lineNum, startCol],
|
|
1858
|
+
[lineNum, endCol],
|
|
1859
|
+
[[ATTR_CELL, String(targetCellIndex)]]
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
ed.ep_data_tables_applyMeta(
|
|
1864
|
+
lineNum,
|
|
1865
|
+
tableMetadata.tblId,
|
|
1866
|
+
tableMetadata.row,
|
|
1867
|
+
tableMetadata.cols,
|
|
1868
|
+
repAfter,
|
|
1869
|
+
ed,
|
|
1870
|
+
null,
|
|
1871
|
+
docManager
|
|
1872
|
+
);
|
|
1873
|
+
|
|
1874
|
+
const newCaretPos = [lineNum, endCol];
|
|
1875
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
1876
|
+
aceInstance.ace_fastIncorp(10);
|
|
1877
|
+
|
|
1878
|
+
const editor = ed.ep_data_tables_editor;
|
|
1879
|
+
if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
1880
|
+
const freshLineText = lineEntryAfter ? lineEntryAfter.text : '';
|
|
1881
|
+
const freshCells = freshLineText.split(DELIMITER);
|
|
1882
|
+
let freshOffset = 0;
|
|
1883
|
+
for (let i = 0; i < targetCellIndex; i++) {
|
|
1884
|
+
freshOffset += (freshCells[i]?.length ?? 0) + DELIMITER.length;
|
|
1885
|
+
}
|
|
1886
|
+
const newRelativePos = newCaretPos[1] - freshOffset;
|
|
1887
|
+
editor.ep_data_tables_last_clicked = {
|
|
1888
|
+
lineNum,
|
|
1889
|
+
tblId: tableMetadata.tblId,
|
|
1890
|
+
cellIndex: targetCellIndex,
|
|
1891
|
+
relativePos: newRelativePos < 0 ? 0 : newRelativePos,
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
}, 'mobileSuggestionBlocker', true);
|
|
1895
|
+
} catch (e) {
|
|
1896
|
+
console.error('[ep_data_tables:mobileSuggestionBlocker] Error applying predictive text:', e);
|
|
1897
|
+
}
|
|
1898
|
+
}, 0);
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1901
|
+
// IME/autocorrect diagnostics: capture-phase logging and newline soft-normalization for table lines
|
|
1902
|
+
const logIMEEvent = (rawEvt, tag) => {
|
|
1903
|
+
try {
|
|
1904
|
+
const e = rawEvt && (rawEvt.originalEvent || rawEvt);
|
|
1905
|
+
const rep = ed.ace_getRep && ed.ace_getRep();
|
|
1906
|
+
const selStart = rep && rep.selStart;
|
|
1907
|
+
const lineNum = selStart ? selStart[0] : -1;
|
|
1908
|
+
let isTableLine = false;
|
|
1909
|
+
if (lineNum >= 0) {
|
|
1910
|
+
let s = docManager && docManager.getAttributeOnLine ? docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON) : null;
|
|
1911
|
+
if (!s) {
|
|
1912
|
+
const meta = getTableLineMetadata(lineNum, ed, docManager);
|
|
1913
|
+
isTableLine = !!meta && typeof meta.cols === 'number';
|
|
1914
|
+
} else {
|
|
1915
|
+
isTableLine = true;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (!isTableLine) return;
|
|
1919
|
+
const payload = {
|
|
1920
|
+
tag,
|
|
1921
|
+
type: e && e.type,
|
|
1922
|
+
inputType: e && e.inputType,
|
|
1923
|
+
data: typeof (e && e.data) === 'string' ? e.data : null,
|
|
1924
|
+
isComposing: !!(e && e.isComposing),
|
|
1925
|
+
key: e && e.key,
|
|
1926
|
+
code: e && e.code,
|
|
1927
|
+
which: e && e.which,
|
|
1928
|
+
keyCode: e && e.keyCode,
|
|
1929
|
+
};
|
|
1930
|
+
console.debug('[ep_data_tables:ime-diag]', payload);
|
|
1931
|
+
} catch (_) {}
|
|
1932
|
+
};
|
|
1933
|
+
|
|
1934
|
+
const softBreakNormalizer = (rawEvt) => {
|
|
1935
|
+
try {
|
|
1936
|
+
const e = rawEvt && (rawEvt.originalEvent || rawEvt);
|
|
1937
|
+
if (!e || e._epDataTablesNormalized) return;
|
|
1938
|
+
const t = e.inputType || '';
|
|
1939
|
+
const dataStr = typeof e.data === 'string' ? e.data : '';
|
|
1940
|
+
const hasSoftWs = /[\r\n\u00A0]/.test(dataStr); // include NBSP (U+00A0)
|
|
1941
|
+
const isSoftBreak = t === 'insertParagraph' || t === 'insertLineBreak' || hasSoftWs;
|
|
1942
|
+
if (!isSoftBreak) return;
|
|
1943
|
+
|
|
1944
|
+
const rep = ed.ace_getRep && ed.ace_getRep();
|
|
1945
|
+
if (!rep || !rep.selStart) return;
|
|
1946
|
+
const lineNum = rep.selStart[0];
|
|
1947
|
+
let metaStr = docManager && docManager.getAttributeOnLine ? docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON) : null;
|
|
1948
|
+
let meta = null;
|
|
1949
|
+
if (metaStr) { try { meta = JSON.parse(metaStr); } catch (_) {} }
|
|
1950
|
+
if (!meta) meta = getTableLineMetadata(lineNum, ed, docManager);
|
|
1951
|
+
if (!meta || typeof meta.cols !== 'number') return;
|
|
1952
|
+
|
|
1953
|
+
e.preventDefault();
|
|
1954
|
+
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
|
|
1955
|
+
e._epDataTablesNormalized = true;
|
|
1956
|
+
setTimeout(() => {
|
|
1957
|
+
try {
|
|
1958
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
1959
|
+
aceInstance.ace_fastIncorp(10);
|
|
1960
|
+
const rep2 = aceInstance.ace_getRep();
|
|
1961
|
+
const start = rep2.selStart;
|
|
1962
|
+
const end = rep2.selEnd;
|
|
1963
|
+
// Replace the attempted soft-break with a single space
|
|
1964
|
+
aceInstance.ace_performDocumentReplaceRange(start, end, ' ');
|
|
1965
|
+
}, 'softBreakNormalizer', true);
|
|
1966
|
+
} catch (err) { console.error('[ep_data_tables:softBreakNormalizer] error', err); }
|
|
1967
|
+
}, 0);
|
|
1968
|
+
} catch (_) {}
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
if ($inner && $inner.length > 0 && $inner[0].addEventListener) {
|
|
1972
|
+
const el = $inner[0];
|
|
1973
|
+
['beforeinput','input','textInput','compositionstart','compositionupdate','compositionend','keydown','keyup'].forEach((t) => {
|
|
1974
|
+
el.addEventListener(t, (ev) => logIMEEvent(ev, 'capture'), true);
|
|
1975
|
+
});
|
|
1976
|
+
el.addEventListener('beforeinput', softBreakNormalizer, true);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
if ($inner && $inner.length > 0 && $inner[0].addEventListener) {
|
|
1980
|
+
$inner[0].addEventListener('beforeinput', mobileSuggestionBlocker, true);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
if (isAndroidUA && isAndroidUA() && $inner && $inner.length > 0 && $inner[0].addEventListener) {
|
|
1984
|
+
$inner[0].addEventListener('textInput', (evt) => {
|
|
1985
|
+
const s = typeof evt.data === 'string' ? evt.data : '';
|
|
1986
|
+
if (s && s.length > 1) {
|
|
1987
|
+
mobileSuggestionBlocker({
|
|
1988
|
+
inputType: 'insertText',
|
|
1989
|
+
isComposing: false,
|
|
1990
|
+
data: s,
|
|
1991
|
+
preventDefault: () => { try { evt.preventDefault(); } catch (_) {} },
|
|
1992
|
+
stopImmediatePropagation: () => { try { evt.stopImmediatePropagation(); } catch (_) {} },
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
}, true);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
try {
|
|
1999
|
+
const disableAuto = (el) => {
|
|
2000
|
+
if (!el) return;
|
|
2001
|
+
el.setAttribute('autocorrect', 'off');
|
|
2002
|
+
el.setAttribute('autocomplete', 'off');
|
|
2003
|
+
el.setAttribute('autocapitalize', 'off');
|
|
2004
|
+
el.setAttribute('spellcheck', 'false');
|
|
2005
|
+
};
|
|
2006
|
+
disableAuto(innerDocBody[0] || innerDocBody);
|
|
2007
|
+
} catch (_) {}
|
|
1954
2008
|
} catch (e) {
|
|
1955
2009
|
console.error(`${callWithAceLogPrefix} ERROR: Exception while trying to find inner iframe body:`, e);
|
|
1956
2010
|
// log(`${callWithAceLogPrefix} Exception details:`, { message: e.message, stack: e.stack });
|
|
@@ -1963,7 +2017,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
1963
2017
|
return;
|
|
1964
2018
|
}
|
|
1965
2019
|
|
|
1966
|
-
// *** CUT EVENT LISTENER ***
|
|
1967
2020
|
// log(`${callWithAceLogPrefix} Attaching cut event listener to $inner (inner iframe body).`);
|
|
1968
2021
|
$inner.on('cut', (evt) => {
|
|
1969
2022
|
const cutLogPrefix = '[ep_data_tables:cutHandler]';
|
|
@@ -1974,20 +2027,18 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
1974
2027
|
if (!rep || !rep.selStart) {
|
|
1975
2028
|
console.warn(`${cutLogPrefix} WARNING: Could not get representation or selection. Allowing default cut.`);
|
|
1976
2029
|
console.warn(`${cutLogPrefix} Could not get rep or selStart.`);
|
|
1977
|
-
return;
|
|
2030
|
+
return;
|
|
1978
2031
|
}
|
|
1979
2032
|
console.log(`${cutLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
|
|
1980
2033
|
const selStart = rep.selStart;
|
|
1981
2034
|
const selEnd = rep.selEnd;
|
|
1982
2035
|
const lineNum = selStart[0];
|
|
1983
2036
|
console.log(`${cutLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
|
|
1984
|
-
// Determine if there is a selection in the editor representation
|
|
1985
2037
|
const hasSelectionInRep = !(selStart[0] === selEnd[0] && selStart[1] === selEnd[1]);
|
|
1986
2038
|
if (!hasSelectionInRep) {
|
|
1987
2039
|
console.log(`${cutLogPrefix} No selection detected in rep; deferring decision until table-line check.`);
|
|
1988
2040
|
}
|
|
1989
2041
|
|
|
1990
|
-
// Check if selection spans multiple lines
|
|
1991
2042
|
if (selStart[0] !== selEnd[0]) {
|
|
1992
2043
|
console.warn(`${cutLogPrefix} WARNING: Selection spans multiple lines. Preventing cut to protect table structure.`);
|
|
1993
2044
|
evt.preventDefault();
|
|
@@ -1999,32 +2050,28 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
1999
2050
|
let tableMetadata = null;
|
|
2000
2051
|
|
|
2001
2052
|
if (lineAttrString) {
|
|
2002
|
-
// Fast-path: attribute exists – parse it.
|
|
2003
2053
|
try {
|
|
2004
2054
|
tableMetadata = JSON.parse(lineAttrString);
|
|
2005
2055
|
} catch {}
|
|
2006
2056
|
}
|
|
2007
2057
|
|
|
2008
2058
|
if (!tableMetadata) {
|
|
2009
|
-
// Fallback for block-styled rows – reconstruct via DOM helper.
|
|
2010
2059
|
tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2011
2060
|
}
|
|
2012
2061
|
|
|
2013
2062
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
|
|
2014
2063
|
console.log(`${cutLogPrefix} Line ${lineNum} is NOT a recognised table line. Allowing default cut.`);
|
|
2015
|
-
return;
|
|
2064
|
+
return;
|
|
2016
2065
|
}
|
|
2017
2066
|
|
|
2018
2067
|
console.log(`${cutLogPrefix} Line ${lineNum} IS a table line. Metadata:`, tableMetadata);
|
|
2019
2068
|
|
|
2020
|
-
// If inside a table line but the rep shows no selection, prevent default to protect structure
|
|
2021
2069
|
if (!hasSelectionInRep) {
|
|
2022
2070
|
console.log(`${cutLogPrefix} Preventing default CUT on table line with collapsed selection to protect delimiters.`);
|
|
2023
2071
|
evt.preventDefault();
|
|
2024
2072
|
return;
|
|
2025
2073
|
}
|
|
2026
2074
|
|
|
2027
|
-
// Validate selection is within cell boundaries
|
|
2028
2075
|
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2029
2076
|
const cells = lineText.split(DELIMITER);
|
|
2030
2077
|
let currentOffset = 0;
|
|
@@ -2035,7 +2082,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2035
2082
|
for (let i = 0; i < cells.length; i++) {
|
|
2036
2083
|
const cellLength = cells[i]?.length ?? 0;
|
|
2037
2084
|
const cellEndColThisIteration = currentOffset + cellLength;
|
|
2038
|
-
|
|
2085
|
+
|
|
2039
2086
|
if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
|
|
2040
2087
|
targetCellIndex = i;
|
|
2041
2088
|
cellStartCol = currentOffset;
|
|
@@ -2048,7 +2095,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2048
2095
|
/* allow "…cell content + delimiter" selections */
|
|
2049
2096
|
const wouldClampStart = targetCellIndex > 0 && selStart[1] === cellStartCol - DELIMITER.length;
|
|
2050
2097
|
const wouldClampEnd = targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length;
|
|
2051
|
-
|
|
2098
|
+
|
|
2052
2099
|
console.log(`[ep_data_tables:cut-handler] Cut selection analysis:`, {
|
|
2053
2100
|
targetCellIndex,
|
|
2054
2101
|
selStartCol: selStart[1],
|
|
@@ -2061,15 +2108,15 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2061
2108
|
wouldClampStart,
|
|
2062
2109
|
wouldClampEnd
|
|
2063
2110
|
});
|
|
2064
|
-
|
|
2111
|
+
|
|
2065
2112
|
if (wouldClampStart) {
|
|
2066
2113
|
console.log(`[ep_data_tables:cut-handler] CLAMPING cut selection start from ${selStart[1]} to ${cellStartCol}`);
|
|
2067
|
-
selStart[1] = cellStartCol;
|
|
2114
|
+
selStart[1] = cellStartCol;
|
|
2068
2115
|
}
|
|
2069
|
-
|
|
2116
|
+
|
|
2070
2117
|
if (wouldClampEnd) {
|
|
2071
2118
|
console.log(`[ep_data_tables:cut-handler] CLAMPING cut selection end from ${selEnd[1]} to ${cellEndCol}`);
|
|
2072
|
-
selEnd[1] = cellEndCol;
|
|
2119
|
+
selEnd[1] = cellEndCol;
|
|
2073
2120
|
}
|
|
2074
2121
|
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
|
|
2075
2122
|
console.warn(`${cutLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing cut to protect table structure.`);
|
|
@@ -2077,16 +2124,13 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2077
2124
|
return;
|
|
2078
2125
|
}
|
|
2079
2126
|
|
|
2080
|
-
// If we reach here, the selection is entirely within a single cell - allow cut and preserve table structure
|
|
2081
2127
|
console.log(`${cutLogPrefix} Selection is entirely within cell ${targetCellIndex}. Intercepting cut to preserve table structure.`);
|
|
2082
2128
|
evt.preventDefault();
|
|
2083
2129
|
|
|
2084
2130
|
try {
|
|
2085
|
-
// Get the selected text to copy to clipboard
|
|
2086
2131
|
const selectedText = lineText.substring(selStart[1], selEnd[1]);
|
|
2087
2132
|
console.log(`${cutLogPrefix} Selected text to cut: "${selectedText}"`);
|
|
2088
2133
|
|
|
2089
|
-
// Copy to clipboard manually
|
|
2090
2134
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2091
2135
|
navigator.clipboard.writeText(selectedText).then(() => {
|
|
2092
2136
|
console.log(`${cutLogPrefix} Successfully copied to clipboard via Navigator API.`);
|
|
@@ -2094,7 +2138,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2094
2138
|
console.warn(`${cutLogPrefix} Failed to copy to clipboard via Navigator API:`, err);
|
|
2095
2139
|
});
|
|
2096
2140
|
} else {
|
|
2097
|
-
// Fallback for older browsers
|
|
2098
2141
|
console.log(`${cutLogPrefix} Using fallback clipboard method.`);
|
|
2099
2142
|
const textArea = document.createElement('textarea');
|
|
2100
2143
|
textArea.value = selectedText;
|
|
@@ -2109,17 +2152,15 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2109
2152
|
document.body.removeChild(textArea);
|
|
2110
2153
|
}
|
|
2111
2154
|
|
|
2112
|
-
// Now perform the deletion within the cell using ace operations
|
|
2113
2155
|
console.log(`${cutLogPrefix} Performing deletion via ed.ace_callWithAce.`);
|
|
2114
2156
|
ed.ace_callWithAce((aceInstance) => {
|
|
2115
2157
|
const callAceLogPrefix = `${cutLogPrefix}[ace_callWithAceOps]`;
|
|
2116
2158
|
console.log(`${callAceLogPrefix} Entered ace_callWithAce for cut operations. selStart:`, selStart, `selEnd:`, selEnd);
|
|
2117
|
-
|
|
2159
|
+
|
|
2118
2160
|
console.log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to delete selected text.`);
|
|
2119
2161
|
aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, '');
|
|
2120
2162
|
console.log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
|
|
2121
2163
|
|
|
2122
|
-
// --- Ensure cell is not left empty (zero-length) ---
|
|
2123
2164
|
const repAfterDeletion = aceInstance.ace_getRep();
|
|
2124
2165
|
const lineTextAfterDeletion = repAfterDeletion.lines.atIndex(lineNum).text;
|
|
2125
2166
|
const cellsAfterDeletion = lineTextAfterDeletion.split(DELIMITER);
|
|
@@ -2127,10 +2168,9 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2127
2168
|
|
|
2128
2169
|
if (cellTextAfterDeletion.length === 0) {
|
|
2129
2170
|
console.log(`${callAceLogPrefix} Cell ${targetCellIndex} became empty after cut – inserting single space to preserve structure.`);
|
|
2130
|
-
const insertPos = [lineNum, selStart[1]];
|
|
2171
|
+
const insertPos = [lineNum, selStart[1]];
|
|
2131
2172
|
aceInstance.ace_performDocumentReplaceRange(insertPos, insertPos, ' ');
|
|
2132
2173
|
|
|
2133
|
-
// NEW – re-apply td attribute to the freshly inserted space
|
|
2134
2174
|
const attrStart = insertPos;
|
|
2135
2175
|
const attrEnd = [insertPos[0], insertPos[1] + 1];
|
|
2136
2176
|
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
@@ -2141,7 +2181,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2141
2181
|
console.log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
|
|
2142
2182
|
const repAfterCut = aceInstance.ace_getRep();
|
|
2143
2183
|
console.log(`${callAceLogPrefix} Fetched rep after cut for applyMeta. Line ${lineNum} text now: "${repAfterCut.lines.atIndex(lineNum).text}"`);
|
|
2144
|
-
|
|
2184
|
+
|
|
2145
2185
|
ed.ep_data_tables_applyMeta(
|
|
2146
2186
|
lineNum,
|
|
2147
2187
|
tableMetadata.tblId,
|
|
@@ -2169,16 +2209,14 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2169
2209
|
}
|
|
2170
2210
|
});
|
|
2171
2211
|
|
|
2172
|
-
// *** BEFOREINPUT EVENT LISTENER FOR CONTEXT-MENU DELETE ***
|
|
2173
2212
|
// log(`${callWithAceLogPrefix} Attaching beforeinput event listener to $inner (inner iframe body).`);
|
|
2174
2213
|
$inner.on('beforeinput', (evt) => {
|
|
2175
2214
|
const deleteLogPrefix = '[ep_data_tables:beforeinputDeleteHandler]';
|
|
2176
2215
|
// log(`${deleteLogPrefix} BEFOREINPUT EVENT TRIGGERED. inputType: "${evt.originalEvent.inputType}", event object:`, evt);
|
|
2177
2216
|
|
|
2178
|
-
// Only intercept deletion-related input events
|
|
2179
2217
|
if (!evt.originalEvent.inputType || !evt.originalEvent.inputType.startsWith('delete')) {
|
|
2180
2218
|
// log(`${deleteLogPrefix} Not a deletion event (inputType: "${evt.originalEvent.inputType}"). Allowing default.`);
|
|
2181
|
-
return;
|
|
2219
|
+
return;
|
|
2182
2220
|
}
|
|
2183
2221
|
|
|
2184
2222
|
// log(`${deleteLogPrefix} Getting current editor representation (rep).`);
|
|
@@ -2186,7 +2224,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2186
2224
|
if (!rep || !rep.selStart) {
|
|
2187
2225
|
// log(`${deleteLogPrefix} WARNING: Could not get representation or selection. Allowing default delete.`);
|
|
2188
2226
|
console.warn(`${deleteLogPrefix} Could not get rep or selStart.`);
|
|
2189
|
-
return;
|
|
2227
|
+
return;
|
|
2190
2228
|
}
|
|
2191
2229
|
// log(`${deleteLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
|
|
2192
2230
|
const selStart = rep.selStart;
|
|
@@ -2194,23 +2232,19 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2194
2232
|
const lineNum = selStart[0];
|
|
2195
2233
|
// log(`${deleteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
|
|
2196
2234
|
|
|
2197
|
-
// Android Chrome IME: collapsed backspace/forward-delete often comes via beforeinput
|
|
2198
2235
|
const isAndroidChrome = isAndroidUA();
|
|
2199
2236
|
const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
|
|
2200
2237
|
|
|
2201
|
-
// Handle collapsed deletes on Android Chrome inside a table line to protect delimiters
|
|
2202
2238
|
const isCollapsed = (selStart[0] === selEnd[0] && selStart[1] === selEnd[1]);
|
|
2203
2239
|
if (isCollapsed && isAndroidChrome && (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward')) {
|
|
2204
|
-
// Resolve metadata for this line
|
|
2205
2240
|
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2206
2241
|
let tableMetadata = null;
|
|
2207
2242
|
if (lineAttrString) { try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {} }
|
|
2208
2243
|
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2209
2244
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
|
|
2210
|
-
return;
|
|
2245
|
+
return;
|
|
2211
2246
|
}
|
|
2212
2247
|
|
|
2213
|
-
// Compute current cell and boundaries
|
|
2214
2248
|
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2215
2249
|
const cells = lineText.split(DELIMITER);
|
|
2216
2250
|
let currentOffset = 0;
|
|
@@ -2229,27 +2263,52 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2229
2263
|
currentOffset += cellLength + DELIMITER.length;
|
|
2230
2264
|
}
|
|
2231
2265
|
|
|
2232
|
-
if (targetCellIndex === -1) return;
|
|
2266
|
+
if (targetCellIndex === -1) return;
|
|
2233
2267
|
|
|
2234
2268
|
const isBackward = inputType === 'deleteContentBackward';
|
|
2235
2269
|
const caretCol = selStart[1];
|
|
2236
2270
|
|
|
2237
|
-
// Prevent deletion across delimiters or line boundaries
|
|
2238
2271
|
if ((isBackward && caretCol === cellStartCol) || (!isBackward && caretCol === cellEndCol)) {
|
|
2239
2272
|
evt.preventDefault();
|
|
2240
2273
|
return;
|
|
2241
2274
|
}
|
|
2242
2275
|
|
|
2243
|
-
// Intercept and perform one-character deletion via Ace
|
|
2244
2276
|
evt.preventDefault();
|
|
2245
2277
|
try {
|
|
2246
2278
|
ed.ace_callWithAce((aceInstance) => {
|
|
2279
|
+
// Refresh representation to account for any prior synchronous edits
|
|
2280
|
+
aceInstance.ace_fastIncorp(10);
|
|
2281
|
+
const freshRep = aceInstance.ace_getRep();
|
|
2282
|
+
|
|
2283
|
+
const freshLineEntry = freshRep.lines.atIndex(lineNum);
|
|
2284
|
+
const freshText = (freshLineEntry && freshLineEntry.text) || '';
|
|
2285
|
+
const freshCells = freshText.split(DELIMITER);
|
|
2286
|
+
|
|
2287
|
+
let offset = 0;
|
|
2288
|
+
let freshCellStart = 0;
|
|
2289
|
+
let freshCellEnd = 0;
|
|
2290
|
+
for (let i = 0; i < freshCells.length; i++) {
|
|
2291
|
+
const len = freshCells[i]?.length ?? 0;
|
|
2292
|
+
const end = offset + len;
|
|
2293
|
+
if (i === targetCellIndex) {
|
|
2294
|
+
freshCellStart = offset;
|
|
2295
|
+
freshCellEnd = end;
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
offset += len + DELIMITER.length;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
// If caret is flush against delimiter after refresh, abort to protect structure
|
|
2302
|
+
if ((isBackward && caretCol <= freshCellStart) || (!isBackward && caretCol >= freshCellEnd)) {
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2247
2306
|
const delStart = isBackward ? [lineNum, caretCol - 1] : [lineNum, caretCol];
|
|
2248
2307
|
const delEnd = isBackward ? [lineNum, caretCol] : [lineNum, caretCol + 1];
|
|
2249
2308
|
aceInstance.ace_performDocumentReplaceRange(delStart, delEnd, '');
|
|
2250
2309
|
|
|
2251
|
-
// Re-apply table metadata attribute to ensure renderer stability
|
|
2252
2310
|
const repAfter = aceInstance.ace_getRep();
|
|
2311
|
+
|
|
2253
2312
|
ed.ep_data_tables_applyMeta(
|
|
2254
2313
|
lineNum,
|
|
2255
2314
|
tableMetadata.tblId,
|
|
@@ -2261,13 +2320,11 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2261
2320
|
docManager
|
|
2262
2321
|
);
|
|
2263
2322
|
|
|
2264
|
-
// Set caret position after deletion
|
|
2265
2323
|
const newCaretCol = isBackward ? caretCol - 1 : caretCol;
|
|
2266
2324
|
const newCaretPos = [lineNum, newCaretCol];
|
|
2267
2325
|
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2268
2326
|
aceInstance.ace_fastIncorp(10);
|
|
2269
2327
|
|
|
2270
|
-
// Update last-clicked state if available
|
|
2271
2328
|
if (ed.ep_data_tables_editor && ed.ep_data_tables_editor.ep_data_tables_last_clicked && ed.ep_data_tables_editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
2272
2329
|
const newRelativePos = newCaretCol - cellStartCol;
|
|
2273
2330
|
ed.ep_data_tables_editor.ep_data_tables_last_clicked = {
|
|
@@ -2284,12 +2341,10 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2284
2341
|
return;
|
|
2285
2342
|
}
|
|
2286
2343
|
|
|
2287
|
-
// Non-Android or non-collapsed: require an actual selection to handle here; otherwise allow default
|
|
2288
2344
|
if (isCollapsed) {
|
|
2289
|
-
return;
|
|
2345
|
+
return;
|
|
2290
2346
|
}
|
|
2291
2347
|
|
|
2292
|
-
// Check if selection spans multiple lines
|
|
2293
2348
|
if (selStart[0] !== selEnd[0]) {
|
|
2294
2349
|
// log(`${deleteLogPrefix} WARNING: Selection spans multiple lines. Preventing delete to protect table structure.`);
|
|
2295
2350
|
evt.preventDefault();
|
|
@@ -2301,25 +2356,22 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2301
2356
|
let tableMetadata = null;
|
|
2302
2357
|
|
|
2303
2358
|
if (lineAttrString) {
|
|
2304
|
-
// Fast-path: attribute exists – parse it.
|
|
2305
2359
|
try {
|
|
2306
2360
|
tableMetadata = JSON.parse(lineAttrString);
|
|
2307
2361
|
} catch {}
|
|
2308
2362
|
}
|
|
2309
2363
|
|
|
2310
2364
|
if (!tableMetadata) {
|
|
2311
|
-
// Fallback for block-styled rows – reconstruct via DOM helper.
|
|
2312
2365
|
tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2313
2366
|
}
|
|
2314
2367
|
|
|
2315
2368
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
|
|
2316
2369
|
// log(`${deleteLogPrefix} Line ${lineNum} is NOT a recognised table line. Allowing default delete.`);
|
|
2317
|
-
return;
|
|
2370
|
+
return;
|
|
2318
2371
|
}
|
|
2319
2372
|
|
|
2320
2373
|
// log(`${deleteLogPrefix} Line ${lineNum} IS a table line. Metadata:`, tableMetadata);
|
|
2321
2374
|
|
|
2322
|
-
// Validate selection is within cell boundaries
|
|
2323
2375
|
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2324
2376
|
const cells = lineText.split(DELIMITER);
|
|
2325
2377
|
let currentOffset = 0;
|
|
@@ -2330,7 +2382,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2330
2382
|
for (let i = 0; i < cells.length; i++) {
|
|
2331
2383
|
const cellLength = cells[i]?.length ?? 0;
|
|
2332
2384
|
const cellEndColThisIteration = currentOffset + cellLength;
|
|
2333
|
-
|
|
2385
|
+
|
|
2334
2386
|
if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
|
|
2335
2387
|
targetCellIndex = i;
|
|
2336
2388
|
cellStartCol = currentOffset;
|
|
@@ -2343,7 +2395,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2343
2395
|
/* allow "…cell content + delimiter" selections */
|
|
2344
2396
|
const wouldClampStart = targetCellIndex > 0 && selStart[1] === cellStartCol - DELIMITER.length;
|
|
2345
2397
|
const wouldClampEnd = targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length;
|
|
2346
|
-
|
|
2398
|
+
|
|
2347
2399
|
console.log(`[ep_data_tables:beforeinput-delete] Delete selection analysis:`, {
|
|
2348
2400
|
targetCellIndex,
|
|
2349
2401
|
selStartCol: selStart[1],
|
|
@@ -2356,39 +2408,36 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2356
2408
|
wouldClampStart,
|
|
2357
2409
|
wouldClampEnd
|
|
2358
2410
|
});
|
|
2359
|
-
|
|
2411
|
+
|
|
2360
2412
|
if (wouldClampStart) {
|
|
2361
2413
|
console.log(`[ep_data_tables:beforeinput-delete] CLAMPING delete selection start from ${selStart[1]} to ${cellStartCol}`);
|
|
2362
|
-
selStart[1] = cellStartCol;
|
|
2414
|
+
selStart[1] = cellStartCol;
|
|
2363
2415
|
}
|
|
2364
|
-
|
|
2416
|
+
|
|
2365
2417
|
if (wouldClampEnd) {
|
|
2366
2418
|
console.log(`[ep_data_tables:beforeinput-delete] CLAMPING delete selection end from ${selEnd[1]} to ${cellEndCol}`);
|
|
2367
|
-
selEnd[1] = cellEndCol;
|
|
2419
|
+
selEnd[1] = cellEndCol;
|
|
2368
2420
|
}
|
|
2369
|
-
|
|
2421
|
+
|
|
2370
2422
|
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
|
|
2371
2423
|
// log(`${deleteLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing delete to protect table structure.`);
|
|
2372
2424
|
evt.preventDefault();
|
|
2373
2425
|
return;
|
|
2374
2426
|
}
|
|
2375
2427
|
|
|
2376
|
-
// If we reach here, the selection is entirely within a single cell - intercept delete and preserve table structure
|
|
2377
2428
|
// log(`${deleteLogPrefix} Selection is entirely within cell ${targetCellIndex}. Intercepting delete to preserve table structure.`);
|
|
2378
2429
|
evt.preventDefault();
|
|
2379
2430
|
|
|
2380
2431
|
try {
|
|
2381
|
-
// No clipboard operations needed for delete - just perform the deletion within the cell using ace operations
|
|
2382
2432
|
// log(`${deleteLogPrefix} Performing deletion via ed.ace_callWithAce.`);
|
|
2383
2433
|
ed.ace_callWithAce((aceInstance) => {
|
|
2384
2434
|
const callAceLogPrefix = `${deleteLogPrefix}[ace_callWithAceOps]`;
|
|
2385
2435
|
// log(`${callAceLogPrefix} Entered ace_callWithAce for delete operations. selStart:`, selStart, `selEnd:`, selEnd);
|
|
2386
|
-
|
|
2436
|
+
|
|
2387
2437
|
// log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to delete selected text.`);
|
|
2388
2438
|
aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, '');
|
|
2389
2439
|
// log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
|
|
2390
2440
|
|
|
2391
|
-
// --- Ensure cell is not left empty (zero-length) ---
|
|
2392
2441
|
const repAfterDeletion = aceInstance.ace_getRep();
|
|
2393
2442
|
const lineTextAfterDeletion = repAfterDeletion.lines.atIndex(lineNum).text;
|
|
2394
2443
|
const cellsAfterDeletion = lineTextAfterDeletion.split(DELIMITER);
|
|
@@ -2396,10 +2445,9 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2396
2445
|
|
|
2397
2446
|
if (cellTextAfterDeletion.length === 0) {
|
|
2398
2447
|
// log(`${callAceLogPrefix} Cell ${targetCellIndex} became empty after delete – inserting single space to preserve structure.`);
|
|
2399
|
-
const insertPos = [lineNum, selStart[1]];
|
|
2448
|
+
const insertPos = [lineNum, selStart[1]];
|
|
2400
2449
|
aceInstance.ace_performDocumentReplaceRange(insertPos, insertPos, ' ');
|
|
2401
2450
|
|
|
2402
|
-
// NEW – give the placeholder its cell attribute back
|
|
2403
2451
|
const attrStart = insertPos;
|
|
2404
2452
|
const attrEnd = [insertPos[0], insertPos[1] + 1];
|
|
2405
2453
|
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
@@ -2410,7 +2458,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2410
2458
|
// log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
|
|
2411
2459
|
const repAfterDelete = aceInstance.ace_getRep();
|
|
2412
2460
|
// log(`${callAceLogPrefix} Fetched rep after delete for applyMeta. Line ${lineNum} text now: "${repAfterDelete.lines.atIndex(lineNum).text}"`);
|
|
2413
|
-
|
|
2461
|
+
|
|
2414
2462
|
ed.ep_data_tables_applyMeta(
|
|
2415
2463
|
lineNum,
|
|
2416
2464
|
tableMetadata.tblId,
|
|
@@ -2423,7 +2471,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2423
2471
|
);
|
|
2424
2472
|
// log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
|
|
2425
2473
|
|
|
2426
|
-
// Determine new caret position – one char forward if we inserted a space
|
|
2427
2474
|
const newCaretAbsoluteCol = (cellTextAfterDeletion.length === 0) ? selStart[1] + 1 : selStart[1];
|
|
2428
2475
|
const newCaretPos = [lineNum, newCaretAbsoluteCol];
|
|
2429
2476
|
// log(`${callAceLogPrefix} Setting caret position to: [${newCaretPos}].`);
|
|
@@ -2440,25 +2487,22 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2440
2487
|
}
|
|
2441
2488
|
});
|
|
2442
2489
|
|
|
2443
|
-
// Android Chrome IME insert handling (targeted fix)
|
|
2444
2490
|
$inner.on('beforeinput', (evt) => {
|
|
2445
2491
|
const insertLogPrefix = '[ep_data_tables:beforeinputInsertHandler]';
|
|
2446
2492
|
const inputType = evt.originalEvent && evt.originalEvent.inputType || '';
|
|
2447
2493
|
|
|
2448
|
-
// Only intercept insert types
|
|
2449
2494
|
if (!inputType || !inputType.startsWith('insert')) return;
|
|
2450
2495
|
|
|
2451
|
-
|
|
2496
|
+
if ((evt && evt._epDataTablesHandled) || (evt.originalEvent && evt.originalEvent._epDataTablesHandled)) return;
|
|
2497
|
+
|
|
2452
2498
|
if (!isAndroidUA()) return;
|
|
2453
2499
|
|
|
2454
|
-
// Get current selection and ensure we are inside a table line
|
|
2455
2500
|
const rep = ed.ace_getRep();
|
|
2456
2501
|
if (!rep || !rep.selStart) return;
|
|
2457
2502
|
const selStart = rep.selStart;
|
|
2458
2503
|
const selEnd = rep.selEnd;
|
|
2459
2504
|
const lineNum = selStart[0];
|
|
2460
2505
|
|
|
2461
|
-
// Resolve table metadata for this line
|
|
2462
2506
|
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2463
2507
|
let tableMetadata = null;
|
|
2464
2508
|
if (lineAttrString) {
|
|
@@ -2466,10 +2510,9 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2466
2510
|
}
|
|
2467
2511
|
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2468
2512
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
|
|
2469
|
-
return;
|
|
2513
|
+
return;
|
|
2470
2514
|
}
|
|
2471
2515
|
|
|
2472
|
-
// Compute cell boundaries and target cell index
|
|
2473
2516
|
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2474
2517
|
const cells = lineText.split(DELIMITER);
|
|
2475
2518
|
let currentOffset = 0;
|
|
@@ -2488,65 +2531,95 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2488
2531
|
currentOffset += cellLength + DELIMITER.length;
|
|
2489
2532
|
}
|
|
2490
2533
|
|
|
2491
|
-
// Clamp selection end if it includes a trailing delimiter of the same cell
|
|
2492
|
-
if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
|
|
2493
|
-
selEnd[1] = cellEndCol;
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
// If selection spills outside the cell boundaries, block to protect structure
|
|
2497
2534
|
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
|
|
2498
2535
|
evt.preventDefault();
|
|
2499
2536
|
return;
|
|
2500
2537
|
}
|
|
2501
2538
|
|
|
2502
|
-
// Handle line breaks by routing to Enter behavior
|
|
2503
2539
|
if (inputType === 'insertParagraph' || inputType === 'insertLineBreak') {
|
|
2504
2540
|
evt.preventDefault();
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2541
|
+
evt.stopPropagation();
|
|
2542
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2543
|
+
setTimeout(() => {
|
|
2544
|
+
try {
|
|
2545
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2546
|
+
aceInstance.ace_fastIncorp(10);
|
|
2547
|
+
const freshRep = aceInstance.ace_getRep();
|
|
2548
|
+
const freshSelStart = freshRep.selStart;
|
|
2549
|
+
const freshSelEnd = freshRep.selEnd;
|
|
2550
|
+
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, ' ');
|
|
2551
|
+
|
|
2552
|
+
const afterRep = aceInstance.ace_getRep();
|
|
2553
|
+
const maxLen = Math.max(0, afterRep.lines.atIndex(lineNum)?.text?.length || 0);
|
|
2554
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
2555
|
+
const endCol = Math.min(startCol + 1, maxLen);
|
|
2556
|
+
if (endCol > startCol) {
|
|
2557
|
+
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
2558
|
+
[lineNum, startCol], [lineNum, endCol], [[ATTR_CELL, String(targetCellIndex)]]
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
ed.ep_data_tables_applyMeta(
|
|
2563
|
+
lineNum, tableMetadata.tblId, tableMetadata.row, tableMetadata.cols,
|
|
2564
|
+
afterRep, ed, null, docManager
|
|
2565
|
+
);
|
|
2566
|
+
|
|
2567
|
+
const newCaretPos = [lineNum, endCol];
|
|
2568
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2569
|
+
aceInstance.ace_fastIncorp(10);
|
|
2570
|
+
}, 'iosSoftBreakToSpace', true);
|
|
2571
|
+
} catch (e) {
|
|
2572
|
+
console.error(`${autoLogPrefix} ERROR replacing soft break:`, e);
|
|
2573
|
+
}
|
|
2574
|
+
}, 0);
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
|
|
2579
|
+
selEnd[1] = cellEndCol;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
if (inputType === 'insertParagraph' || inputType === 'insertLineBreak') {
|
|
2583
|
+
evt.preventDefault();
|
|
2584
|
+
try {
|
|
2585
|
+
navigateToCellBelow(lineNum, targetCellIndex, tableMetadata, ed, docManager);
|
|
2586
|
+
} catch (e) { console.error(`${insertLogPrefix} Error navigating on line break:`, e); }
|
|
2508
2587
|
return;
|
|
2509
2588
|
}
|
|
2510
2589
|
|
|
2511
|
-
// If we are in Android Chrome composition flow, suppress all insertText until composition ends
|
|
2512
2590
|
if (suppressBeforeInputInsertTextDuringComposition && inputType === 'insertText') {
|
|
2513
2591
|
evt.preventDefault();
|
|
2514
2592
|
return;
|
|
2515
2593
|
}
|
|
2516
2594
|
|
|
2517
|
-
// If we already handled one insertion via composition handler, skip once (legacy single-shot)
|
|
2518
2595
|
if (suppressNextBeforeInputInsertTextOnce && inputType === 'insertText') {
|
|
2519
2596
|
suppressNextBeforeInputInsertTextOnce = false;
|
|
2520
2597
|
evt.preventDefault();
|
|
2521
2598
|
return;
|
|
2522
2599
|
}
|
|
2523
2600
|
|
|
2524
|
-
// If composition session is active, let composition handler manage
|
|
2525
2601
|
if (isAndroidChromeComposition) return;
|
|
2526
2602
|
|
|
2527
|
-
// Only proceed for textual insertions we can retrieve
|
|
2528
2603
|
const rawData = evt.originalEvent && typeof evt.originalEvent.data === 'string' ? evt.originalEvent.data : '';
|
|
2529
|
-
// If no data for this insert type, allow default (paste/drop paths are handled elsewhere)
|
|
2530
2604
|
if (!rawData) return;
|
|
2531
2605
|
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2606
|
+
let insertedText = normalizeSoftWhitespace(
|
|
2607
|
+
rawData
|
|
2608
|
+
.replace(new RegExp(DELIMITER, 'g'), ' ')
|
|
2609
|
+
.replace(/[\u200B\u200C\u200D\uFEFF]/g, '')
|
|
2610
|
+
.replace(/\s+/g, ' ')
|
|
2611
|
+
);
|
|
2537
2612
|
|
|
2538
2613
|
if (insertedText.length === 0) {
|
|
2539
2614
|
evt.preventDefault();
|
|
2540
2615
|
return;
|
|
2541
2616
|
}
|
|
2542
2617
|
|
|
2543
|
-
// Intercept the browser default and perform the edit via Ace APIs
|
|
2544
2618
|
evt.preventDefault();
|
|
2545
2619
|
evt.stopPropagation();
|
|
2546
2620
|
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2547
2621
|
|
|
2548
2622
|
try {
|
|
2549
|
-
// Defer to next tick to avoid racing with browser internal composition state
|
|
2550
2623
|
setTimeout(() => {
|
|
2551
2624
|
ed.ace_callWithAce((aceInstance) => {
|
|
2552
2625
|
aceInstance.ace_fastIncorp(10);
|
|
@@ -2554,10 +2627,8 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2554
2627
|
const freshSelStart = freshRep.selStart;
|
|
2555
2628
|
const freshSelEnd = freshRep.selEnd;
|
|
2556
2629
|
|
|
2557
|
-
// Replace selection with sanitized text
|
|
2558
2630
|
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
|
|
2559
2631
|
|
|
2560
|
-
// Re-apply cell attribute to the newly inserted text (clamped to line length)
|
|
2561
2632
|
const repAfterReplace = aceInstance.ace_getRep();
|
|
2562
2633
|
const freshLineIndex = freshSelStart[0];
|
|
2563
2634
|
const freshLineEntry = repAfterReplace.lines.atIndex(freshLineIndex);
|
|
@@ -2571,7 +2642,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2571
2642
|
);
|
|
2572
2643
|
}
|
|
2573
2644
|
|
|
2574
|
-
// Re-apply table metadata line attribute
|
|
2575
2645
|
ed.ep_data_tables_applyMeta(
|
|
2576
2646
|
freshLineIndex,
|
|
2577
2647
|
tableMetadata.tblId,
|
|
@@ -2583,14 +2653,12 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2583
2653
|
docManager
|
|
2584
2654
|
);
|
|
2585
2655
|
|
|
2586
|
-
// Move caret to end of inserted text and update last-click state
|
|
2587
2656
|
const newCaretCol = endCol;
|
|
2588
2657
|
const newCaretPos = [freshLineIndex, newCaretCol];
|
|
2589
2658
|
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2590
2659
|
aceInstance.ace_fastIncorp(10);
|
|
2591
2660
|
|
|
2592
2661
|
if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
2593
|
-
// Recompute cellStartCol for the fresh line to avoid mismatches
|
|
2594
2662
|
const freshLineText = (freshLineEntry && freshLineEntry.text) || '';
|
|
2595
2663
|
const freshCells = freshLineText.split(DELIMITER);
|
|
2596
2664
|
let freshOffset = 0;
|
|
@@ -2612,7 +2680,271 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2612
2680
|
}
|
|
2613
2681
|
});
|
|
2614
2682
|
|
|
2615
|
-
|
|
2683
|
+
$inner.on('beforeinput', (evt) => {
|
|
2684
|
+
const genericLogPrefix = '[ep_data_tables:beforeinputInsertTextGeneric]';
|
|
2685
|
+
const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
|
|
2686
|
+
if (!inputType || !inputType.startsWith('insert')) return;
|
|
2687
|
+
|
|
2688
|
+
if (evt._epDataTablesNormalized || (evt.originalEvent && evt.originalEvent._epDataTablesNormalized)) return;
|
|
2689
|
+
if (isAndroidUA() || isIOSUA()) return;
|
|
2690
|
+
|
|
2691
|
+
const rep = ed.ace_getRep();
|
|
2692
|
+
if (!rep || !rep.selStart) return;
|
|
2693
|
+
const selStart = rep.selStart;
|
|
2694
|
+
const selEnd = rep.selEnd;
|
|
2695
|
+
const lineNum = selStart[0];
|
|
2696
|
+
|
|
2697
|
+
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2698
|
+
let tableMetadata = null;
|
|
2699
|
+
if (lineAttrString) {
|
|
2700
|
+
try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {}
|
|
2701
|
+
}
|
|
2702
|
+
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2703
|
+
if (!tableMetadata || typeof tableMetadata.cols !== 'number') return;
|
|
2704
|
+
console.debug(`${genericLogPrefix} event`, { inputType, data: evt.originalEvent && evt.originalEvent.data });
|
|
2705
|
+
|
|
2706
|
+
const rawData = evt.originalEvent && typeof evt.originalEvent.data === 'string' ? evt.originalEvent.data : ' ';
|
|
2707
|
+
|
|
2708
|
+
const insertedText = normalizeSoftWhitespace(
|
|
2709
|
+
rawData
|
|
2710
|
+
.replace(/[\u00A0\r\n\t]/g, ' ') // NBSP sanitized back to space for stability
|
|
2711
|
+
.replace(new RegExp(DELIMITER, 'g'), ' ')
|
|
2712
|
+
.replace(/[\u200B\u200C\u200D\uFEFF]/g, '')
|
|
2713
|
+
.replace(/\s+/g, ' ')
|
|
2714
|
+
);
|
|
2715
|
+
|
|
2716
|
+
if (!insertedText) { evt.preventDefault(); return; }
|
|
2717
|
+
|
|
2718
|
+
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2719
|
+
const cells = lineText.split(DELIMITER);
|
|
2720
|
+
let currentOffset = 0;
|
|
2721
|
+
let targetCellIndex = -1;
|
|
2722
|
+
let cellStartCol = 0;
|
|
2723
|
+
let cellEndCol = 0;
|
|
2724
|
+
for (let i = 0; i < cells.length; i++) {
|
|
2725
|
+
const len = cells[i]?.length ?? 0;
|
|
2726
|
+
const end = currentOffset + len;
|
|
2727
|
+
if (selStart[1] >= currentOffset && selStart[1] <= end) {
|
|
2728
|
+
targetCellIndex = i;
|
|
2729
|
+
cellStartCol = currentOffset;
|
|
2730
|
+
cellEndCol = end;
|
|
2731
|
+
break;
|
|
2732
|
+
}
|
|
2733
|
+
currentOffset += len + DELIMITER.length;
|
|
2734
|
+
}
|
|
2735
|
+
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) { evt.preventDefault(); console.debug(`${genericLogPrefix} abort: selection outside cell`, { selStart, selEnd, cellStartCol, cellEndCol }); return; }
|
|
2736
|
+
|
|
2737
|
+
evt.preventDefault();
|
|
2738
|
+
evt.stopPropagation();
|
|
2739
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2740
|
+
|
|
2741
|
+
try {
|
|
2742
|
+
ed.ace_callWithAce((ace) => {
|
|
2743
|
+
ace.ace_fastIncorp(10);
|
|
2744
|
+
const freshRep = ace.ace_getRep();
|
|
2745
|
+
const freshSelStart = freshRep.selStart;
|
|
2746
|
+
const freshSelEnd = freshRep.selEnd;
|
|
2747
|
+
|
|
2748
|
+
ace.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
|
|
2749
|
+
|
|
2750
|
+
const afterRep = ace.ace_getRep();
|
|
2751
|
+
const lineEntry = afterRep.lines.atIndex(lineNum);
|
|
2752
|
+
const maxLen = lineEntry ? lineEntry.text.length : 0;
|
|
2753
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
2754
|
+
const endCol = Math.min(startCol + insertedText.length, maxLen);
|
|
2755
|
+
if (endCol > startCol) {
|
|
2756
|
+
ace.ace_performDocumentApplyAttributesToRange([lineNum, startCol], [lineNum, endCol], [[ATTR_CELL, String(targetCellIndex)]]);
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
ed.ep_data_tables_applyMeta(lineNum, tableMetadata.tblId, tableMetadata.row, tableMetadata.cols, afterRep, ed, null, docManager);
|
|
2760
|
+
|
|
2761
|
+
const newCaretPos = [lineNum, endCol];
|
|
2762
|
+
ace.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2763
|
+
}, 'tableGenericInsertText', true);
|
|
2764
|
+
} catch (e) {
|
|
2765
|
+
console.error(`${genericLogPrefix} ERROR handling generic insertText:`, e);
|
|
2766
|
+
}
|
|
2767
|
+
});
|
|
2768
|
+
|
|
2769
|
+
$inner.on('beforeinput', (evt) => {
|
|
2770
|
+
const autoLogPrefix = '[ep_data_tables:beforeinputAutoReplaceHandler]';
|
|
2771
|
+
const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
|
|
2772
|
+
|
|
2773
|
+
if ((evt && evt._epDataTablesHandled) || (evt.originalEvent && evt.originalEvent._epDataTablesHandled)) return;
|
|
2774
|
+
|
|
2775
|
+
if (!isIOSUA()) return;
|
|
2776
|
+
|
|
2777
|
+
const rep = ed.ace_getRep();
|
|
2778
|
+
if (!rep || !rep.selStart) return;
|
|
2779
|
+
const selStart = rep.selStart;
|
|
2780
|
+
const selEnd = rep.selEnd;
|
|
2781
|
+
const lineNum = selStart[0];
|
|
2782
|
+
|
|
2783
|
+
const dataStr = (evt.originalEvent && typeof evt.originalEvent.data === 'string') ? evt.originalEvent.data : '';
|
|
2784
|
+
const hasSelection = !(selStart[0] === selEnd[0] && selStart[1] === selEnd[1]);
|
|
2785
|
+
const looksLikeIOSAutoReplace = inputType === 'insertText' && dataStr.length > 1;
|
|
2786
|
+
const insertTextNull = inputType === 'insertText' && dataStr === '' && !hasSelection;
|
|
2787
|
+
const shouldHandle = INPUTTYPE_REPLACEMENT_TYPES.has(inputType) || looksLikeIOSAutoReplace || (inputType === 'insertText' && (hasSelection || insertTextNull));
|
|
2788
|
+
if (!shouldHandle) return;
|
|
2789
|
+
|
|
2790
|
+
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2791
|
+
let tableMetadata = null;
|
|
2792
|
+
if (lineAttrString) {
|
|
2793
|
+
try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {}
|
|
2794
|
+
}
|
|
2795
|
+
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2796
|
+
if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2801
|
+
const cells = lineText.split(DELIMITER);
|
|
2802
|
+
let currentOffset = 0;
|
|
2803
|
+
let targetCellIndex = -1;
|
|
2804
|
+
let cellStartCol = 0;
|
|
2805
|
+
let cellEndCol = 0;
|
|
2806
|
+
for (let i = 0; i < cells.length; i++) {
|
|
2807
|
+
const cellLength = cells[i]?.length ?? 0;
|
|
2808
|
+
const cellEndColThisIteration = currentOffset + cellLength;
|
|
2809
|
+
if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
|
|
2810
|
+
targetCellIndex = i;
|
|
2811
|
+
cellStartCol = currentOffset;
|
|
2812
|
+
cellEndCol = cellEndColThisIteration;
|
|
2813
|
+
break;
|
|
2814
|
+
}
|
|
2815
|
+
currentOffset += cellLength + DELIMITER.length;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
|
|
2819
|
+
selEnd[1] = cellEndCol;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
|
|
2823
|
+
evt.preventDefault();
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
let insertedText = dataStr;
|
|
2828
|
+
if (!insertedText) {
|
|
2829
|
+
if (insertTextNull) {
|
|
2830
|
+
evt.preventDefault();
|
|
2831
|
+
evt.stopPropagation();
|
|
2832
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2833
|
+
|
|
2834
|
+
setTimeout(() => {
|
|
2835
|
+
try {
|
|
2836
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2837
|
+
aceInstance.ace_fastIncorp(10);
|
|
2838
|
+
const freshRep = aceInstance.ace_getRep();
|
|
2839
|
+
const freshSelStart = freshRep.selStart;
|
|
2840
|
+
const freshSelEnd = freshRep.selEnd;
|
|
2841
|
+
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, ' ');
|
|
2842
|
+
|
|
2843
|
+
const afterRep = aceInstance.ace_getRep();
|
|
2844
|
+
const maxLen = Math.max(0, afterRep.lines.atIndex(lineNum)?.text?.length || 0);
|
|
2845
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
2846
|
+
const endCol = Math.min(startCol + 1, maxLen);
|
|
2847
|
+
if (endCol > startCol) {
|
|
2848
|
+
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
2849
|
+
[lineNum, startCol], [lineNum, endCol], [[ATTR_CELL, String(targetCellIndex)]]
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
ed.ep_data_tables_applyMeta(
|
|
2854
|
+
lineNum, tableMetadata.tblId, tableMetadata.row, tableMetadata.cols,
|
|
2855
|
+
afterRep, ed, null, docManager
|
|
2856
|
+
);
|
|
2857
|
+
|
|
2858
|
+
const newCaretPos = [lineNum, endCol];
|
|
2859
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2860
|
+
aceInstance.ace_fastIncorp(10);
|
|
2861
|
+
}, 'iosPredictiveCommit', true);
|
|
2862
|
+
} catch (e) {
|
|
2863
|
+
console.error(`${autoLogPrefix} ERROR fixing predictive commit:`, e);
|
|
2864
|
+
}
|
|
2865
|
+
}, 0);
|
|
2866
|
+
return;
|
|
2867
|
+
} else {
|
|
2868
|
+
if (INPUTTYPE_REPLACEMENT_TYPES.has(inputType) || hasSelection) {
|
|
2869
|
+
evt.preventDefault();
|
|
2870
|
+
evt.stopPropagation();
|
|
2871
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2872
|
+
}
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
insertedText = normalizeSoftWhitespace(
|
|
2878
|
+
insertedText
|
|
2879
|
+
.replace(new RegExp(DELIMITER, 'g'), ' ')
|
|
2880
|
+
.replace(/[\u200B\u200C\u200D\uFEFF]/g, '')
|
|
2881
|
+
);
|
|
2882
|
+
|
|
2883
|
+
evt.preventDefault();
|
|
2884
|
+
evt.stopPropagation();
|
|
2885
|
+
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2886
|
+
|
|
2887
|
+
try {
|
|
2888
|
+
setTimeout(() => {
|
|
2889
|
+
ed.ace_callWithAce((aceInstance) => {
|
|
2890
|
+
aceInstance.ace_fastIncorp(10);
|
|
2891
|
+
const freshRep = aceInstance.ace_getRep();
|
|
2892
|
+
const freshSelStart = freshRep.selStart;
|
|
2893
|
+
const freshSelEnd = freshRep.selEnd;
|
|
2894
|
+
|
|
2895
|
+
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
|
|
2896
|
+
|
|
2897
|
+
const repAfterReplace = aceInstance.ace_getRep();
|
|
2898
|
+
const freshLineIndex = freshSelStart[0];
|
|
2899
|
+
const freshLineEntry = repAfterReplace.lines.atIndex(freshLineIndex);
|
|
2900
|
+
const maxLen = Math.max(0, (freshLineEntry && freshLineEntry.text) ? freshLineEntry.text.length : 0);
|
|
2901
|
+
const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
|
|
2902
|
+
const endColRaw = startCol + insertedText.length;
|
|
2903
|
+
const endCol = Math.min(endColRaw, maxLen);
|
|
2904
|
+
if (endCol > startCol) {
|
|
2905
|
+
aceInstance.ace_performDocumentApplyAttributesToRange(
|
|
2906
|
+
[freshLineIndex, startCol], [freshLineIndex, endCol], [[ATTR_CELL, String(targetCellIndex)]]
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
ed.ep_data_tables_applyMeta(
|
|
2911
|
+
freshLineIndex,
|
|
2912
|
+
tableMetadata.tblId,
|
|
2913
|
+
tableMetadata.row,
|
|
2914
|
+
tableMetadata.cols,
|
|
2915
|
+
repAfterReplace,
|
|
2916
|
+
ed,
|
|
2917
|
+
null,
|
|
2918
|
+
docManager
|
|
2919
|
+
);
|
|
2920
|
+
|
|
2921
|
+
const newCaretCol = endCol;
|
|
2922
|
+
const newCaretPos = [freshLineIndex, newCaretCol];
|
|
2923
|
+
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2924
|
+
aceInstance.ace_fastIncorp(10);
|
|
2925
|
+
|
|
2926
|
+
if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
2927
|
+
const freshLineText = (freshLineEntry && freshLineEntry.text) || '';
|
|
2928
|
+
const freshCells = freshLineText.split(DELIMITER);
|
|
2929
|
+
let freshOffset = 0;
|
|
2930
|
+
for (let i = 0; i < targetCellIndex; i++) {
|
|
2931
|
+
freshOffset += (freshCells[i]?.length ?? 0) + DELIMITER.length;
|
|
2932
|
+
}
|
|
2933
|
+
const newRelativePos = newCaretCol - freshOffset;
|
|
2934
|
+
editor.ep_data_tables_last_clicked = {
|
|
2935
|
+
lineNum: freshLineIndex,
|
|
2936
|
+
tblId: tableMetadata.tblId,
|
|
2937
|
+
cellIndex: targetCellIndex,
|
|
2938
|
+
relativePos: newRelativePos < 0 ? 0 : newRelativePos,
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
}, 'tableAutoReplaceTextOperations', true);
|
|
2942
|
+
}, 0);
|
|
2943
|
+
} catch (error) {
|
|
2944
|
+
console.error(`${autoLogPrefix} ERROR during auto-replace handling:`, error);
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2947
|
+
|
|
2616
2948
|
$inner.on('compositionstart', (evt) => {
|
|
2617
2949
|
if (!isAndroidUA()) return;
|
|
2618
2950
|
const rep = ed.ace_getRep();
|
|
@@ -2626,8 +2958,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2626
2958
|
handledCurrentComposition = false;
|
|
2627
2959
|
suppressBeforeInputInsertTextDuringComposition = false;
|
|
2628
2960
|
});
|
|
2629
|
-
|
|
2630
|
-
// Android Chrome composition handling for whitespace (space) to prevent DOM mutation breaking delimiters
|
|
2631
2961
|
$inner.on('compositionupdate', (evt) => {
|
|
2632
2962
|
const compLogPrefix = '[ep_data_tables:compositionHandler]';
|
|
2633
2963
|
|
|
@@ -2639,20 +2969,17 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2639
2969
|
const selEnd = rep.selEnd;
|
|
2640
2970
|
const lineNum = selStart[0];
|
|
2641
2971
|
|
|
2642
|
-
// Ensure we are inside a table line
|
|
2643
2972
|
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2644
2973
|
let tableMetadata = null;
|
|
2645
2974
|
if (lineAttrString) { try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {} }
|
|
2646
2975
|
if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
|
|
2647
2976
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number') return;
|
|
2648
2977
|
|
|
2649
|
-
// Only act on whitespace-only composition updates (space/non-breaking space)
|
|
2650
2978
|
const d = evt.originalEvent && typeof evt.originalEvent.data === 'string' ? evt.originalEvent.data : '';
|
|
2651
2979
|
if (evt.type === 'compositionupdate') {
|
|
2652
|
-
const isWhitespaceOnly = d && d
|
|
2980
|
+
const isWhitespaceOnly = d && normalizeSoftWhitespace(d).trim() === '';
|
|
2653
2981
|
if (!isWhitespaceOnly) return;
|
|
2654
2982
|
|
|
2655
|
-
// Compute target cell and clamp selection to cell boundaries
|
|
2656
2983
|
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2657
2984
|
const cells = lineText.split(DELIMITER);
|
|
2658
2985
|
let currentOffset = 0;
|
|
@@ -2672,12 +2999,13 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2672
2999
|
}
|
|
2673
3000
|
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) return;
|
|
2674
3001
|
|
|
2675
|
-
// Prevent composition DOM mutation and insert sanitized space via Ace
|
|
2676
3002
|
evt.preventDefault();
|
|
2677
3003
|
evt.stopPropagation();
|
|
2678
3004
|
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
2679
3005
|
|
|
2680
|
-
let insertedText = d
|
|
3006
|
+
let insertedText = d
|
|
3007
|
+
.replace(/[\u00A0\r\n\t]/g, ' ')
|
|
3008
|
+
.replace(/\s+/g, ' ');
|
|
2681
3009
|
if (insertedText.length === 0) insertedText = ' ';
|
|
2682
3010
|
|
|
2683
3011
|
try {
|
|
@@ -2689,7 +3017,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2689
3017
|
const freshSelEnd = freshRep.selEnd;
|
|
2690
3018
|
aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
|
|
2691
3019
|
|
|
2692
|
-
// Clamp attribute application to current line bounds and use fresh line index
|
|
2693
3020
|
const repAfterReplace = aceInstance.ace_getRep();
|
|
2694
3021
|
const freshLineIndex = freshSelStart[0];
|
|
2695
3022
|
const freshLineEntry = repAfterReplace.lines.atIndex(freshLineIndex);
|
|
@@ -2719,7 +3046,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2719
3046
|
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
2720
3047
|
aceInstance.ace_fastIncorp(10);
|
|
2721
3048
|
if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
2722
|
-
// Recompute cellStartCol for fresh line
|
|
2723
3049
|
const freshLineText = (freshLineEntry && freshLineEntry.text) || '';
|
|
2724
3050
|
const freshCells = freshLineText.split(DELIMITER);
|
|
2725
3051
|
let freshOffset = 0;
|
|
@@ -2736,7 +3062,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2736
3062
|
}
|
|
2737
3063
|
}, 'tableCompositionSpaceInsert', true);
|
|
2738
3064
|
}, 0);
|
|
2739
|
-
// Suppress all subsequent beforeinput insertText events in this composition session
|
|
2740
3065
|
suppressBeforeInputInsertTextDuringComposition = true;
|
|
2741
3066
|
} catch (error) {
|
|
2742
3067
|
console.error(`${compLogPrefix} ERROR inserting space during composition:`, error);
|
|
@@ -2744,7 +3069,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2744
3069
|
}
|
|
2745
3070
|
});
|
|
2746
3071
|
|
|
2747
|
-
// Composition end cleanup
|
|
2748
3072
|
$inner.on('compositionend', () => {
|
|
2749
3073
|
if (isAndroidChromeComposition) {
|
|
2750
3074
|
isAndroidChromeComposition = false;
|
|
@@ -2753,15 +3077,12 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2753
3077
|
}
|
|
2754
3078
|
});
|
|
2755
3079
|
|
|
2756
|
-
// *** DRAG AND DROP EVENT LISTENERS ***
|
|
2757
3080
|
// log(`${callWithAceLogPrefix} Attaching drag and drop event listeners to $inner (inner iframe body).`);
|
|
2758
|
-
|
|
2759
|
-
// Prevent drops that could damage table structure
|
|
3081
|
+
|
|
2760
3082
|
$inner.on('drop', (evt) => {
|
|
2761
3083
|
const dropLogPrefix = '[ep_data_tables:dropHandler]';
|
|
2762
3084
|
// log(`${dropLogPrefix} DROP EVENT TRIGGERED. Event object:`, evt);
|
|
2763
3085
|
|
|
2764
|
-
// Block drops directly targeted at a table element regardless of selection state
|
|
2765
3086
|
const targetEl = evt.target;
|
|
2766
3087
|
if (targetEl && typeof targetEl.closest === 'function' && targetEl.closest('table.dataTable')) {
|
|
2767
3088
|
evt.preventDefault();
|
|
@@ -2777,14 +3098,13 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2777
3098
|
const rep = ed.ace_getRep();
|
|
2778
3099
|
if (!rep || !rep.selStart) {
|
|
2779
3100
|
// log(`${dropLogPrefix} WARNING: Could not get representation or selection. Allowing default drop.`);
|
|
2780
|
-
return;
|
|
3101
|
+
return;
|
|
2781
3102
|
}
|
|
2782
3103
|
|
|
2783
3104
|
const selStart = rep.selStart;
|
|
2784
3105
|
const lineNum = selStart[0];
|
|
2785
3106
|
// log(`${dropLogPrefix} Current line number: ${lineNum}.`);
|
|
2786
3107
|
|
|
2787
|
-
// Check if we're dropping onto a table line
|
|
2788
3108
|
// log(`${dropLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
|
|
2789
3109
|
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2790
3110
|
let isTableLine = !!lineAttrString;
|
|
@@ -2802,11 +3122,9 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2802
3122
|
}
|
|
2803
3123
|
});
|
|
2804
3124
|
|
|
2805
|
-
// Also prevent dragover to ensure drop events are properly handled
|
|
2806
3125
|
$inner.on('dragover', (evt) => {
|
|
2807
3126
|
const dragLogPrefix = '[ep_data_tables:dragoverHandler]';
|
|
2808
|
-
|
|
2809
|
-
// If hovering over a table, signal not-allowed and block default
|
|
3127
|
+
|
|
2810
3128
|
const targetEl = evt.target;
|
|
2811
3129
|
if (targetEl && typeof targetEl.closest === 'function' && targetEl.closest('table.dataTable')) {
|
|
2812
3130
|
if (evt.originalEvent && evt.originalEvent.dataTransfer) {
|
|
@@ -2819,13 +3137,12 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2819
3137
|
|
|
2820
3138
|
const rep = ed.ace_getRep();
|
|
2821
3139
|
if (!rep || !rep.selStart) {
|
|
2822
|
-
return;
|
|
3140
|
+
return;
|
|
2823
3141
|
}
|
|
2824
3142
|
|
|
2825
3143
|
const selStart = rep.selStart;
|
|
2826
3144
|
const lineNum = selStart[0];
|
|
2827
3145
|
|
|
2828
|
-
// Check if we're dragging over a table line
|
|
2829
3146
|
// log(`${dragLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
|
|
2830
3147
|
let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
|
|
2831
3148
|
let isTableLine = !!lineAttrString;
|
|
@@ -2840,7 +3157,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2840
3157
|
}
|
|
2841
3158
|
});
|
|
2842
3159
|
|
|
2843
|
-
// Guard against dragenter into table areas
|
|
2844
3160
|
$inner.on('dragenter', (evt) => {
|
|
2845
3161
|
const targetEl = evt.target;
|
|
2846
3162
|
if (targetEl && typeof targetEl.closest === 'function' && targetEl.closest('table.dataTable')) {
|
|
@@ -2852,7 +3168,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2852
3168
|
}
|
|
2853
3169
|
});
|
|
2854
3170
|
|
|
2855
|
-
// *** EXISTING PASTE LISTENER ***
|
|
2856
3171
|
// log(`${callWithAceLogPrefix} Attaching paste event listener to $inner (inner iframe body).`);
|
|
2857
3172
|
$inner.on('paste', (evt) => {
|
|
2858
3173
|
const pasteLogPrefix = '[ep_data_tables:pasteHandler]';
|
|
@@ -2863,7 +3178,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2863
3178
|
if (!rep || !rep.selStart) {
|
|
2864
3179
|
// log(`${pasteLogPrefix} WARNING: Could not get representation or selection. Allowing default paste.`);
|
|
2865
3180
|
console.warn(`${pasteLogPrefix} Could not get rep or selStart.`);
|
|
2866
|
-
return;
|
|
3181
|
+
return;
|
|
2867
3182
|
}
|
|
2868
3183
|
// log(`${pasteLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
|
|
2869
3184
|
const selStart = rep.selStart;
|
|
@@ -2871,7 +3186,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2871
3186
|
const lineNum = selStart[0];
|
|
2872
3187
|
// log(`${pasteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
|
|
2873
3188
|
|
|
2874
|
-
// NEW: Check if selection spans multiple lines
|
|
2875
3189
|
if (selStart[0] !== selEnd[0]) {
|
|
2876
3190
|
// log(`${pasteLogPrefix} WARNING: Selection spans multiple lines. Preventing paste to protect table structure.`);
|
|
2877
3191
|
evt.preventDefault();
|
|
@@ -2883,7 +3197,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2883
3197
|
let tableMetadata = null;
|
|
2884
3198
|
|
|
2885
3199
|
if (!lineAttrString) {
|
|
2886
|
-
// Block-styled row? Reconstruct metadata from the DOM.
|
|
2887
3200
|
// log(`${pasteLogPrefix} No '${ATTR_TABLE_JSON}' attribute found. Checking if this is a block-styled table row via DOM reconstruction.`);
|
|
2888
3201
|
const fallbackMeta = getTableLineMetadata(lineNum, ed, docManager);
|
|
2889
3202
|
if (fallbackMeta) {
|
|
@@ -2895,7 +3208,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2895
3208
|
|
|
2896
3209
|
if (!lineAttrString) {
|
|
2897
3210
|
// log(`${pasteLogPrefix} Line ${lineNum} is NOT a table line (no '${ATTR_TABLE_JSON}' attribute found and no DOM reconstruction possible). Allowing default paste.`);
|
|
2898
|
-
return;
|
|
3211
|
+
return;
|
|
2899
3212
|
}
|
|
2900
3213
|
// log(`${pasteLogPrefix} Line ${lineNum} IS a table line. Attribute string: "${lineAttrString}".`);
|
|
2901
3214
|
|
|
@@ -2908,16 +3221,15 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2908
3221
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
|
|
2909
3222
|
// log(`${pasteLogPrefix} WARNING: Invalid or incomplete table metadata on line ${lineNum}. Allowing default paste. Metadata:`, tableMetadata);
|
|
2910
3223
|
console.warn(`${pasteLogPrefix} Invalid table metadata for line ${lineNum}.`);
|
|
2911
|
-
return;
|
|
3224
|
+
return;
|
|
2912
3225
|
}
|
|
2913
3226
|
// log(`${pasteLogPrefix} Table metadata validated successfully: tblId=${tableMetadata.tblId}, row=${tableMetadata.row}, cols=${tableMetadata.cols}.`);
|
|
2914
3227
|
} catch(e) {
|
|
2915
3228
|
console.error(`${pasteLogPrefix} ERROR parsing table metadata for line ${lineNum}:`, e);
|
|
2916
3229
|
// log(`${pasteLogPrefix} Metadata parse error. Allowing default paste. Error details:`, { message: e.message, stack: e.stack });
|
|
2917
|
-
return;
|
|
3230
|
+
return;
|
|
2918
3231
|
}
|
|
2919
3232
|
|
|
2920
|
-
// NEW: Validate selection is within cell boundaries
|
|
2921
3233
|
const lineText = rep.lines.atIndex(lineNum)?.text || '';
|
|
2922
3234
|
const cells = lineText.split(DELIMITER);
|
|
2923
3235
|
let currentOffset = 0;
|
|
@@ -2928,7 +3240,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2928
3240
|
for (let i = 0; i < cells.length; i++) {
|
|
2929
3241
|
const cellLength = cells[i]?.length ?? 0;
|
|
2930
3242
|
const cellEndColThisIteration = currentOffset + cellLength;
|
|
2931
|
-
|
|
3243
|
+
|
|
2932
3244
|
if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
|
|
2933
3245
|
targetCellIndex = i;
|
|
2934
3246
|
cellStartCol = currentOffset;
|
|
@@ -2941,7 +3253,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2941
3253
|
/* allow "…cell content + delimiter" selections */
|
|
2942
3254
|
if (targetCellIndex !== -1 &&
|
|
2943
3255
|
selEnd[1] === cellEndCol + DELIMITER.length) {
|
|
2944
|
-
selEnd[1] = cellEndCol;
|
|
3256
|
+
selEnd[1] = cellEndCol;
|
|
2945
3257
|
}
|
|
2946
3258
|
if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
|
|
2947
3259
|
// log(`${pasteLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing paste to protect table structure.`);
|
|
@@ -2953,28 +3265,26 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2953
3265
|
const clipboardData = evt.originalEvent.clipboardData || window.clipboardData;
|
|
2954
3266
|
if (!clipboardData) {
|
|
2955
3267
|
// log(`${pasteLogPrefix} WARNING: No clipboard data found. Allowing default paste.`);
|
|
2956
|
-
return;
|
|
3268
|
+
return;
|
|
2957
3269
|
}
|
|
2958
3270
|
// log(`${pasteLogPrefix} Clipboard data object obtained:`, clipboardData);
|
|
2959
3271
|
|
|
2960
|
-
// Allow default handling (so ep_hyperlinked_text plugin can process) if rich HTML is present
|
|
2961
3272
|
const types = clipboardData.types || [];
|
|
2962
3273
|
if (types.includes('text/html') && clipboardData.getData('text/html')) {
|
|
2963
3274
|
// log(`${pasteLogPrefix} Detected text/html in clipboard – deferring to other plugins and default paste.`);
|
|
2964
|
-
return;
|
|
3275
|
+
return;
|
|
2965
3276
|
}
|
|
2966
3277
|
|
|
2967
3278
|
// log(`${pasteLogPrefix} Getting 'text/plain' from clipboard.`);
|
|
2968
3279
|
const pastedTextRaw = clipboardData.getData('text/plain');
|
|
2969
3280
|
// log(`${pasteLogPrefix} Pasted text raw: "${pastedTextRaw}" (Type: ${typeof pastedTextRaw})`);
|
|
2970
3281
|
|
|
2971
|
-
// ENHANCED: More thorough sanitization of pasted content
|
|
2972
3282
|
let pastedText = pastedTextRaw
|
|
2973
|
-
.replace(/(\r\n|\n|\r)/gm, " ")
|
|
2974
|
-
.replace(new RegExp(DELIMITER, 'g'), ' ')
|
|
2975
|
-
.replace(/\t/g, " ")
|
|
2976
|
-
.replace(/\s+/g, " ")
|
|
2977
|
-
.trim();
|
|
3283
|
+
.replace(/(\r\n|\n|\r)/gm, " ")
|
|
3284
|
+
.replace(new RegExp(DELIMITER, 'g'), ' ')
|
|
3285
|
+
.replace(/\t/g, " ")
|
|
3286
|
+
.replace(/\s+/g, " ")
|
|
3287
|
+
.trim();
|
|
2978
3288
|
|
|
2979
3289
|
// log(`${pasteLogPrefix} Pasted text after sanitization: "${pastedText}"`);
|
|
2980
3290
|
|
|
@@ -2985,20 +3295,14 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
2985
3295
|
if (types && types.includes('text/html')) {
|
|
2986
3296
|
// log(`${pasteLogPrefix} Clipboard also contains HTML:`, clipboardData.getData('text/html'));
|
|
2987
3297
|
}
|
|
2988
|
-
return;
|
|
3298
|
+
return;
|
|
2989
3299
|
}
|
|
2990
3300
|
// log(`${pasteLogPrefix} Plain text obtained from clipboard: "${pastedText}". Length: ${pastedText.length}.`);
|
|
2991
3301
|
|
|
2992
|
-
// NEW: Check if paste would exceed cell boundaries
|
|
2993
3302
|
const currentCellText = cells[targetCellIndex] || '';
|
|
2994
3303
|
const selectionLength = selEnd[1] - selStart[1];
|
|
2995
3304
|
const newCellLength = currentCellText.length - selectionLength + pastedText.length;
|
|
2996
|
-
|
|
2997
|
-
// Soft safety-valve: Etherpad can technically handle very long lines but
|
|
2998
|
-
// extremely large cells slow down rendering. 8 000 chars ≈ five classic
|
|
2999
|
-
// 'Lorem Ipsum' paragraphs and feels like a reasonable upper bound while
|
|
3000
|
-
// still letting users paste substantive text. Increase/decrease as you
|
|
3001
|
-
// see fit or set to `Infinity` to remove the cap entirely.
|
|
3305
|
+
|
|
3002
3306
|
const MAX_CELL_LENGTH = 8000;
|
|
3003
3307
|
if (newCellLength > MAX_CELL_LENGTH) {
|
|
3004
3308
|
// log(`${pasteLogPrefix} WARNING: Paste would exceed maximum cell length (${newCellLength} > ${MAX_CELL_LENGTH}). Truncating paste.`);
|
|
@@ -3014,8 +3318,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3014
3318
|
|
|
3015
3319
|
// log(`${pasteLogPrefix} INTERCEPTING paste of plain text into table line ${lineNum}. PREVENTING DEFAULT browser action.`);
|
|
3016
3320
|
evt.preventDefault();
|
|
3017
|
-
// Prevent other plugins from handling the same paste event once we
|
|
3018
|
-
// have intercepted it inside a table cell.
|
|
3019
3321
|
evt.stopPropagation();
|
|
3020
3322
|
if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
|
|
3021
3323
|
|
|
@@ -3024,9 +3326,9 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3024
3326
|
ed.ace_callWithAce((aceInstance) => {
|
|
3025
3327
|
const callAceLogPrefix = `${pasteLogPrefix}[ace_callWithAceOps]`;
|
|
3026
3328
|
// log(`${callAceLogPrefix} Entered ace_callWithAce for paste operations. selStart:`, selStart, `selEnd:`, selEnd);
|
|
3027
|
-
|
|
3329
|
+
|
|
3028
3330
|
// log(`${callAceLogPrefix} Original line text from initial rep: "${rep.lines.atIndex(lineNum).text}". SelStartCol: ${selStart[1]}, SelEndCol: ${selEnd[1]}.`);
|
|
3029
|
-
|
|
3331
|
+
|
|
3030
3332
|
// log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to insert text: "${pastedText}".`);
|
|
3031
3333
|
aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, pastedText);
|
|
3032
3334
|
// log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
|
|
@@ -3034,7 +3336,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3034
3336
|
// log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
|
|
3035
3337
|
const repAfterReplace = aceInstance.ace_getRep();
|
|
3036
3338
|
// log(`${callAceLogPrefix} Fetched rep after replace for applyMeta. Line ${lineNum} text now: "${repAfterReplace.lines.atIndex(lineNum).text}"`);
|
|
3037
|
-
|
|
3339
|
+
|
|
3038
3340
|
ed.ep_data_tables_applyMeta(
|
|
3039
3341
|
lineNum,
|
|
3040
3342
|
tableMetadata.tblId,
|
|
@@ -3052,12 +3354,11 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3052
3354
|
// log(`${callAceLogPrefix} New calculated caret position: [${newCaretPos}]. Setting selection.`);
|
|
3053
3355
|
aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
|
|
3054
3356
|
// log(`${callAceLogPrefix} Selection change successful.`);
|
|
3055
|
-
|
|
3357
|
+
|
|
3056
3358
|
// log(`${callAceLogPrefix} Requesting fastIncorp(10) for sync.`);
|
|
3057
3359
|
aceInstance.ace_fastIncorp(10);
|
|
3058
3360
|
// log(`${callAceLogPrefix} fastIncorp requested.`);
|
|
3059
3361
|
|
|
3060
|
-
// Update stored click/caret info
|
|
3061
3362
|
if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
|
|
3062
3363
|
const newRelativePos = newCaretCol - cellStartCol;
|
|
3063
3364
|
editor.ep_data_tables_last_clicked = {
|
|
@@ -3081,63 +3382,57 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3081
3382
|
});
|
|
3082
3383
|
// log(`${callWithAceLogPrefix} Paste event listener attached.`);
|
|
3083
3384
|
|
|
3084
|
-
// *** NEW: Column resize listeners ***
|
|
3085
3385
|
// log(`${callWithAceLogPrefix} Attaching column resize listeners...`);
|
|
3086
|
-
|
|
3087
|
-
// Get the iframe documents for proper event delegation
|
|
3386
|
+
|
|
3088
3387
|
const $iframeOuter = $('iframe[name="ace_outer"]');
|
|
3089
3388
|
const $iframeInner = $iframeOuter.contents().find('iframe[name="ace_inner"]');
|
|
3090
3389
|
const innerDoc = $iframeInner.contents();
|
|
3091
3390
|
const outerDoc = $iframeOuter.contents();
|
|
3092
|
-
|
|
3391
|
+
|
|
3093
3392
|
// log(`${callWithAceLogPrefix} Found iframe documents: outer=${outerDoc.length}, inner=${innerDoc.length}`);
|
|
3094
|
-
|
|
3095
|
-
// Mousedown on resize handles
|
|
3393
|
+
|
|
3096
3394
|
$inner.on('mousedown', '.ep-data_tables-resize-handle', (evt) => {
|
|
3097
3395
|
const resizeLogPrefix = '[ep_data_tables:resizeMousedown]';
|
|
3098
3396
|
// log(`${resizeLogPrefix} Resize handle mousedown detected`);
|
|
3099
|
-
|
|
3100
|
-
// Only handle left mouse button clicks
|
|
3397
|
+
|
|
3101
3398
|
if (evt.button !== 0) {
|
|
3102
3399
|
// log(`${resizeLogPrefix} Ignoring non-left mouse button: ${evt.button}`);
|
|
3103
3400
|
return;
|
|
3104
3401
|
}
|
|
3105
|
-
|
|
3106
|
-
// Check if this is related to an image element to avoid conflicts
|
|
3402
|
+
|
|
3107
3403
|
const target = evt.target;
|
|
3108
3404
|
const $target = $(target);
|
|
3109
3405
|
const isImageRelated = $target.closest('.inline-image, .image-placeholder, .image-inner').length > 0;
|
|
3110
3406
|
const isImageResizeHandle = $target.hasClass('image-resize-handle') || $target.closest('.image-resize-handle').length > 0;
|
|
3111
|
-
|
|
3407
|
+
|
|
3112
3408
|
if (isImageRelated || isImageResizeHandle) {
|
|
3113
3409
|
// log(`${resizeLogPrefix} Click detected on image-related element or image resize handle, ignoring for table resize`);
|
|
3114
3410
|
return;
|
|
3115
3411
|
}
|
|
3116
|
-
|
|
3412
|
+
|
|
3117
3413
|
evt.preventDefault();
|
|
3118
3414
|
evt.stopPropagation();
|
|
3119
|
-
|
|
3415
|
+
|
|
3120
3416
|
const handle = evt.target;
|
|
3121
3417
|
const columnIndex = parseInt(handle.getAttribute('data-column'), 10);
|
|
3122
3418
|
const table = handle.closest('table.dataTable');
|
|
3123
3419
|
const lineNode = table.closest('div.ace-line');
|
|
3124
|
-
|
|
3420
|
+
|
|
3125
3421
|
// log(`${resizeLogPrefix} Parsed resize target: columnIndex=${columnIndex}, table=${!!table}, lineNode=${!!lineNode}`);
|
|
3126
|
-
|
|
3422
|
+
|
|
3127
3423
|
if (table && lineNode && !isNaN(columnIndex)) {
|
|
3128
|
-
// Get table metadata
|
|
3129
3424
|
const tblId = table.getAttribute('data-tblId');
|
|
3130
3425
|
const rep = ed.ace_getRep();
|
|
3131
|
-
|
|
3426
|
+
|
|
3132
3427
|
if (!rep || !rep.lines) {
|
|
3133
3428
|
console.error(`${resizeLogPrefix} Cannot get editor representation`);
|
|
3134
3429
|
return;
|
|
3135
3430
|
}
|
|
3136
|
-
|
|
3431
|
+
|
|
3137
3432
|
const lineNum = rep.lines.indexOfKey(lineNode.id);
|
|
3138
|
-
|
|
3433
|
+
|
|
3139
3434
|
// log(`${resizeLogPrefix} Table info: tblId=${tblId}, lineNum=${lineNum}`);
|
|
3140
|
-
|
|
3435
|
+
|
|
3141
3436
|
if (tblId && lineNum !== -1) {
|
|
3142
3437
|
try {
|
|
3143
3438
|
const lineAttrString = docManager.getAttributeOnLine(lineNum, 'tbljson');
|
|
@@ -3147,16 +3442,14 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3147
3442
|
// log(`${resizeLogPrefix} Starting resize with metadata:`, metadata);
|
|
3148
3443
|
startColumnResize(table, columnIndex, evt.clientX, metadata, lineNum);
|
|
3149
3444
|
// log(`${resizeLogPrefix} Started resize for column ${columnIndex}`);
|
|
3150
|
-
|
|
3151
|
-
// DEBUG: Verify global state is set
|
|
3445
|
+
|
|
3152
3446
|
// log(`${resizeLogPrefix} Global resize state: isResizing=${isResizing}, targetTable=${!!resizeTargetTable}, targetColumn=${resizeTargetColumn}`);
|
|
3153
3447
|
} else {
|
|
3154
3448
|
// log(`${resizeLogPrefix} Table ID mismatch: ${metadata.tblId} vs ${tblId}`);
|
|
3155
3449
|
}
|
|
3156
3450
|
} else {
|
|
3157
3451
|
// log(`${resizeLogPrefix} No table metadata found for line ${lineNum}, trying DOM reconstruction...`);
|
|
3158
|
-
|
|
3159
|
-
// Fallback: Reconstruct metadata from DOM (same logic as ace_doDatatableOptions)
|
|
3452
|
+
|
|
3160
3453
|
const rep = ed.ace_getRep();
|
|
3161
3454
|
if (rep && rep.lines) {
|
|
3162
3455
|
const lineEntry = rep.lines.atIndex(lineNum);
|
|
@@ -3168,7 +3461,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3168
3461
|
if (domTblId === tblId && domRow !== null) {
|
|
3169
3462
|
const domCells = tableInDOM.querySelectorAll('td');
|
|
3170
3463
|
if (domCells.length > 0) {
|
|
3171
|
-
// Extract column widths from DOM cells
|
|
3172
3464
|
const columnWidths = [];
|
|
3173
3465
|
domCells.forEach(cell => {
|
|
3174
3466
|
const style = cell.getAttribute('style') || '';
|
|
@@ -3176,12 +3468,10 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3176
3468
|
if (widthMatch) {
|
|
3177
3469
|
columnWidths.push(parseFloat(widthMatch[1]));
|
|
3178
3470
|
} else {
|
|
3179
|
-
// Fallback to equal distribution if no width found
|
|
3180
3471
|
columnWidths.push(100 / domCells.length);
|
|
3181
3472
|
}
|
|
3182
3473
|
});
|
|
3183
|
-
|
|
3184
|
-
// Reconstruct metadata from DOM with preserved column widths
|
|
3474
|
+
|
|
3185
3475
|
const reconstructedMetadata = {
|
|
3186
3476
|
tblId: domTblId,
|
|
3187
3477
|
row: parseInt(domRow, 10),
|
|
@@ -3189,11 +3479,10 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3189
3479
|
columnWidths: columnWidths
|
|
3190
3480
|
};
|
|
3191
3481
|
// log(`${resizeLogPrefix} Reconstructed metadata from DOM:`, reconstructedMetadata);
|
|
3192
|
-
|
|
3482
|
+
|
|
3193
3483
|
startColumnResize(table, columnIndex, evt.clientX, reconstructedMetadata, lineNum);
|
|
3194
3484
|
// log(`${resizeLogPrefix} Started resize for column ${columnIndex} using reconstructed metadata`);
|
|
3195
|
-
|
|
3196
|
-
// DEBUG: Verify global state is set
|
|
3485
|
+
|
|
3197
3486
|
// log(`${resizeLogPrefix} Global resize state: isResizing=${isResizing}, targetTable=${!!resizeTargetTable}, targetColumn=${resizeTargetColumn}`);
|
|
3198
3487
|
} else {
|
|
3199
3488
|
// log(`${resizeLogPrefix} DOM table found but no cells detected`);
|
|
@@ -3221,30 +3510,26 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3221
3510
|
// log(`${resizeLogPrefix} Invalid resize target:`, { table: !!table, lineNode: !!lineNode, columnIndex });
|
|
3222
3511
|
}
|
|
3223
3512
|
});
|
|
3224
|
-
|
|
3225
|
-
// Enhanced mousemove and mouseup handlers - attach to multiple contexts for better coverage
|
|
3513
|
+
|
|
3226
3514
|
const setupGlobalHandlers = () => {
|
|
3227
3515
|
const mouseupLogPrefix = '[ep_data_tables:resizeMouseup]';
|
|
3228
3516
|
const mousemoveLogPrefix = '[ep_data_tables:resizeMousemove]';
|
|
3229
|
-
|
|
3230
|
-
// Mousemove handler
|
|
3517
|
+
|
|
3231
3518
|
const handleMousemove = (evt) => {
|
|
3232
3519
|
if (isResizing) {
|
|
3233
3520
|
evt.preventDefault();
|
|
3234
3521
|
updateColumnResize(evt.clientX);
|
|
3235
3522
|
}
|
|
3236
3523
|
};
|
|
3237
|
-
|
|
3238
|
-
// Mouseup handler with enhanced debugging
|
|
3524
|
+
|
|
3239
3525
|
const handleMouseup = (evt) => {
|
|
3240
3526
|
// log(`${mouseupLogPrefix} Mouseup detected on ${evt.target.tagName || 'unknown'}. isResizing: ${isResizing}`);
|
|
3241
|
-
|
|
3527
|
+
|
|
3242
3528
|
if (isResizing) {
|
|
3243
3529
|
// log(`${mouseupLogPrefix} Processing resize completion...`);
|
|
3244
3530
|
evt.preventDefault();
|
|
3245
3531
|
evt.stopPropagation();
|
|
3246
|
-
|
|
3247
|
-
// Add a small delay to ensure all DOM updates are complete
|
|
3532
|
+
|
|
3248
3533
|
setTimeout(() => {
|
|
3249
3534
|
// log(`${mouseupLogPrefix} Executing finishColumnResize after delay...`);
|
|
3250
3535
|
finishColumnResize(ed, docManager);
|
|
@@ -3254,59 +3539,50 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3254
3539
|
// log(`${mouseupLogPrefix} Not in resize mode, ignoring mouseup.`);
|
|
3255
3540
|
}
|
|
3256
3541
|
};
|
|
3257
|
-
|
|
3258
|
-
// Attach to multiple contexts to ensure we catch the event
|
|
3542
|
+
|
|
3259
3543
|
// log(`${callWithAceLogPrefix} Attaching global mousemove/mouseup handlers to multiple contexts...`);
|
|
3260
|
-
|
|
3261
|
-
// 1. Main document (outside iframes)
|
|
3544
|
+
|
|
3262
3545
|
$(document).on('mousemove', handleMousemove);
|
|
3263
3546
|
$(document).on('mouseup', handleMouseup);
|
|
3264
3547
|
// log(`${callWithAceLogPrefix} Attached to main document`);
|
|
3265
|
-
|
|
3266
|
-
// 2. Outer iframe document
|
|
3548
|
+
|
|
3267
3549
|
if (outerDoc.length > 0) {
|
|
3268
3550
|
outerDoc.on('mousemove', handleMousemove);
|
|
3269
3551
|
outerDoc.on('mouseup', handleMouseup);
|
|
3270
3552
|
// log(`${callWithAceLogPrefix} Attached to outer iframe document`);
|
|
3271
3553
|
}
|
|
3272
|
-
|
|
3273
|
-
// 3. Inner iframe document
|
|
3554
|
+
|
|
3274
3555
|
if (innerDoc.length > 0) {
|
|
3275
3556
|
innerDoc.on('mousemove', handleMousemove);
|
|
3276
3557
|
innerDoc.on('mouseup', handleMouseup);
|
|
3277
3558
|
// log(`${callWithAceLogPrefix} Attached to inner iframe document`);
|
|
3278
3559
|
}
|
|
3279
|
-
|
|
3280
|
-
// 4. Inner iframe body (the editing area)
|
|
3560
|
+
|
|
3281
3561
|
$inner.on('mousemove', handleMousemove);
|
|
3282
3562
|
$inner.on('mouseup', handleMouseup);
|
|
3283
3563
|
// log(`${callWithAceLogPrefix} Attached to inner iframe body`);
|
|
3284
|
-
|
|
3285
|
-
// 5. Add a failsafe - listen for any mouse events during resize
|
|
3564
|
+
|
|
3286
3565
|
const failsafeMouseup = (evt) => {
|
|
3287
3566
|
if (isResizing) {
|
|
3288
3567
|
// log(`${mouseupLogPrefix} FAILSAFE: Detected mouse event during resize: ${evt.type}`);
|
|
3289
3568
|
if (evt.type === 'mouseup' || evt.type === 'mousedown' || evt.type === 'click') {
|
|
3290
3569
|
// log(`${mouseupLogPrefix} FAILSAFE: Triggering resize completion due to ${evt.type}`);
|
|
3291
3570
|
setTimeout(() => {
|
|
3292
|
-
if (isResizing) {
|
|
3571
|
+
if (isResizing) {
|
|
3293
3572
|
finishColumnResize(ed, docManager);
|
|
3294
3573
|
}
|
|
3295
3574
|
}, 100);
|
|
3296
3575
|
}
|
|
3297
3576
|
}
|
|
3298
3577
|
};
|
|
3299
|
-
|
|
3300
|
-
// Attach failsafe to main document with capture=true to catch events early
|
|
3578
|
+
|
|
3301
3579
|
document.addEventListener('mouseup', failsafeMouseup, true);
|
|
3302
3580
|
document.addEventListener('mousedown', failsafeMouseup, true);
|
|
3303
3581
|
document.addEventListener('click', failsafeMouseup, true);
|
|
3304
3582
|
// log(`${callWithAceLogPrefix} Attached failsafe event handlers`);
|
|
3305
|
-
|
|
3306
|
-
// *** DRAG PREVENTION FOR TABLE ELEMENTS ***
|
|
3583
|
+
|
|
3307
3584
|
const preventTableDrag = (evt) => {
|
|
3308
3585
|
const target = evt.target;
|
|
3309
|
-
// Block ANY drag gesture that originates within a table (including spans/text inside cells)
|
|
3310
3586
|
const inTable = target && typeof target.closest === 'function' && target.closest('table.dataTable');
|
|
3311
3587
|
if (inTable) {
|
|
3312
3588
|
// log('[ep_data_tables:dragPrevention] Preventing drag operation originating from inside table');
|
|
@@ -3318,14 +3594,12 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3318
3594
|
return false;
|
|
3319
3595
|
}
|
|
3320
3596
|
};
|
|
3321
|
-
|
|
3322
|
-
// Add drag event listeners to prevent table dragging
|
|
3597
|
+
|
|
3323
3598
|
$inner.on('dragstart', preventTableDrag);
|
|
3324
3599
|
$inner.on('drag', preventTableDrag);
|
|
3325
3600
|
$inner.on('dragend', preventTableDrag);
|
|
3326
3601
|
// log(`${callWithAceLogPrefix} Attached drag prevention handlers to inner body`);
|
|
3327
3602
|
|
|
3328
|
-
// Attach drag prevention broadly to cover iframe boundaries and the host document
|
|
3329
3603
|
if (innerDoc.length > 0) {
|
|
3330
3604
|
innerDoc.on('dragstart', preventTableDrag);
|
|
3331
3605
|
innerDoc.on('drag', preventTableDrag);
|
|
@@ -3340,32 +3614,27 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3340
3614
|
$(document).on('drag', preventTableDrag);
|
|
3341
3615
|
$(document).on('dragend', preventTableDrag);
|
|
3342
3616
|
};
|
|
3343
|
-
|
|
3344
|
-
// Setup the global handlers
|
|
3617
|
+
|
|
3345
3618
|
setupGlobalHandlers();
|
|
3346
|
-
|
|
3619
|
+
|
|
3347
3620
|
// log(`${callWithAceLogPrefix} Column resize listeners attached successfully.`);
|
|
3348
3621
|
|
|
3349
3622
|
}, 'tablePasteAndResizeListeners', true);
|
|
3350
3623
|
// log(`${logPrefix} ace_callWithAce for listeners setup completed.`);
|
|
3351
3624
|
|
|
3352
|
-
// Helper function to apply the metadata attribute to a line
|
|
3353
3625
|
function applyTableLineMetadataAttribute (lineNum, tblId, rowIndex, numCols, rep, editorInfo, attributeString = null, documentAttributeManager = null) {
|
|
3354
3626
|
const funcName = 'applyTableLineMetadataAttribute';
|
|
3355
3627
|
// log(`${logPrefix}:${funcName}: START - Applying METADATA attribute to line ${lineNum}`, {tblId, rowIndex, numCols});
|
|
3356
3628
|
|
|
3357
3629
|
let finalMetadata;
|
|
3358
|
-
|
|
3359
|
-
// If attributeString is provided, check if it contains columnWidths
|
|
3630
|
+
|
|
3360
3631
|
if (attributeString) {
|
|
3361
3632
|
try {
|
|
3362
3633
|
const providedMetadata = JSON.parse(attributeString);
|
|
3363
3634
|
if (providedMetadata.columnWidths && Array.isArray(providedMetadata.columnWidths) && providedMetadata.columnWidths.length === numCols) {
|
|
3364
|
-
// Already has valid columnWidths, use as-is
|
|
3365
3635
|
finalMetadata = providedMetadata;
|
|
3366
3636
|
// log(`${logPrefix}:${funcName}: Using provided metadata with existing columnWidths`);
|
|
3367
3637
|
} else {
|
|
3368
|
-
// Has metadata but missing/invalid columnWidths, extract from DOM
|
|
3369
3638
|
finalMetadata = providedMetadata;
|
|
3370
3639
|
// log(`${logPrefix}:${funcName}: Provided metadata missing columnWidths, attempting DOM extraction`);
|
|
3371
3640
|
}
|
|
@@ -3374,12 +3643,10 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3374
3643
|
finalMetadata = null;
|
|
3375
3644
|
}
|
|
3376
3645
|
}
|
|
3377
|
-
|
|
3378
|
-
// If we don't have complete metadata or need to extract columnWidths
|
|
3646
|
+
|
|
3379
3647
|
if (!finalMetadata || !finalMetadata.columnWidths) {
|
|
3380
3648
|
let columnWidths = null;
|
|
3381
|
-
|
|
3382
|
-
// Try to extract existing column widths from DOM if available
|
|
3649
|
+
|
|
3383
3650
|
try {
|
|
3384
3651
|
const lineEntry = rep.lines.atIndex(lineNum);
|
|
3385
3652
|
if (lineEntry && lineEntry.lineNode) {
|
|
@@ -3389,7 +3656,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3389
3656
|
if (domTblId === tblId) {
|
|
3390
3657
|
const domCells = tableInDOM.querySelectorAll('td');
|
|
3391
3658
|
if (domCells.length === numCols) {
|
|
3392
|
-
// Extract column widths from DOM cells
|
|
3393
3659
|
columnWidths = [];
|
|
3394
3660
|
domCells.forEach(cell => {
|
|
3395
3661
|
const style = cell.getAttribute('style') || '';
|
|
@@ -3397,7 +3663,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3397
3663
|
if (widthMatch) {
|
|
3398
3664
|
columnWidths.push(parseFloat(widthMatch[1]));
|
|
3399
3665
|
} else {
|
|
3400
|
-
// Fallback to equal distribution if no width found
|
|
3401
3666
|
columnWidths.push(100 / numCols);
|
|
3402
3667
|
}
|
|
3403
3668
|
});
|
|
@@ -3409,15 +3674,13 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3409
3674
|
} catch (e) {
|
|
3410
3675
|
// log(`${logPrefix}:${funcName}: Error extracting column widths from DOM:`, e);
|
|
3411
3676
|
}
|
|
3412
|
-
|
|
3413
|
-
// Build final metadata
|
|
3677
|
+
|
|
3414
3678
|
finalMetadata = finalMetadata || {
|
|
3415
3679
|
tblId: tblId,
|
|
3416
3680
|
row: rowIndex,
|
|
3417
3681
|
cols: numCols
|
|
3418
3682
|
};
|
|
3419
|
-
|
|
3420
|
-
// Add column widths if we found them
|
|
3683
|
+
|
|
3421
3684
|
if (columnWidths && columnWidths.length === numCols) {
|
|
3422
3685
|
finalMetadata.columnWidths = columnWidths;
|
|
3423
3686
|
}
|
|
@@ -3425,9 +3688,8 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3425
3688
|
|
|
3426
3689
|
const finalAttributeString = JSON.stringify(finalMetadata);
|
|
3427
3690
|
// log(`${logPrefix}:${funcName}: Final metadata attribute string: ${finalAttributeString}`);
|
|
3428
|
-
|
|
3691
|
+
|
|
3429
3692
|
try {
|
|
3430
|
-
// Get current line info
|
|
3431
3693
|
const lineEntry = rep.lines.atIndex(lineNum);
|
|
3432
3694
|
if (!lineEntry) {
|
|
3433
3695
|
// log(`${logPrefix}:${funcName}: ERROR - Could not find line entry for line number ${lineNum}`);
|
|
@@ -3436,15 +3698,14 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3436
3698
|
const lineLength = Math.max(1, lineEntry.text.length);
|
|
3437
3699
|
// log(`${logPrefix}:${funcName}: Line ${lineNum} text length: ${lineLength}`);
|
|
3438
3700
|
|
|
3439
|
-
// Simple attribute application - just add the tbljson attribute
|
|
3440
3701
|
const attributes = [[ATTR_TABLE_JSON, finalAttributeString]];
|
|
3441
3702
|
const start = [lineNum, 0];
|
|
3442
3703
|
const end = [lineNum, lineLength];
|
|
3443
|
-
|
|
3704
|
+
|
|
3444
3705
|
// log(`${logPrefix}:${funcName}: Applying tbljson attribute to range [${start}]-[${end}]`);
|
|
3445
3706
|
editorInfo.ace_performDocumentApplyAttributesToRange(start, end, attributes);
|
|
3446
3707
|
// log(`${logPrefix}:${funcName}: Successfully applied tbljson attribute to line ${lineNum}`);
|
|
3447
|
-
|
|
3708
|
+
|
|
3448
3709
|
} catch(e) {
|
|
3449
3710
|
console.error(`[ep_data_tables] ${logPrefix}:${funcName}: Error applying metadata attribute on line ${lineNum}:`, e);
|
|
3450
3711
|
}
|
|
@@ -3457,16 +3718,14 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3457
3718
|
rows = Math.max(1, rows); cols = Math.max(1, cols);
|
|
3458
3719
|
// log(`${funcName}: Ensuring minimum 1 row, 1 col.`);
|
|
3459
3720
|
|
|
3460
|
-
// --- Phase 1: Prepare Data ---
|
|
3461
3721
|
const tblId = rand();
|
|
3462
3722
|
// log(`${funcName}: Generated table ID: ${tblId}`);
|
|
3463
|
-
const initialCellContent = ' ';
|
|
3723
|
+
const initialCellContent = ' ';
|
|
3464
3724
|
const lineTxt = Array.from({ length: cols }).fill(initialCellContent).join(DELIMITER);
|
|
3465
3725
|
// log(`${funcName}: Constructed initial line text for ${cols} cols: "${lineTxt}"`);
|
|
3466
3726
|
const block = Array.from({ length: rows }).fill(lineTxt).join('\n') + '\n';
|
|
3467
3727
|
// log(`${funcName}: Constructed block for ${rows} rows:\n${block}`);
|
|
3468
3728
|
|
|
3469
|
-
// Get current selection BEFORE making changes using ace_getRep()
|
|
3470
3729
|
// log(`${funcName}: Getting current representation and selection...`);
|
|
3471
3730
|
const currentRepInitial = ed.ace_getRep();
|
|
3472
3731
|
if (!currentRepInitial || !currentRepInitial.selStart || !currentRepInitial.selEnd) {
|
|
@@ -3476,25 +3735,21 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3476
3735
|
}
|
|
3477
3736
|
const start = currentRepInitial.selStart;
|
|
3478
3737
|
const end = currentRepInitial.selEnd;
|
|
3479
|
-
const initialStartLine = start[0];
|
|
3738
|
+
const initialStartLine = start[0];
|
|
3480
3739
|
// log(`${funcName}: Current selection from initial rep:`, { start, end });
|
|
3481
3740
|
|
|
3482
|
-
// --- Phase 2: Insert Text Block ---
|
|
3483
3741
|
// log(`${funcName}: Phase 2 - Inserting text block...`);
|
|
3484
3742
|
ed.ace_performDocumentReplaceRange(start, end, block);
|
|
3485
3743
|
// log(`${funcName}: Inserted block of delimited text lines.`);
|
|
3486
3744
|
// log(`${funcName}: Requesting text sync (ace_fastIncorp 20)...`);
|
|
3487
|
-
ed.ace_fastIncorp(20);
|
|
3745
|
+
ed.ace_fastIncorp(20);
|
|
3488
3746
|
// log(`${funcName}: Text sync requested.`);
|
|
3489
3747
|
|
|
3490
|
-
// --- Phase 3: Apply Metadata Attributes ---
|
|
3491
3748
|
// log(`${funcName}: Phase 3 - Applying metadata attributes to ${rows} inserted lines...`);
|
|
3492
|
-
|
|
3493
|
-
const currentRep = ed.ace_getRep(); // Get potentially updated rep
|
|
3749
|
+
const currentRep = ed.ace_getRep();
|
|
3494
3750
|
if (!currentRep || !currentRep.lines) {
|
|
3495
3751
|
console.error(`[ep_data_tables] ${funcName}: Could not get updated rep after text insertion. Cannot apply attributes reliably.`);
|
|
3496
3752
|
// log(`${funcName}: END - Error getting updated rep`);
|
|
3497
|
-
// Maybe attempt to continue without rep? Risky.
|
|
3498
3753
|
return;
|
|
3499
3754
|
}
|
|
3500
3755
|
// log(`${funcName}: Fetched updated rep for attribute application.`);
|
|
@@ -3512,10 +3767,9 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3512
3767
|
const cells = lineText.split(DELIMITER);
|
|
3513
3768
|
let offset = 0;
|
|
3514
3769
|
|
|
3515
|
-
// Apply cell-specific attributes to trigger authorship span splitting
|
|
3516
3770
|
for (let c = 0; c < cols; c++) {
|
|
3517
3771
|
const cellContent = (c < cells.length) ? cells[c] || '' : '';
|
|
3518
|
-
if (cellContent.length > 0) {
|
|
3772
|
+
if (cellContent.length > 0) {
|
|
3519
3773
|
const cellStart = [lineNumToApply, offset];
|
|
3520
3774
|
const cellEnd = [lineNumToApply, offset + cellContent.length];
|
|
3521
3775
|
// log(`${funcName}: Applying ${ATTR_CELL} attribute to Line ${lineNumToApply} Col ${c} Range ${offset}-${offset + cellContent.length}`);
|
|
@@ -3527,18 +3781,15 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3527
3781
|
}
|
|
3528
3782
|
}
|
|
3529
3783
|
|
|
3530
|
-
// Call the module-level helper, passing necessary context (currentRep, ed)
|
|
3531
|
-
// Note: documentAttributeManager not available in this context for new table creation
|
|
3532
3784
|
applyTableLineMetadataAttribute(lineNumToApply, tblId, r, cols, currentRep, ed, null, null);
|
|
3533
3785
|
}
|
|
3534
3786
|
// log(`${funcName}: Finished applying metadata attributes.`);
|
|
3535
3787
|
// log(`${funcName}: Requesting attribute sync (ace_fastIncorp 20)...`);
|
|
3536
|
-
ed.ace_fastIncorp(20);
|
|
3788
|
+
ed.ace_fastIncorp(20);
|
|
3537
3789
|
// log(`${funcName}: Attribute sync requested.`);
|
|
3538
3790
|
|
|
3539
|
-
// --- Phase 4: Set Caret Position ---
|
|
3540
3791
|
// log(`${funcName}: Phase 4 - Setting final caret position...`);
|
|
3541
|
-
const finalCaretLine = initialStartLine + rows;
|
|
3792
|
+
const finalCaretLine = initialStartLine + rows;
|
|
3542
3793
|
const finalCaretPos = [finalCaretLine, 0];
|
|
3543
3794
|
// log(`${funcName}: Target caret position:`, finalCaretPos);
|
|
3544
3795
|
try {
|
|
@@ -3555,50 +3806,44 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3555
3806
|
ed.ace_doDatatableOptions = (action) => {
|
|
3556
3807
|
const funcName = 'ace_doDatatableOptions';
|
|
3557
3808
|
// log(`${funcName}: START - Processing action: ${action}`);
|
|
3558
|
-
|
|
3559
|
-
// Get the last clicked cell info to determine which table to operate on
|
|
3809
|
+
|
|
3560
3810
|
const editor = ed.ep_data_tables_editor;
|
|
3561
3811
|
if (!editor) {
|
|
3562
3812
|
console.error(`[ep_data_tables] ${funcName}: Could not get editor reference.`);
|
|
3563
3813
|
return;
|
|
3564
3814
|
}
|
|
3565
|
-
|
|
3815
|
+
|
|
3566
3816
|
const lastClick = editor.ep_data_tables_last_clicked;
|
|
3567
3817
|
if (!lastClick || !lastClick.tblId) {
|
|
3568
3818
|
// log(`${funcName}: No table selected. Please click on a table cell first.`);
|
|
3569
3819
|
console.warn('[ep_data_tables] No table selected. Please click on a table cell first.');
|
|
3570
3820
|
return;
|
|
3571
3821
|
}
|
|
3572
|
-
|
|
3822
|
+
|
|
3573
3823
|
// log(`${funcName}: Operating on table ${lastClick.tblId}, clicked line ${lastClick.lineNum}, cell ${lastClick.cellIndex}`);
|
|
3574
|
-
|
|
3824
|
+
|
|
3575
3825
|
try {
|
|
3576
|
-
// Get current representation and document manager
|
|
3577
3826
|
const currentRep = ed.ace_getRep();
|
|
3578
3827
|
if (!currentRep || !currentRep.lines) {
|
|
3579
3828
|
console.error(`[ep_data_tables] ${funcName}: Could not get current representation.`);
|
|
3580
3829
|
return;
|
|
3581
3830
|
}
|
|
3582
|
-
|
|
3583
|
-
// Use the stored documentAttributeManager reference
|
|
3831
|
+
|
|
3584
3832
|
const docManager = ed.ep_data_tables_docManager;
|
|
3585
3833
|
if (!docManager) {
|
|
3586
3834
|
console.error(`[ep_data_tables] ${funcName}: Could not get document attribute manager from stored reference.`);
|
|
3587
3835
|
return;
|
|
3588
3836
|
}
|
|
3589
|
-
|
|
3837
|
+
|
|
3590
3838
|
// log(`${funcName}: Successfully obtained documentAttributeManager from stored reference.`);
|
|
3591
|
-
|
|
3592
|
-
// Find all lines that belong to this table
|
|
3839
|
+
|
|
3593
3840
|
const tableLines = [];
|
|
3594
3841
|
const totalLines = currentRep.lines.length();
|
|
3595
|
-
|
|
3842
|
+
|
|
3596
3843
|
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
|
|
3597
3844
|
try {
|
|
3598
|
-
// Use the same robust approach as acePostWriteDomLineHTML to find nested tables
|
|
3599
3845
|
let lineAttrString = docManager.getAttributeOnLine(lineIndex, ATTR_TABLE_JSON);
|
|
3600
|
-
|
|
3601
|
-
// If no attribute found directly, check if there's a table in the DOM even though attribute is missing
|
|
3846
|
+
|
|
3602
3847
|
if (!lineAttrString) {
|
|
3603
3848
|
const lineEntry = currentRep.lines.atIndex(lineIndex);
|
|
3604
3849
|
if (lineEntry && lineEntry.lineNode) {
|
|
@@ -3609,7 +3854,6 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3609
3854
|
if (domTblId && domRow !== null) {
|
|
3610
3855
|
const domCells = tableInDOM.querySelectorAll('td');
|
|
3611
3856
|
if (domCells.length > 0) {
|
|
3612
|
-
// Reconstruct metadata from DOM
|
|
3613
3857
|
const reconstructedMetadata = {
|
|
3614
3858
|
tblId: domTblId,
|
|
3615
3859
|
row: parseInt(domRow, 10),
|
|
@@ -3622,7 +3866,7 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3622
3866
|
}
|
|
3623
3867
|
}
|
|
3624
3868
|
}
|
|
3625
|
-
|
|
3869
|
+
|
|
3626
3870
|
if (lineAttrString) {
|
|
3627
3871
|
const lineMetadata = JSON.parse(lineAttrString);
|
|
3628
3872
|
if (lineMetadata.tblId === lastClick.tblId) {
|
|
@@ -3642,27 +3886,22 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3642
3886
|
continue;
|
|
3643
3887
|
}
|
|
3644
3888
|
}
|
|
3645
|
-
|
|
3889
|
+
|
|
3646
3890
|
if (tableLines.length === 0) {
|
|
3647
3891
|
// log(`${funcName}: No table lines found for table ${lastClick.tblId}`);
|
|
3648
3892
|
return;
|
|
3649
3893
|
}
|
|
3650
|
-
|
|
3651
|
-
// Sort by row number to ensure correct order
|
|
3894
|
+
|
|
3652
3895
|
tableLines.sort((a, b) => a.row - b.row);
|
|
3653
3896
|
// log(`${funcName}: Found ${tableLines.length} table lines`);
|
|
3654
|
-
|
|
3655
|
-
// Determine table dimensions and target indices with robust matching
|
|
3897
|
+
|
|
3656
3898
|
const numRows = tableLines.length;
|
|
3657
3899
|
const numCols = tableLines[0].cols;
|
|
3658
|
-
|
|
3659
|
-
// More robust way to find the target row - match by both line number AND row metadata
|
|
3900
|
+
|
|
3660
3901
|
let targetRowIndex = -1;
|
|
3661
|
-
|
|
3662
|
-
// First try to match by line number
|
|
3902
|
+
|
|
3663
3903
|
targetRowIndex = tableLines.findIndex(line => line.lineIndex === lastClick.lineNum);
|
|
3664
|
-
|
|
3665
|
-
// If that fails, try to match by finding the row that contains the clicked table
|
|
3904
|
+
|
|
3666
3905
|
if (targetRowIndex === -1) {
|
|
3667
3906
|
// log(`${funcName}: Direct line number match failed, searching by DOM structure...`);
|
|
3668
3907
|
const clickedLineEntry = currentRep.lines.atIndex(lastClick.lineNum);
|
|
@@ -3678,46 +3917,43 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3678
3917
|
}
|
|
3679
3918
|
}
|
|
3680
3919
|
}
|
|
3681
|
-
|
|
3682
|
-
// If still not found, default to first row but log the issue
|
|
3920
|
+
|
|
3683
3921
|
if (targetRowIndex === -1) {
|
|
3684
3922
|
// log(`${funcName}: Warning: Could not find target row, defaulting to row 0`);
|
|
3685
3923
|
targetRowIndex = 0;
|
|
3686
3924
|
}
|
|
3687
|
-
|
|
3925
|
+
|
|
3688
3926
|
const targetColIndex = lastClick.cellIndex || 0;
|
|
3689
|
-
|
|
3927
|
+
|
|
3690
3928
|
// log(`${funcName}: Table dimensions: ${numRows} rows x ${numCols} cols. Target: row ${targetRowIndex}, col ${targetColIndex}`);
|
|
3691
|
-
|
|
3692
|
-
// Perform table operations with both text and metadata updates
|
|
3929
|
+
|
|
3693
3930
|
let newNumCols = numCols;
|
|
3694
3931
|
let success = false;
|
|
3695
|
-
|
|
3932
|
+
|
|
3696
3933
|
switch (action) {
|
|
3697
|
-
case 'addTblRowA':
|
|
3934
|
+
case 'addTblRowA':
|
|
3698
3935
|
// log(`${funcName}: Inserting row above row ${targetRowIndex}`);
|
|
3699
3936
|
success = addTableRowAboveWithText(tableLines, targetRowIndex, numCols, lastClick.tblId, ed, docManager);
|
|
3700
3937
|
break;
|
|
3701
|
-
|
|
3702
|
-
case 'addTblRowB':
|
|
3938
|
+
|
|
3939
|
+
case 'addTblRowB':
|
|
3703
3940
|
// log(`${funcName}: Inserting row below row ${targetRowIndex}`);
|
|
3704
3941
|
success = addTableRowBelowWithText(tableLines, targetRowIndex, numCols, lastClick.tblId, ed, docManager);
|
|
3705
3942
|
break;
|
|
3706
|
-
|
|
3707
|
-
case 'addTblColL':
|
|
3943
|
+
|
|
3944
|
+
case 'addTblColL':
|
|
3708
3945
|
// log(`${funcName}: Inserting column left of column ${targetColIndex}`);
|
|
3709
3946
|
newNumCols = numCols + 1;
|
|
3710
3947
|
success = addTableColumnLeftWithText(tableLines, targetColIndex, ed, docManager);
|
|
3711
3948
|
break;
|
|
3712
|
-
|
|
3713
|
-
case 'addTblColR':
|
|
3949
|
+
|
|
3950
|
+
case 'addTblColR':
|
|
3714
3951
|
// log(`${funcName}: Inserting column right of column ${targetColIndex}`);
|
|
3715
3952
|
newNumCols = numCols + 1;
|
|
3716
3953
|
success = addTableColumnRightWithText(tableLines, targetColIndex, ed, docManager);
|
|
3717
3954
|
break;
|
|
3718
|
-
|
|
3719
|
-
case 'delTblRow':
|
|
3720
|
-
// Show confirmation prompt for row deletion
|
|
3955
|
+
|
|
3956
|
+
case 'delTblRow':
|
|
3721
3957
|
const rowConfirmMessage = `Are you sure you want to delete Row ${targetRowIndex + 1} and all content within?`;
|
|
3722
3958
|
if (!confirm(rowConfirmMessage)) {
|
|
3723
3959
|
// log(`${funcName}: Row deletion cancelled by user`);
|
|
@@ -3726,9 +3962,8 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3726
3962
|
// log(`${funcName}: Deleting row ${targetRowIndex}`);
|
|
3727
3963
|
success = deleteTableRowWithText(tableLines, targetRowIndex, ed, docManager);
|
|
3728
3964
|
break;
|
|
3729
|
-
|
|
3730
|
-
case 'delTblCol':
|
|
3731
|
-
// Show confirmation prompt for column deletion
|
|
3965
|
+
|
|
3966
|
+
case 'delTblCol':
|
|
3732
3967
|
const colConfirmMessage = `Are you sure you want to delete Column ${targetColIndex + 1} and all content within?`;
|
|
3733
3968
|
if (!confirm(colConfirmMessage)) {
|
|
3734
3969
|
// log(`${funcName}: Column deletion cancelled by user`);
|
|
@@ -3738,36 +3973,33 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3738
3973
|
newNumCols = numCols - 1;
|
|
3739
3974
|
success = deleteTableColumnWithText(tableLines, targetColIndex, ed, docManager);
|
|
3740
3975
|
break;
|
|
3741
|
-
|
|
3976
|
+
|
|
3742
3977
|
default:
|
|
3743
3978
|
// log(`${funcName}: Unknown action: ${action}`);
|
|
3744
3979
|
return;
|
|
3745
3980
|
}
|
|
3746
|
-
|
|
3981
|
+
|
|
3747
3982
|
if (!success) {
|
|
3748
3983
|
console.error(`[ep_data_tables] ${funcName}: Table operation failed for action: ${action}`);
|
|
3749
3984
|
return;
|
|
3750
3985
|
}
|
|
3751
|
-
|
|
3986
|
+
|
|
3752
3987
|
// log(`${funcName}: Table operation completed successfully with text and metadata synchronization`);
|
|
3753
|
-
|
|
3988
|
+
|
|
3754
3989
|
} catch (error) {
|
|
3755
3990
|
console.error(`[ep_data_tables] ${funcName}: Error during table operation:`, error);
|
|
3756
3991
|
// log(`${funcName}: Error details:`, { message: error.message, stack: error.stack });
|
|
3757
3992
|
}
|
|
3758
3993
|
};
|
|
3759
3994
|
|
|
3760
|
-
// Helper functions for table operations with text updates
|
|
3761
3995
|
function addTableRowAboveWithText(tableLines, targetRowIndex, numCols, tblId, editorInfo, docManager) {
|
|
3762
3996
|
try {
|
|
3763
3997
|
const targetLine = tableLines[targetRowIndex];
|
|
3764
3998
|
const newLineText = Array.from({ length: numCols }).fill(' ').join(DELIMITER);
|
|
3765
3999
|
const insertLineIndex = targetLine.lineIndex;
|
|
3766
|
-
|
|
3767
|
-
// Insert new line in text
|
|
4000
|
+
|
|
3768
4001
|
editorInfo.ace_performDocumentReplaceRange([insertLineIndex, 0], [insertLineIndex, 0], newLineText + '\n');
|
|
3769
|
-
|
|
3770
|
-
// Apply cell-specific attributes to the new row for authorship
|
|
4002
|
+
|
|
3771
4003
|
const rep = editorInfo.ace_getRep();
|
|
3772
4004
|
const cells = newLineText.split(DELIMITER);
|
|
3773
4005
|
let offset = 0;
|
|
@@ -3783,14 +4015,12 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3783
4015
|
offset += DELIMITER.length;
|
|
3784
4016
|
}
|
|
3785
4017
|
}
|
|
3786
|
-
|
|
3787
|
-
// Preserve column widths from existing metadata or extract from DOM
|
|
4018
|
+
|
|
3788
4019
|
let columnWidths = targetLine.metadata.columnWidths;
|
|
3789
4020
|
if (!columnWidths) {
|
|
3790
|
-
// Extract from DOM for block-styled rows
|
|
3791
4021
|
try {
|
|
3792
4022
|
const rep = editorInfo.ace_getRep();
|
|
3793
|
-
const lineEntry = rep.lines.atIndex(targetLine.lineIndex + 1);
|
|
4023
|
+
const lineEntry = rep.lines.atIndex(targetLine.lineIndex + 1);
|
|
3794
4024
|
if (lineEntry && lineEntry.lineNode) {
|
|
3795
4025
|
const tableInDOM = lineEntry.lineNode.querySelector(`table.dataTable[data-tblId="${tblId}"]`);
|
|
3796
4026
|
if (tableInDOM) {
|
|
@@ -3814,20 +4044,18 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3814
4044
|
console.error('[ep_data_tables] addTableRowAbove: Error extracting column widths from DOM:', e);
|
|
3815
4045
|
}
|
|
3816
4046
|
}
|
|
3817
|
-
|
|
3818
|
-
// Update metadata for all subsequent rows
|
|
4047
|
+
|
|
3819
4048
|
for (let i = targetRowIndex; i < tableLines.length; i++) {
|
|
3820
|
-
const lineToUpdate = tableLines[i].lineIndex + 1;
|
|
4049
|
+
const lineToUpdate = tableLines[i].lineIndex + 1;
|
|
3821
4050
|
const newRowIndex = tableLines[i].metadata.row + 1;
|
|
3822
4051
|
const newMetadata = { ...tableLines[i].metadata, row: newRowIndex, columnWidths };
|
|
3823
|
-
|
|
4052
|
+
|
|
3824
4053
|
applyTableLineMetadataAttribute(lineToUpdate, tblId, newRowIndex, numCols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
|
|
3825
4054
|
}
|
|
3826
|
-
|
|
3827
|
-
// Apply metadata to the new row
|
|
4055
|
+
|
|
3828
4056
|
const newMetadata = { tblId, row: targetLine.metadata.row, cols: numCols, columnWidths };
|
|
3829
4057
|
applyTableLineMetadataAttribute(insertLineIndex, tblId, targetLine.metadata.row, numCols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
|
|
3830
|
-
|
|
4058
|
+
|
|
3831
4059
|
editorInfo.ace_fastIncorp(10);
|
|
3832
4060
|
return true;
|
|
3833
4061
|
} catch (e) {
|
|
@@ -3835,17 +4063,15 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3835
4063
|
return false;
|
|
3836
4064
|
}
|
|
3837
4065
|
}
|
|
3838
|
-
|
|
4066
|
+
|
|
3839
4067
|
function addTableRowBelowWithText(tableLines, targetRowIndex, numCols, tblId, editorInfo, docManager) {
|
|
3840
4068
|
try {
|
|
3841
4069
|
const targetLine = tableLines[targetRowIndex];
|
|
3842
4070
|
const newLineText = Array.from({ length: numCols }).fill(' ').join(DELIMITER);
|
|
3843
4071
|
const insertLineIndex = targetLine.lineIndex + 1;
|
|
3844
|
-
|
|
3845
|
-
// Insert new line in text
|
|
4072
|
+
|
|
3846
4073
|
editorInfo.ace_performDocumentReplaceRange([insertLineIndex, 0], [insertLineIndex, 0], newLineText + '\n');
|
|
3847
|
-
|
|
3848
|
-
// Apply cell-specific attributes to the new row for authorship
|
|
4074
|
+
|
|
3849
4075
|
const rep = editorInfo.ace_getRep();
|
|
3850
4076
|
const cells = newLineText.split(DELIMITER);
|
|
3851
4077
|
let offset = 0;
|
|
@@ -3861,11 +4087,9 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3861
4087
|
offset += DELIMITER.length;
|
|
3862
4088
|
}
|
|
3863
4089
|
}
|
|
3864
|
-
|
|
3865
|
-
// Preserve column widths from existing metadata or extract from DOM
|
|
4090
|
+
|
|
3866
4091
|
let columnWidths = targetLine.metadata.columnWidths;
|
|
3867
4092
|
if (!columnWidths) {
|
|
3868
|
-
// Extract from DOM for block-styled rows
|
|
3869
4093
|
try {
|
|
3870
4094
|
const rep = editorInfo.ace_getRep();
|
|
3871
4095
|
const lineEntry = rep.lines.atIndex(targetLine.lineIndex);
|
|
@@ -3892,20 +4116,18 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3892
4116
|
console.error('[ep_data_tables] addTableRowBelow: Error extracting column widths from DOM:', e);
|
|
3893
4117
|
}
|
|
3894
4118
|
}
|
|
3895
|
-
|
|
3896
|
-
// Update metadata for all subsequent rows
|
|
4119
|
+
|
|
3897
4120
|
for (let i = targetRowIndex + 1; i < tableLines.length; i++) {
|
|
3898
|
-
const lineToUpdate = tableLines[i].lineIndex + 1;
|
|
4121
|
+
const lineToUpdate = tableLines[i].lineIndex + 1;
|
|
3899
4122
|
const newRowIndex = tableLines[i].metadata.row + 1;
|
|
3900
4123
|
const newMetadata = { ...tableLines[i].metadata, row: newRowIndex, columnWidths };
|
|
3901
|
-
|
|
4124
|
+
|
|
3902
4125
|
applyTableLineMetadataAttribute(lineToUpdate, tblId, newRowIndex, numCols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
|
|
3903
4126
|
}
|
|
3904
|
-
|
|
3905
|
-
// Apply metadata to the new row
|
|
4127
|
+
|
|
3906
4128
|
const newMetadata = { tblId, row: targetLine.metadata.row + 1, cols: numCols, columnWidths };
|
|
3907
4129
|
applyTableLineMetadataAttribute(insertLineIndex, tblId, targetLine.metadata.row + 1, numCols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
|
|
3908
|
-
|
|
4130
|
+
|
|
3909
4131
|
editorInfo.ace_fastIncorp(10);
|
|
3910
4132
|
return true;
|
|
3911
4133
|
} catch (e) {
|
|
@@ -3913,40 +4135,35 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3913
4135
|
return false;
|
|
3914
4136
|
}
|
|
3915
4137
|
}
|
|
3916
|
-
|
|
4138
|
+
|
|
3917
4139
|
function addTableColumnLeftWithText(tableLines, targetColIndex, editorInfo, docManager) {
|
|
3918
4140
|
const funcName = 'addTableColumnLeftWithText';
|
|
3919
4141
|
try {
|
|
3920
|
-
// Process each line individually like table creation does
|
|
3921
4142
|
for (const tableLine of tableLines) {
|
|
3922
4143
|
const lineText = tableLine.lineText;
|
|
3923
4144
|
const cells = lineText.split(DELIMITER);
|
|
3924
|
-
|
|
3925
|
-
// Calculate the exact insertion position - stop BEFORE the target column's delimiter
|
|
4145
|
+
|
|
3926
4146
|
let insertPos = 0;
|
|
3927
4147
|
for (let i = 0; i < targetColIndex; i++) {
|
|
3928
4148
|
insertPos += (cells[i]?.length ?? 0) + DELIMITER.length;
|
|
3929
4149
|
}
|
|
3930
|
-
|
|
3931
|
-
// Insert blank cell then delimiter (BLANK + separator)
|
|
4150
|
+
|
|
3932
4151
|
const textToInsert = ' ' + DELIMITER;
|
|
3933
4152
|
const insertStart = [tableLine.lineIndex, insertPos];
|
|
3934
4153
|
const insertEnd = [tableLine.lineIndex, insertPos];
|
|
3935
|
-
|
|
4154
|
+
|
|
3936
4155
|
editorInfo.ace_performDocumentReplaceRange(insertStart, insertEnd, textToInsert);
|
|
3937
|
-
|
|
3938
|
-
// Immediately apply authorship attributes like table creation does
|
|
4156
|
+
|
|
3939
4157
|
const rep = editorInfo.ace_getRep();
|
|
3940
4158
|
const lineEntry = rep.lines.atIndex(tableLine.lineIndex);
|
|
3941
4159
|
if (lineEntry) {
|
|
3942
4160
|
const newLineText = lineEntry.text || '';
|
|
3943
4161
|
const newCells = newLineText.split(DELIMITER);
|
|
3944
4162
|
let offset = 0;
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
for (let c = 0; c < tableLine.cols + 1; c++) { // +1 for the new column
|
|
4163
|
+
|
|
4164
|
+
for (let c = 0; c < tableLine.cols + 1; c++) {
|
|
3948
4165
|
const cellContent = (c < newCells.length) ? newCells[c] || '' : '';
|
|
3949
|
-
if (cellContent.length > 0) {
|
|
4166
|
+
if (cellContent.length > 0) {
|
|
3950
4167
|
const cellStart = [tableLine.lineIndex, offset];
|
|
3951
4168
|
const cellEnd = [tableLine.lineIndex, offset + cellContent.length];
|
|
3952
4169
|
// log(`[ep_data_tables] ${funcName}: Applying ${ATTR_CELL} attribute to Line ${tableLine.lineIndex} Col ${c} Range ${offset}-${offset + cellContent.length}`);
|
|
@@ -3958,20 +4175,16 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3958
4175
|
}
|
|
3959
4176
|
}
|
|
3960
4177
|
}
|
|
3961
|
-
|
|
3962
|
-
// Reset all column widths to equal distribution when adding a column
|
|
3963
|
-
// This avoids complex width calculations and ensures robust behavior
|
|
4178
|
+
|
|
3964
4179
|
const newColCount = tableLine.cols + 1;
|
|
3965
4180
|
const equalWidth = 100 / newColCount;
|
|
3966
4181
|
const normalizedWidths = Array(newColCount).fill(equalWidth);
|
|
3967
4182
|
// log(`[ep_data_tables] addTableColumnLeft: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
|
|
3968
|
-
|
|
3969
|
-
// Apply updated metadata
|
|
4183
|
+
|
|
3970
4184
|
const newMetadata = { ...tableLine.metadata, cols: tableLine.cols + 1, columnWidths: normalizedWidths };
|
|
3971
4185
|
applyTableLineMetadataAttribute(tableLine.lineIndex, tableLine.metadata.tblId, tableLine.metadata.row, tableLine.cols + 1, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
|
|
3972
4186
|
}
|
|
3973
|
-
|
|
3974
|
-
// Final sync
|
|
4187
|
+
|
|
3975
4188
|
editorInfo.ace_fastIncorp(10);
|
|
3976
4189
|
return true;
|
|
3977
4190
|
} catch (e) {
|
|
@@ -3979,41 +4192,36 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
3979
4192
|
return false;
|
|
3980
4193
|
}
|
|
3981
4194
|
}
|
|
3982
|
-
|
|
4195
|
+
|
|
3983
4196
|
function addTableColumnRightWithText(tableLines, targetColIndex, editorInfo, docManager) {
|
|
3984
4197
|
const funcName = 'addTableColumnRightWithText';
|
|
3985
4198
|
try {
|
|
3986
|
-
// Process each line individually like table creation does
|
|
3987
4199
|
for (const tableLine of tableLines) {
|
|
3988
4200
|
const lineText = tableLine.lineText;
|
|
3989
4201
|
const cells = lineText.split(DELIMITER);
|
|
3990
|
-
|
|
3991
|
-
// Calculate the exact insertion position - stop BEFORE the target column's trailing delimiter
|
|
4202
|
+
|
|
3992
4203
|
let insertPos = 0;
|
|
3993
4204
|
for (let i = 0; i <= targetColIndex; i++) {
|
|
3994
4205
|
insertPos += (cells[i]?.length ?? 0);
|
|
3995
4206
|
if (i < targetColIndex) insertPos += DELIMITER.length;
|
|
3996
4207
|
}
|
|
3997
|
-
|
|
3998
|
-
// Insert delimiter then blank cell (separator + BLANK)
|
|
4208
|
+
|
|
3999
4209
|
const textToInsert = DELIMITER + ' ';
|
|
4000
4210
|
const insertStart = [tableLine.lineIndex, insertPos];
|
|
4001
4211
|
const insertEnd = [tableLine.lineIndex, insertPos];
|
|
4002
|
-
|
|
4212
|
+
|
|
4003
4213
|
editorInfo.ace_performDocumentReplaceRange(insertStart, insertEnd, textToInsert);
|
|
4004
|
-
|
|
4005
|
-
// Immediately apply authorship attributes like table creation does
|
|
4214
|
+
|
|
4006
4215
|
const rep = editorInfo.ace_getRep();
|
|
4007
4216
|
const lineEntry = rep.lines.atIndex(tableLine.lineIndex);
|
|
4008
4217
|
if (lineEntry) {
|
|
4009
4218
|
const newLineText = lineEntry.text || '';
|
|
4010
4219
|
const newCells = newLineText.split(DELIMITER);
|
|
4011
4220
|
let offset = 0;
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
for (let c = 0; c < tableLine.cols + 1; c++) { // +1 for the new column
|
|
4221
|
+
|
|
4222
|
+
for (let c = 0; c < tableLine.cols + 1; c++) {
|
|
4015
4223
|
const cellContent = (c < newCells.length) ? newCells[c] || '' : '';
|
|
4016
|
-
if (cellContent.length > 0) {
|
|
4224
|
+
if (cellContent.length > 0) {
|
|
4017
4225
|
const cellStart = [tableLine.lineIndex, offset];
|
|
4018
4226
|
const cellEnd = [tableLine.lineIndex, offset + cellContent.length];
|
|
4019
4227
|
// log(`[ep_data_tables] ${funcName}: Applying ${ATTR_CELL} attribute to Line ${tableLine.lineIndex} Col ${c} Range ${offset}-${offset + cellContent.length}`);
|
|
@@ -4025,20 +4233,16 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4025
4233
|
}
|
|
4026
4234
|
}
|
|
4027
4235
|
}
|
|
4028
|
-
|
|
4029
|
-
// Reset all column widths to equal distribution when adding a column
|
|
4030
|
-
// This avoids complex width calculations and ensures robust behavior
|
|
4236
|
+
|
|
4031
4237
|
const newColCount = tableLine.cols + 1;
|
|
4032
4238
|
const equalWidth = 100 / newColCount;
|
|
4033
4239
|
const normalizedWidths = Array(newColCount).fill(equalWidth);
|
|
4034
4240
|
// log(`[ep_data_tables] addTableColumnRight: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
|
|
4035
|
-
|
|
4036
|
-
// Apply updated metadata
|
|
4241
|
+
|
|
4037
4242
|
const newMetadata = { ...tableLine.metadata, cols: tableLine.cols + 1, columnWidths: normalizedWidths };
|
|
4038
4243
|
applyTableLineMetadataAttribute(tableLine.lineIndex, tableLine.metadata.tblId, tableLine.metadata.row, tableLine.cols + 1, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
|
|
4039
4244
|
}
|
|
4040
|
-
|
|
4041
|
-
// Final sync
|
|
4245
|
+
|
|
4042
4246
|
editorInfo.ace_fastIncorp(10);
|
|
4043
4247
|
return true;
|
|
4044
4248
|
} catch (e) {
|
|
@@ -4046,36 +4250,29 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4046
4250
|
return false;
|
|
4047
4251
|
}
|
|
4048
4252
|
}
|
|
4049
|
-
|
|
4253
|
+
|
|
4050
4254
|
function deleteTableRowWithText(tableLines, targetRowIndex, editorInfo, docManager) {
|
|
4051
4255
|
try {
|
|
4052
4256
|
const targetLine = tableLines[targetRowIndex];
|
|
4053
|
-
|
|
4054
|
-
// Special handling for deleting the first row (row index 0)
|
|
4055
|
-
// Insert a blank line to prevent the table from getting stuck at line 1
|
|
4257
|
+
|
|
4056
4258
|
if (targetRowIndex === 0) {
|
|
4057
4259
|
// log('[ep_data_tables] Deleting first row (row 0) - inserting blank line to prevent table from getting stuck');
|
|
4058
4260
|
const insertStart = [targetLine.lineIndex, 0];
|
|
4059
4261
|
editorInfo.ace_performDocumentReplaceRange(insertStart, insertStart, '\n');
|
|
4060
|
-
|
|
4061
|
-
// Now delete the table line (which is now at lineIndex + 1)
|
|
4262
|
+
|
|
4062
4263
|
const deleteStart = [targetLine.lineIndex + 1, 0];
|
|
4063
4264
|
const deleteEnd = [targetLine.lineIndex + 2, 0];
|
|
4064
4265
|
editorInfo.ace_performDocumentReplaceRange(deleteStart, deleteEnd, '');
|
|
4065
4266
|
} else {
|
|
4066
|
-
// Delete the entire line normally
|
|
4067
4267
|
const deleteStart = [targetLine.lineIndex, 0];
|
|
4068
4268
|
const deleteEnd = [targetLine.lineIndex + 1, 0];
|
|
4069
4269
|
editorInfo.ace_performDocumentReplaceRange(deleteStart, deleteEnd, '');
|
|
4070
4270
|
}
|
|
4071
|
-
|
|
4072
|
-
// Extract column widths from target line before deletion for preserving in remaining rows
|
|
4271
|
+
|
|
4073
4272
|
let columnWidths = targetLine.metadata.columnWidths;
|
|
4074
4273
|
if (!columnWidths) {
|
|
4075
|
-
// Extract from DOM for block-styled rows
|
|
4076
4274
|
try {
|
|
4077
4275
|
const rep = editorInfo.ace_getRep();
|
|
4078
|
-
// Check any remaining table line for column widths
|
|
4079
4276
|
for (const tableLine of tableLines) {
|
|
4080
4277
|
if (tableLine.lineIndex !== targetLine.lineIndex) {
|
|
4081
4278
|
const lineEntry = rep.lines.atIndex(tableLine.lineIndex >= targetLine.lineIndex ? tableLine.lineIndex - 1 : tableLine.lineIndex);
|
|
@@ -4105,16 +4302,15 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4105
4302
|
console.error('[ep_data_tables] deleteTableRow: Error extracting column widths from DOM:', e);
|
|
4106
4303
|
}
|
|
4107
4304
|
}
|
|
4108
|
-
|
|
4109
|
-
// Update metadata for all subsequent rows
|
|
4305
|
+
|
|
4110
4306
|
for (let i = targetRowIndex + 1; i < tableLines.length; i++) {
|
|
4111
|
-
const lineToUpdate = tableLines[i].lineIndex - 1;
|
|
4307
|
+
const lineToUpdate = tableLines[i].lineIndex - 1;
|
|
4112
4308
|
const newRowIndex = tableLines[i].metadata.row - 1;
|
|
4113
4309
|
const newMetadata = { ...tableLines[i].metadata, row: newRowIndex, columnWidths };
|
|
4114
|
-
|
|
4310
|
+
|
|
4115
4311
|
applyTableLineMetadataAttribute(lineToUpdate, tableLines[i].metadata.tblId, newRowIndex, tableLines[i].cols, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
|
|
4116
4312
|
}
|
|
4117
|
-
|
|
4313
|
+
|
|
4118
4314
|
editorInfo.ace_fastIncorp(10);
|
|
4119
4315
|
return true;
|
|
4120
4316
|
} catch (e) {
|
|
@@ -4122,62 +4318,50 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4122
4318
|
return false;
|
|
4123
4319
|
}
|
|
4124
4320
|
}
|
|
4125
|
-
|
|
4126
4321
|
function deleteTableColumnWithText(tableLines, targetColIndex, editorInfo, docManager) {
|
|
4127
4322
|
try {
|
|
4128
|
-
// Update text content for all table lines using precise character deletion
|
|
4129
4323
|
for (const tableLine of tableLines) {
|
|
4130
4324
|
const lineText = tableLine.lineText;
|
|
4131
4325
|
const cells = lineText.split(DELIMITER);
|
|
4132
|
-
|
|
4326
|
+
|
|
4133
4327
|
if (targetColIndex >= cells.length) {
|
|
4134
4328
|
// log(`[ep_data_tables] Warning: Target column ${targetColIndex} doesn't exist in line with ${cells.length} columns`);
|
|
4135
4329
|
continue;
|
|
4136
4330
|
}
|
|
4137
|
-
|
|
4138
|
-
// Calculate the exact character range to delete
|
|
4331
|
+
|
|
4139
4332
|
let deleteStart = 0;
|
|
4140
4333
|
let deleteEnd = 0;
|
|
4141
|
-
|
|
4142
|
-
// Calculate start position
|
|
4334
|
+
|
|
4143
4335
|
for (let i = 0; i < targetColIndex; i++) {
|
|
4144
4336
|
deleteStart += (cells[i]?.length ?? 0) + DELIMITER.length;
|
|
4145
4337
|
}
|
|
4146
|
-
|
|
4147
|
-
// Calculate end position
|
|
4338
|
+
|
|
4148
4339
|
deleteEnd = deleteStart + (cells[targetColIndex]?.length ?? 0);
|
|
4149
|
-
|
|
4150
|
-
// Include the delimiter in deletion
|
|
4340
|
+
|
|
4151
4341
|
if (targetColIndex === 0 && cells.length > 1) {
|
|
4152
|
-
// If deleting first column, include the delimiter after it
|
|
4153
4342
|
deleteEnd += DELIMITER.length;
|
|
4154
4343
|
} else if (targetColIndex > 0) {
|
|
4155
|
-
// If deleting any other column, include the delimiter before it
|
|
4156
4344
|
deleteStart -= DELIMITER.length;
|
|
4157
4345
|
}
|
|
4158
|
-
|
|
4346
|
+
|
|
4159
4347
|
// log(`[ep_data_tables] Deleting column ${targetColIndex} from line ${tableLine.lineIndex}: chars ${deleteStart}-${deleteEnd} from "${lineText}"`);
|
|
4160
|
-
|
|
4161
|
-
// Perform the precise deletion
|
|
4348
|
+
|
|
4162
4349
|
const rangeStart = [tableLine.lineIndex, deleteStart];
|
|
4163
4350
|
const rangeEnd = [tableLine.lineIndex, deleteEnd];
|
|
4164
|
-
|
|
4351
|
+
|
|
4165
4352
|
editorInfo.ace_performDocumentReplaceRange(rangeStart, rangeEnd, '');
|
|
4166
|
-
|
|
4167
|
-
// Reset all column widths to equal distribution when deleting a column
|
|
4168
|
-
// This avoids complex width calculations and ensures robust behavior
|
|
4353
|
+
|
|
4169
4354
|
const newColCount = tableLine.cols - 1;
|
|
4170
4355
|
if (newColCount > 0) {
|
|
4171
4356
|
const equalWidth = 100 / newColCount;
|
|
4172
4357
|
const normalizedWidths = Array(newColCount).fill(equalWidth);
|
|
4173
4358
|
// log(`[ep_data_tables] deleteTableColumn: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
|
|
4174
|
-
|
|
4175
|
-
// Update metadata
|
|
4359
|
+
|
|
4176
4360
|
const newMetadata = { ...tableLine.metadata, cols: newColCount, columnWidths: normalizedWidths };
|
|
4177
4361
|
applyTableLineMetadataAttribute(tableLine.lineIndex, tableLine.metadata.tblId, tableLine.metadata.row, newColCount, editorInfo.ace_getRep(), editorInfo, JSON.stringify(newMetadata), docManager);
|
|
4178
4362
|
}
|
|
4179
4363
|
}
|
|
4180
|
-
|
|
4364
|
+
|
|
4181
4365
|
editorInfo.ace_fastIncorp(10);
|
|
4182
4366
|
return true;
|
|
4183
4367
|
} catch (e) {
|
|
@@ -4185,45 +4369,37 @@ exports.aceInitialized = (h, ctx) => {
|
|
|
4185
4369
|
return false;
|
|
4186
4370
|
}
|
|
4187
4371
|
}
|
|
4188
|
-
|
|
4189
|
-
// ... existing code ...
|
|
4372
|
+
|
|
4190
4373
|
|
|
4191
4374
|
// log('aceInitialized: END - helpers defined.');
|
|
4192
4375
|
};
|
|
4193
4376
|
|
|
4194
|
-
// ───────────────────── required no‑op stubs ─────────────────────
|
|
4195
4377
|
exports.aceStartLineAndCharForPoint = () => { return undefined; };
|
|
4196
4378
|
exports.aceEndLineAndCharForPoint = () => { return undefined; };
|
|
4197
4379
|
|
|
4198
|
-
// NEW: Style protection for table cells
|
|
4199
4380
|
exports.aceSetAuthorStyle = (hook, ctx) => {
|
|
4200
4381
|
const logPrefix = '[ep_data_tables:aceSetAuthorStyle]';
|
|
4201
4382
|
// log(`${logPrefix} START`, { hook, ctx });
|
|
4202
4383
|
|
|
4203
|
-
// If no selection or no style to apply, allow default
|
|
4204
4384
|
if (!ctx || !ctx.rep || !ctx.rep.selStart || !ctx.rep.selEnd || !ctx.key) {
|
|
4205
4385
|
// log(`${logPrefix} No selection or style key. Allowing default.`);
|
|
4206
4386
|
return;
|
|
4207
4387
|
}
|
|
4208
4388
|
|
|
4209
|
-
// Check if selection is within a table
|
|
4210
4389
|
const startLine = ctx.rep.selStart[0];
|
|
4211
4390
|
const endLine = ctx.rep.selEnd[0];
|
|
4212
|
-
|
|
4213
|
-
// If selection spans multiple lines, prevent style application
|
|
4391
|
+
|
|
4214
4392
|
if (startLine !== endLine) {
|
|
4215
4393
|
// log(`${logPrefix} Selection spans multiple lines. Preventing style application to protect table structure.`);
|
|
4216
4394
|
return false;
|
|
4217
4395
|
}
|
|
4218
4396
|
|
|
4219
|
-
// Check if the line is a table line
|
|
4220
4397
|
const lineAttrString = ctx.documentAttributeManager?.getAttributeOnLine(startLine, ATTR_TABLE_JSON);
|
|
4221
4398
|
if (!lineAttrString) {
|
|
4222
4399
|
// log(`${logPrefix} Line ${startLine} is not a table line. Allowing default style application.`);
|
|
4223
4400
|
return;
|
|
4224
4401
|
}
|
|
4225
4402
|
|
|
4226
|
-
// List of styles that could break table structure
|
|
4227
4403
|
const BLOCKED_STYLES = [
|
|
4228
4404
|
'list', 'listType', 'indent', 'align', 'heading', 'code', 'quote',
|
|
4229
4405
|
'horizontalrule', 'pagebreak', 'linebreak', 'clear'
|
|
@@ -4234,7 +4410,6 @@ exports.aceSetAuthorStyle = (hook, ctx) => {
|
|
|
4234
4410
|
return false;
|
|
4235
4411
|
}
|
|
4236
4412
|
|
|
4237
|
-
// For allowed styles, ensure they only apply within cell boundaries
|
|
4238
4413
|
try {
|
|
4239
4414
|
const tableMetadata = JSON.parse(lineAttrString);
|
|
4240
4415
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
|
|
@@ -4248,11 +4423,10 @@ exports.aceSetAuthorStyle = (hook, ctx) => {
|
|
|
4248
4423
|
let selectionStartCell = -1;
|
|
4249
4424
|
let selectionEndCell = -1;
|
|
4250
4425
|
|
|
4251
|
-
// Find which cells the selection spans
|
|
4252
4426
|
for (let i = 0; i < cells.length; i++) {
|
|
4253
4427
|
const cellLength = cells[i]?.length ?? 0;
|
|
4254
4428
|
const cellEndCol = currentOffset + cellLength;
|
|
4255
|
-
|
|
4429
|
+
|
|
4256
4430
|
if (ctx.rep.selStart[1] >= currentOffset && ctx.rep.selStart[1] <= cellEndCol) {
|
|
4257
4431
|
selectionStartCell = i;
|
|
4258
4432
|
}
|
|
@@ -4262,62 +4436,53 @@ exports.aceSetAuthorStyle = (hook, ctx) => {
|
|
|
4262
4436
|
currentOffset += cellLength + DELIMITER.length;
|
|
4263
4437
|
}
|
|
4264
4438
|
|
|
4265
|
-
// If selection spans multiple cells, prevent style application
|
|
4266
4439
|
if (selectionStartCell !== selectionEndCell) {
|
|
4267
4440
|
// log(`${logPrefix} Selection spans multiple cells. Preventing style application to protect table structure.`);
|
|
4268
4441
|
return false;
|
|
4269
4442
|
}
|
|
4270
4443
|
|
|
4271
|
-
// If selection includes cell delimiters, prevent style application
|
|
4272
4444
|
const cellStartCol = cells.slice(0, selectionStartCell).reduce((acc, cell) => acc + cell.length + DELIMITER.length, 0);
|
|
4273
4445
|
const cellEndCol = cellStartCol + cells[selectionStartCell].length;
|
|
4274
|
-
|
|
4446
|
+
|
|
4275
4447
|
if (ctx.rep.selStart[1] <= cellStartCol || ctx.rep.selEnd[1] >= cellEndCol) {
|
|
4276
4448
|
// log(`${logPrefix} Selection includes cell delimiters. Preventing style application to protect table structure.`);
|
|
4277
4449
|
return false;
|
|
4278
4450
|
}
|
|
4279
4451
|
|
|
4280
4452
|
// log(`${logPrefix} Style '${ctx.key}' allowed within cell boundaries.`);
|
|
4281
|
-
return;
|
|
4453
|
+
return;
|
|
4282
4454
|
} catch (e) {
|
|
4283
4455
|
console.error(`${logPrefix} Error processing style application:`, e);
|
|
4284
4456
|
// log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
|
|
4285
|
-
return false;
|
|
4457
|
+
return false;
|
|
4286
4458
|
}
|
|
4287
4459
|
};
|
|
4288
4460
|
|
|
4289
4461
|
exports.aceEditorCSS = () => {
|
|
4290
|
-
// Path relative to Etherpad's static/plugins/ directory
|
|
4291
|
-
// Format should be: pluginName/path/to/file.css
|
|
4292
4462
|
return ['ep_data_tables/static/css/datatables-editor.css', 'ep_data_tables/static/css/caret.css'];
|
|
4293
4463
|
};
|
|
4294
4464
|
|
|
4295
|
-
// Register TABLE as a block element, hoping it influences rendering behavior
|
|
4296
4465
|
exports.aceRegisterBlockElements = () => ['table'];
|
|
4297
4466
|
|
|
4298
|
-
// NEW: Column resize helper functions (adapted from images plugin)
|
|
4299
4467
|
const startColumnResize = (table, columnIndex, startX, metadata, lineNum) => {
|
|
4300
4468
|
const funcName = 'startColumnResize';
|
|
4301
4469
|
// log(`${funcName}: Starting resize for column ${columnIndex}`);
|
|
4302
|
-
|
|
4470
|
+
|
|
4303
4471
|
isResizing = true;
|
|
4304
4472
|
resizeStartX = startX;
|
|
4305
|
-
resizeCurrentX = startX;
|
|
4473
|
+
resizeCurrentX = startX;
|
|
4306
4474
|
resizeTargetTable = table;
|
|
4307
4475
|
resizeTargetColumn = columnIndex;
|
|
4308
4476
|
resizeTableMetadata = metadata;
|
|
4309
4477
|
resizeLineNum = lineNum;
|
|
4310
|
-
|
|
4311
|
-
// Get current column widths
|
|
4478
|
+
|
|
4312
4479
|
const numCols = metadata.cols;
|
|
4313
4480
|
resizeOriginalWidths = metadata.columnWidths ? [...metadata.columnWidths] : Array(numCols).fill(100 / numCols);
|
|
4314
|
-
|
|
4481
|
+
|
|
4315
4482
|
// log(`${funcName}: Original widths:`, resizeOriginalWidths);
|
|
4316
|
-
|
|
4317
|
-
// Create visual overlay instead of modifying table directly
|
|
4483
|
+
|
|
4318
4484
|
createResizeOverlay(table, columnIndex);
|
|
4319
|
-
|
|
4320
|
-
// Prevent text selection during resize
|
|
4485
|
+
|
|
4321
4486
|
document.body.style.userSelect = 'none';
|
|
4322
4487
|
document.body.style.webkitUserSelect = 'none';
|
|
4323
4488
|
document.body.style.mozUserSelect = 'none';
|
|
@@ -4325,12 +4490,10 @@ const startColumnResize = (table, columnIndex, startX, metadata, lineNum) => {
|
|
|
4325
4490
|
};
|
|
4326
4491
|
|
|
4327
4492
|
const createResizeOverlay = (table, columnIndex) => {
|
|
4328
|
-
// Create a visual overlay that shows resize feedback using the same positioning logic as image plugin
|
|
4329
4493
|
if (resizeOverlay) {
|
|
4330
4494
|
resizeOverlay.remove();
|
|
4331
4495
|
}
|
|
4332
|
-
|
|
4333
|
-
// Get all the necessary container references like image plugin
|
|
4496
|
+
|
|
4334
4497
|
const $innerIframe = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]');
|
|
4335
4498
|
if ($innerIframe.length === 0) {
|
|
4336
4499
|
console.error('[ep_data_tables] createResizeOverlay: Could not find inner iframe');
|
|
@@ -4339,51 +4502,47 @@ const createResizeOverlay = (table, columnIndex) => {
|
|
|
4339
4502
|
|
|
4340
4503
|
const innerDocBody = $innerIframe.contents().find('body')[0];
|
|
4341
4504
|
const padOuter = $('iframe[name="ace_outer"]').contents().find('body');
|
|
4342
|
-
|
|
4505
|
+
|
|
4343
4506
|
if (!innerDocBody || padOuter.length === 0) {
|
|
4344
4507
|
console.error('[ep_data_tables] createResizeOverlay: Could not find required container elements');
|
|
4345
4508
|
return;
|
|
4346
4509
|
}
|
|
4347
4510
|
|
|
4348
|
-
// Find all tables that belong to the same table (same tblId)
|
|
4349
4511
|
const tblId = table.getAttribute('data-tblId');
|
|
4350
4512
|
if (!tblId) {
|
|
4351
4513
|
console.error('[ep_data_tables] createResizeOverlay: No tblId found on table');
|
|
4352
4514
|
return;
|
|
4353
4515
|
}
|
|
4354
|
-
|
|
4516
|
+
|
|
4355
4517
|
const allTableRows = innerDocBody.querySelectorAll(`table.dataTable[data-tblId="${tblId}"]`);
|
|
4356
4518
|
if (allTableRows.length === 0) {
|
|
4357
4519
|
console.error('[ep_data_tables] createResizeOverlay: No table rows found for tblId:', tblId);
|
|
4358
4520
|
return;
|
|
4359
4521
|
}
|
|
4360
|
-
|
|
4361
|
-
// Calculate the bounding box that encompasses all table rows
|
|
4522
|
+
|
|
4362
4523
|
let minTop = Infinity;
|
|
4363
4524
|
let maxBottom = -Infinity;
|
|
4364
4525
|
let tableLeft = 0;
|
|
4365
4526
|
let tableWidth = 0;
|
|
4366
|
-
|
|
4527
|
+
|
|
4367
4528
|
Array.from(allTableRows).forEach((tableRow, index) => {
|
|
4368
4529
|
const rect = tableRow.getBoundingClientRect();
|
|
4369
4530
|
minTop = Math.min(minTop, rect.top);
|
|
4370
4531
|
maxBottom = Math.max(maxBottom, rect.bottom);
|
|
4371
|
-
|
|
4372
|
-
// Use the first table row for horizontal positioning
|
|
4532
|
+
|
|
4373
4533
|
if (index === 0) {
|
|
4374
4534
|
tableLeft = rect.left;
|
|
4375
4535
|
tableWidth = rect.width;
|
|
4376
4536
|
}
|
|
4377
4537
|
});
|
|
4378
|
-
|
|
4538
|
+
|
|
4379
4539
|
const totalTableHeight = maxBottom - minTop;
|
|
4380
|
-
|
|
4540
|
+
|
|
4381
4541
|
// log(`createResizeOverlay: Found ${allTableRows.length} table rows, total height: ${totalTableHeight}px`);
|
|
4382
|
-
|
|
4383
|
-
// Calculate positioning using the same method as image plugin
|
|
4542
|
+
|
|
4384
4543
|
let innerBodyRect, innerIframeRect, outerBodyRect;
|
|
4385
4544
|
let scrollTopInner, scrollLeftInner, scrollTopOuter, scrollLeftOuter;
|
|
4386
|
-
|
|
4545
|
+
|
|
4387
4546
|
try {
|
|
4388
4547
|
innerBodyRect = innerDocBody.getBoundingClientRect();
|
|
4389
4548
|
innerIframeRect = $innerIframe[0].getBoundingClientRect();
|
|
@@ -4396,44 +4555,38 @@ const createResizeOverlay = (table, columnIndex) => {
|
|
|
4396
4555
|
console.error('[ep_data_tables] createResizeOverlay: Error getting container rects/scrolls:', e);
|
|
4397
4556
|
return;
|
|
4398
4557
|
}
|
|
4399
|
-
|
|
4400
|
-
// Get table position relative to inner body using the full table bounds
|
|
4558
|
+
|
|
4401
4559
|
const tableTopRelInner = minTop - innerBodyRect.top + scrollTopInner;
|
|
4402
4560
|
const tableLeftRelInner = tableLeft - innerBodyRect.left + scrollLeftInner;
|
|
4403
|
-
|
|
4404
|
-
// Calculate position in outer body coordinates (like image plugin)
|
|
4561
|
+
|
|
4405
4562
|
const innerFrameTopRelOuter = innerIframeRect.top - outerBodyRect.top + scrollTopOuter;
|
|
4406
4563
|
const innerFrameLeftRelOuter = innerIframeRect.left - outerBodyRect.left + scrollLeftOuter;
|
|
4407
|
-
|
|
4564
|
+
|
|
4408
4565
|
const overlayTopOuter = innerFrameTopRelOuter + tableTopRelInner;
|
|
4409
4566
|
const overlayLeftOuter = innerFrameLeftRelOuter + tableLeftRelInner;
|
|
4410
|
-
|
|
4411
|
-
// Apply padding and manual offsets like image plugin
|
|
4567
|
+
|
|
4412
4568
|
const outerPadding = window.getComputedStyle(padOuter[0]);
|
|
4413
4569
|
const outerPaddingTop = parseFloat(outerPadding.paddingTop) || 0;
|
|
4414
4570
|
const outerPaddingLeft = parseFloat(outerPadding.paddingLeft) || 0;
|
|
4415
|
-
|
|
4416
|
-
// Use the same manual offsets as image plugin
|
|
4571
|
+
|
|
4417
4572
|
const MANUAL_OFFSET_TOP = 6;
|
|
4418
4573
|
const MANUAL_OFFSET_LEFT = 39;
|
|
4419
|
-
|
|
4574
|
+
|
|
4420
4575
|
const finalOverlayTop = overlayTopOuter + outerPaddingTop + MANUAL_OFFSET_TOP;
|
|
4421
4576
|
const finalOverlayLeft = overlayLeftOuter + outerPaddingLeft + MANUAL_OFFSET_LEFT;
|
|
4422
|
-
|
|
4423
|
-
// Calculate the position for the blue line at the right edge of the target column
|
|
4577
|
+
|
|
4424
4578
|
const tds = table.querySelectorAll('td');
|
|
4425
4579
|
const tds_array = Array.from(tds);
|
|
4426
4580
|
let linePosition = 0;
|
|
4427
|
-
|
|
4581
|
+
|
|
4428
4582
|
if (columnIndex < tds_array.length) {
|
|
4429
4583
|
const currentTd = tds_array[columnIndex];
|
|
4430
4584
|
const currentTdRect = currentTd.getBoundingClientRect();
|
|
4431
|
-
const currentRelativeLeft = currentTdRect.left - tableLeft;
|
|
4585
|
+
const currentRelativeLeft = currentTdRect.left - tableLeft;
|
|
4432
4586
|
const currentWidth = currentTdRect.width;
|
|
4433
4587
|
linePosition = currentRelativeLeft + currentWidth;
|
|
4434
4588
|
}
|
|
4435
|
-
|
|
4436
|
-
// Create overlay container (invisible background) that spans the entire table height
|
|
4589
|
+
|
|
4437
4590
|
resizeOverlay = document.createElement('div');
|
|
4438
4591
|
resizeOverlay.className = 'ep-data_tables-resize-overlay';
|
|
4439
4592
|
resizeOverlay.style.cssText = `
|
|
@@ -4447,8 +4600,7 @@ const createResizeOverlay = (table, columnIndex) => {
|
|
|
4447
4600
|
background: transparent;
|
|
4448
4601
|
box-sizing: border-box;
|
|
4449
4602
|
`;
|
|
4450
|
-
|
|
4451
|
-
// Create the blue vertical line (Google Docs style) spanning the full table height
|
|
4603
|
+
|
|
4452
4604
|
const resizeLine = document.createElement('div');
|
|
4453
4605
|
resizeLine.className = 'resize-line';
|
|
4454
4606
|
resizeLine.style.cssText = `
|
|
@@ -4461,61 +4613,53 @@ const createResizeOverlay = (table, columnIndex) => {
|
|
|
4461
4613
|
z-index: 1001;
|
|
4462
4614
|
`;
|
|
4463
4615
|
resizeOverlay.appendChild(resizeLine);
|
|
4464
|
-
|
|
4465
|
-
// Append to outer body like image plugin does with its outline
|
|
4616
|
+
|
|
4466
4617
|
padOuter.append(resizeOverlay);
|
|
4467
|
-
|
|
4618
|
+
|
|
4468
4619
|
// log('createResizeOverlay: Created Google Docs style blue line overlay spanning entire table height');
|
|
4469
4620
|
};
|
|
4470
4621
|
|
|
4471
4622
|
const updateColumnResize = (currentX) => {
|
|
4472
4623
|
if (!isResizing || !resizeTargetTable || !resizeOverlay) return;
|
|
4473
|
-
|
|
4474
|
-
resizeCurrentX = currentX;
|
|
4624
|
+
|
|
4625
|
+
resizeCurrentX = currentX;
|
|
4475
4626
|
const deltaX = currentX - resizeStartX;
|
|
4476
|
-
|
|
4477
|
-
// Get the table width from the first row for percentage calculation
|
|
4627
|
+
|
|
4478
4628
|
const tblId = resizeTargetTable.getAttribute('data-tblId');
|
|
4479
4629
|
if (!tblId) return;
|
|
4480
|
-
|
|
4481
|
-
// Find the first table row to get consistent width measurements
|
|
4630
|
+
|
|
4482
4631
|
const $innerIframe = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]');
|
|
4483
4632
|
const innerDocBody = $innerIframe.contents().find('body')[0];
|
|
4484
4633
|
const firstTableRow = innerDocBody.querySelector(`table.dataTable[data-tblId="${tblId}"]`);
|
|
4485
|
-
|
|
4634
|
+
|
|
4486
4635
|
if (!firstTableRow) return;
|
|
4487
|
-
|
|
4636
|
+
|
|
4488
4637
|
const tableRect = firstTableRow.getBoundingClientRect();
|
|
4489
4638
|
const deltaPercent = (deltaX / tableRect.width) * 100;
|
|
4490
|
-
|
|
4491
|
-
// Calculate new widths for final application
|
|
4639
|
+
|
|
4492
4640
|
const newWidths = [...resizeOriginalWidths];
|
|
4493
4641
|
const currentColumn = resizeTargetColumn;
|
|
4494
4642
|
const nextColumn = currentColumn + 1;
|
|
4495
|
-
|
|
4643
|
+
|
|
4496
4644
|
if (nextColumn < newWidths.length) {
|
|
4497
4645
|
const transfer = Math.min(deltaPercent, newWidths[nextColumn] - 5);
|
|
4498
4646
|
const actualTransfer = Math.max(transfer, -(newWidths[currentColumn] - 5));
|
|
4499
|
-
|
|
4647
|
+
|
|
4500
4648
|
newWidths[currentColumn] += actualTransfer;
|
|
4501
4649
|
newWidths[nextColumn] -= actualTransfer;
|
|
4502
|
-
|
|
4503
|
-
// Update the blue line position to show the new column boundary
|
|
4650
|
+
|
|
4504
4651
|
const resizeLine = resizeOverlay.querySelector('.resize-line');
|
|
4505
4652
|
if (resizeLine) {
|
|
4506
|
-
// Calculate new position based on the updated column width
|
|
4507
4653
|
const newColumnWidth = (newWidths[currentColumn] / 100) * tableRect.width;
|
|
4508
|
-
|
|
4509
|
-
// Find the original left position relative to the first table row
|
|
4654
|
+
|
|
4510
4655
|
const tds = firstTableRow.querySelectorAll('td');
|
|
4511
4656
|
const tds_array = Array.from(tds);
|
|
4512
|
-
|
|
4657
|
+
|
|
4513
4658
|
if (currentColumn < tds_array.length) {
|
|
4514
4659
|
const currentTd = tds_array[currentColumn];
|
|
4515
4660
|
const currentTdRect = currentTd.getBoundingClientRect();
|
|
4516
4661
|
const currentRelativeLeft = currentTdRect.left - tableRect.left;
|
|
4517
|
-
|
|
4518
|
-
// New line position is the original left position plus the new width
|
|
4662
|
+
|
|
4519
4663
|
const newLinePosition = currentRelativeLeft + newColumnWidth;
|
|
4520
4664
|
resizeLine.style.left = newLinePosition + 'px';
|
|
4521
4665
|
}
|
|
@@ -4531,75 +4675,66 @@ const finishColumnResize = (editorInfo, docManager) => {
|
|
|
4531
4675
|
|
|
4532
4676
|
const funcName = 'finishColumnResize';
|
|
4533
4677
|
// log(`${funcName}: Finishing resize`);
|
|
4534
|
-
|
|
4535
|
-
// Calculate final widths from actual mouse movement
|
|
4678
|
+
|
|
4536
4679
|
const tableRect = resizeTargetTable.getBoundingClientRect();
|
|
4537
4680
|
const deltaX = resizeCurrentX - resizeStartX;
|
|
4538
4681
|
const deltaPercent = (deltaX / tableRect.width) * 100;
|
|
4539
|
-
|
|
4682
|
+
|
|
4540
4683
|
// log(`${funcName}: Mouse moved ${deltaX}px (${deltaPercent.toFixed(1)}%)`);
|
|
4541
|
-
|
|
4684
|
+
|
|
4542
4685
|
const finalWidths = [...resizeOriginalWidths];
|
|
4543
4686
|
const currentColumn = resizeTargetColumn;
|
|
4544
4687
|
const nextColumn = currentColumn + 1;
|
|
4545
|
-
|
|
4688
|
+
|
|
4546
4689
|
if (nextColumn < finalWidths.length) {
|
|
4547
|
-
// Transfer width between columns with minimum constraints
|
|
4548
4690
|
const transfer = Math.min(deltaPercent, finalWidths[nextColumn] - 5);
|
|
4549
4691
|
const actualTransfer = Math.max(transfer, -(finalWidths[currentColumn] - 5));
|
|
4550
|
-
|
|
4692
|
+
|
|
4551
4693
|
finalWidths[currentColumn] += actualTransfer;
|
|
4552
4694
|
finalWidths[nextColumn] -= actualTransfer;
|
|
4553
|
-
|
|
4695
|
+
|
|
4554
4696
|
// log(`${funcName}: Transferred ${actualTransfer.toFixed(1)}% from column ${nextColumn} to column ${currentColumn}`);
|
|
4555
4697
|
}
|
|
4556
|
-
|
|
4557
|
-
// Normalize widths
|
|
4698
|
+
|
|
4558
4699
|
const totalWidth = finalWidths.reduce((sum, width) => sum + width, 0);
|
|
4559
4700
|
if (totalWidth > 0) {
|
|
4560
4701
|
finalWidths.forEach((width, index) => {
|
|
4561
4702
|
finalWidths[index] = (width / totalWidth) * 100;
|
|
4562
4703
|
});
|
|
4563
4704
|
}
|
|
4564
|
-
|
|
4705
|
+
|
|
4565
4706
|
// log(`${funcName}: Final normalized widths:`, finalWidths.map(w => w.toFixed(1) + '%'));
|
|
4566
|
-
|
|
4567
|
-
// Clean up overlay
|
|
4707
|
+
|
|
4568
4708
|
if (resizeOverlay) {
|
|
4569
4709
|
resizeOverlay.remove();
|
|
4570
4710
|
resizeOverlay = null;
|
|
4571
4711
|
}
|
|
4572
|
-
|
|
4573
|
-
// Clean up global styles
|
|
4712
|
+
|
|
4574
4713
|
document.body.style.userSelect = '';
|
|
4575
4714
|
document.body.style.webkitUserSelect = '';
|
|
4576
4715
|
document.body.style.mozUserSelect = '';
|
|
4577
4716
|
document.body.style.msUserSelect = '';
|
|
4578
|
-
|
|
4579
|
-
// Set isResizing to false BEFORE making changes
|
|
4717
|
+
|
|
4580
4718
|
isResizing = false;
|
|
4581
|
-
|
|
4582
|
-
// Apply updated metadata to ALL rows in the table (not just the resized row)
|
|
4719
|
+
|
|
4583
4720
|
editorInfo.ace_callWithAce((ace) => {
|
|
4584
4721
|
const callWithAceLogPrefix = `${funcName}[ace_callWithAce]`;
|
|
4585
4722
|
// log(`${callWithAceLogPrefix}: Finding and updating all table rows with tblId: ${resizeTableMetadata.tblId}`);
|
|
4586
|
-
|
|
4723
|
+
|
|
4587
4724
|
try {
|
|
4588
4725
|
const rep = ace.ace_getRep();
|
|
4589
4726
|
if (!rep || !rep.lines) {
|
|
4590
4727
|
console.error(`${callWithAceLogPrefix}: Invalid rep`);
|
|
4591
4728
|
return;
|
|
4592
4729
|
}
|
|
4593
|
-
|
|
4594
|
-
// Find all lines that belong to this table
|
|
4730
|
+
|
|
4595
4731
|
const tableLines = [];
|
|
4596
4732
|
const totalLines = rep.lines.length();
|
|
4597
|
-
|
|
4733
|
+
|
|
4598
4734
|
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
|
|
4599
4735
|
try {
|
|
4600
|
-
// Get line metadata to check if it belongs to our table
|
|
4601
4736
|
let lineAttrString = docManager.getAttributeOnLine(lineIndex, ATTR_TABLE_JSON);
|
|
4602
|
-
|
|
4737
|
+
|
|
4603
4738
|
if (lineAttrString) {
|
|
4604
4739
|
const lineMetadata = JSON.parse(lineAttrString);
|
|
4605
4740
|
if (lineMetadata.tblId === resizeTableMetadata.tblId) {
|
|
@@ -4609,7 +4744,6 @@ const finishColumnResize = (editorInfo, docManager) => {
|
|
|
4609
4744
|
});
|
|
4610
4745
|
}
|
|
4611
4746
|
} else {
|
|
4612
|
-
// Fallback: Check if there's a table in the DOM even though attribute is missing (block styles)
|
|
4613
4747
|
const lineEntry = rep.lines.atIndex(lineIndex);
|
|
4614
4748
|
if (lineEntry && lineEntry.lineNode) {
|
|
4615
4749
|
const tableInDOM = lineEntry.lineNode.querySelector('table.dataTable[data-tblId]');
|
|
@@ -4619,7 +4753,6 @@ const finishColumnResize = (editorInfo, docManager) => {
|
|
|
4619
4753
|
if (domTblId === resizeTableMetadata.tblId && domRow !== null) {
|
|
4620
4754
|
const domCells = tableInDOM.querySelectorAll('td');
|
|
4621
4755
|
if (domCells.length > 0) {
|
|
4622
|
-
// Extract column widths from DOM cells
|
|
4623
4756
|
const columnWidths = [];
|
|
4624
4757
|
domCells.forEach(cell => {
|
|
4625
4758
|
const style = cell.getAttribute('style') || '';
|
|
@@ -4627,12 +4760,10 @@ const finishColumnResize = (editorInfo, docManager) => {
|
|
|
4627
4760
|
if (widthMatch) {
|
|
4628
4761
|
columnWidths.push(parseFloat(widthMatch[1]));
|
|
4629
4762
|
} else {
|
|
4630
|
-
// Fallback to equal distribution if no width found
|
|
4631
4763
|
columnWidths.push(100 / domCells.length);
|
|
4632
4764
|
}
|
|
4633
4765
|
});
|
|
4634
|
-
|
|
4635
|
-
// Reconstruct metadata from DOM with preserved column widths
|
|
4766
|
+
|
|
4636
4767
|
const reconstructedMetadata = {
|
|
4637
4768
|
tblId: domTblId,
|
|
4638
4769
|
row: parseInt(domRow, 10),
|
|
@@ -4650,47 +4781,43 @@ const finishColumnResize = (editorInfo, docManager) => {
|
|
|
4650
4781
|
}
|
|
4651
4782
|
}
|
|
4652
4783
|
} catch (e) {
|
|
4653
|
-
continue;
|
|
4784
|
+
continue;
|
|
4654
4785
|
}
|
|
4655
4786
|
}
|
|
4656
|
-
|
|
4787
|
+
|
|
4657
4788
|
// log(`${callWithAceLogPrefix}: Found ${tableLines.length} table lines to update`);
|
|
4658
|
-
|
|
4659
|
-
// Update all table lines with new column widths
|
|
4789
|
+
|
|
4660
4790
|
for (const tableLine of tableLines) {
|
|
4661
4791
|
const updatedMetadata = { ...tableLine.metadata, columnWidths: finalWidths };
|
|
4662
4792
|
const updatedMetadataString = JSON.stringify(updatedMetadata);
|
|
4663
|
-
|
|
4664
|
-
// Get the full line range for this table line
|
|
4793
|
+
|
|
4665
4794
|
const lineEntry = rep.lines.atIndex(tableLine.lineIndex);
|
|
4666
4795
|
if (!lineEntry) {
|
|
4667
4796
|
console.error(`${callWithAceLogPrefix}: Could not get line entry for line ${tableLine.lineIndex}`);
|
|
4668
4797
|
continue;
|
|
4669
4798
|
}
|
|
4670
|
-
|
|
4799
|
+
|
|
4671
4800
|
const lineLength = Math.max(1, lineEntry.text.length);
|
|
4672
4801
|
const rangeStart = [tableLine.lineIndex, 0];
|
|
4673
4802
|
const rangeEnd = [tableLine.lineIndex, lineLength];
|
|
4674
|
-
|
|
4803
|
+
|
|
4675
4804
|
// log(`${callWithAceLogPrefix}: Updating line ${tableLine.lineIndex} (row ${tableLine.metadata.row}) with new column widths`);
|
|
4676
|
-
|
|
4677
|
-
// Apply the updated metadata attribute directly
|
|
4805
|
+
|
|
4678
4806
|
ace.ace_performDocumentApplyAttributesToRange(rangeStart, rangeEnd, [
|
|
4679
4807
|
[ATTR_TABLE_JSON, updatedMetadataString]
|
|
4680
4808
|
]);
|
|
4681
4809
|
}
|
|
4682
|
-
|
|
4810
|
+
|
|
4683
4811
|
// log(`${callWithAceLogPrefix}: Successfully applied updated column widths to all ${tableLines.length} table rows`);
|
|
4684
|
-
|
|
4812
|
+
|
|
4685
4813
|
} catch (error) {
|
|
4686
4814
|
console.error(`${callWithAceLogPrefix}: Error applying updated metadata:`, error);
|
|
4687
4815
|
// log(`${callWithAceLogPrefix}: Error details:`, { message: error.message, stack: error.stack });
|
|
4688
4816
|
}
|
|
4689
4817
|
}, 'applyTableResizeToAllRows', true);
|
|
4690
|
-
|
|
4818
|
+
|
|
4691
4819
|
// log(`${funcName}: Column width update initiated for all table rows via ace_callWithAce`);
|
|
4692
|
-
|
|
4693
|
-
// Reset state
|
|
4820
|
+
|
|
4694
4821
|
resizeStartX = 0;
|
|
4695
4822
|
resizeCurrentX = 0;
|
|
4696
4823
|
resizeTargetTable = null;
|
|
@@ -4698,11 +4825,10 @@ const finishColumnResize = (editorInfo, docManager) => {
|
|
|
4698
4825
|
resizeOriginalWidths = [];
|
|
4699
4826
|
resizeTableMetadata = null;
|
|
4700
4827
|
resizeLineNum = -1;
|
|
4701
|
-
|
|
4828
|
+
|
|
4702
4829
|
// log(`${funcName}: Resize complete - state reset`);
|
|
4703
4830
|
};
|
|
4704
4831
|
|
|
4705
|
-
// NEW: Undo/Redo protection
|
|
4706
4832
|
exports.aceUndoRedo = (hook, ctx) => {
|
|
4707
4833
|
const logPrefix = '[ep_data_tables:aceUndoRedo]';
|
|
4708
4834
|
// log(`${logPrefix} START`, { hook, ctx });
|
|
@@ -4712,11 +4838,9 @@ exports.aceUndoRedo = (hook, ctx) => {
|
|
|
4712
4838
|
return;
|
|
4713
4839
|
}
|
|
4714
4840
|
|
|
4715
|
-
// Get the affected line range
|
|
4716
4841
|
const startLine = ctx.rep.selStart[0];
|
|
4717
4842
|
const endLine = ctx.rep.selEnd[0];
|
|
4718
4843
|
|
|
4719
|
-
// Check if any affected lines are table lines
|
|
4720
4844
|
let hasTableLines = false;
|
|
4721
4845
|
let tableLines = [];
|
|
4722
4846
|
|
|
@@ -4735,7 +4859,6 @@ exports.aceUndoRedo = (hook, ctx) => {
|
|
|
4735
4859
|
|
|
4736
4860
|
// log(`${logPrefix} Table lines affected:`, { tableLines });
|
|
4737
4861
|
|
|
4738
|
-
// Validate table structure after undo/redo
|
|
4739
4862
|
try {
|
|
4740
4863
|
for (const line of tableLines) {
|
|
4741
4864
|
const lineAttrString = ctx.documentAttributeManager?.getAttributeOnLine(line, ATTR_TABLE_JSON);
|
|
@@ -4744,23 +4867,19 @@ exports.aceUndoRedo = (hook, ctx) => {
|
|
|
4744
4867
|
const tableMetadata = JSON.parse(lineAttrString);
|
|
4745
4868
|
if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
|
|
4746
4869
|
// log(`${logPrefix} Invalid table metadata after undo/redo. Attempting recovery.`);
|
|
4747
|
-
// Attempt to recover table structure
|
|
4748
4870
|
const lineText = ctx.rep.lines.atIndex(line)?.text || '';
|
|
4749
4871
|
const cells = lineText.split(DELIMITER);
|
|
4750
|
-
|
|
4751
|
-
// If we have valid cells, try to reconstruct the table metadata
|
|
4872
|
+
|
|
4752
4873
|
if (cells.length > 1) {
|
|
4753
4874
|
const newMetadata = {
|
|
4754
4875
|
cols: cells.length,
|
|
4755
4876
|
rows: 1,
|
|
4756
4877
|
cells: cells.map((_, i) => ({ col: i, row: 0 }))
|
|
4757
4878
|
};
|
|
4758
|
-
|
|
4759
|
-
// Apply the recovered metadata
|
|
4879
|
+
|
|
4760
4880
|
ctx.documentAttributeManager.setAttributeOnLine(line, ATTR_TABLE_JSON, JSON.stringify(newMetadata));
|
|
4761
4881
|
// log(`${logPrefix} Recovered table structure for line ${line}`);
|
|
4762
4882
|
} else {
|
|
4763
|
-
// If we can't recover, remove the table attribute
|
|
4764
4883
|
ctx.documentAttributeManager.removeAttributeOnLine(line, ATTR_TABLE_JSON);
|
|
4765
4884
|
// log(`${logPrefix} Removed invalid table attribute from line ${line}`);
|
|
4766
4885
|
}
|
|
@@ -4770,4 +4889,4 @@ exports.aceUndoRedo = (hook, ctx) => {
|
|
|
4770
4889
|
console.error(`${logPrefix} Error during undo/redo validation:`, e);
|
|
4771
4890
|
// log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
|
|
4772
4891
|
}
|
|
4773
|
-
};
|
|
4892
|
+
};
|