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