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