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