ep_data_tables 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_data_tables",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "BETA - etherpad tables plugin, compatible with other character/line based styling and other features",
5
5
  "author": {
6
6
  "name": "DCastelone",
@@ -60,6 +60,21 @@ let resizeOriginalWidths = [];
60
60
  let resizeTableMetadata = null;
61
61
  let resizeLineNum = -1;
62
62
  let resizeOverlay = null; // Visual overlay element
63
+ // Android Chrome composition handling state
64
+ let suppressNextBeforeInputInsertTextOnce = false;
65
+ let isAndroidChromeComposition = false;
66
+ let handledCurrentComposition = false;
67
+ // Suppress all beforeinput insertText events during an Android Chrome IME composition
68
+ let suppressBeforeInputInsertTextDuringComposition = false;
69
+ // Helper to detect Android Chromium-family browsers (exclude iOS and Firefox)
70
+ function isAndroidChromiumUA() {
71
+ const ua = (navigator.userAgent || '').toLowerCase();
72
+ const isAndroid = ua.includes('android');
73
+ const isIOS = ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod') || ua.includes('crios');
74
+ const isFirefox = ua.includes('firefox');
75
+ const isChromiumFamily = ua.includes('chrome') || ua.includes('edg') || ua.includes('opr') || ua.includes('samsungbrowser') || ua.includes('vivaldi') || ua.includes('brave');
76
+ return isAndroid && !isIOS && !isFirefox && isChromiumFamily;
77
+ }
63
78
 
64
79
  // ─────────────────── Reusable Helper Functions ───────────────────
65
80
 
@@ -936,7 +951,7 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
936
951
  const delimiterCount = (delimitedTextFromLine || '').split(DELIMITER).length - 1;
937
952
  // log(`${logPrefix} NodeID#${nodeId}: Delimiter '${DELIMITER}' count in innerHTML: ${delimiterCount}`);
938
953
  // log(`${logPrefix} NodeID#${nodeId}: Expected delimiters for ${rowMetadata.cols} columns: ${rowMetadata.cols - 1}`);
939
-
954
+
940
955
  // log all delimiter positions
941
956
  let pos = -1;
942
957
  const delimiterPositions = [];
@@ -2180,10 +2195,99 @@ exports.aceInitialized = (h, ctx) => {
2180
2195
  const lineNum = selStart[0];
2181
2196
  // log(`${deleteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
2182
2197
 
2183
- // Check if there's actually a selection to delete
2184
- if (selStart[0] === selEnd[0] && selStart[1] === selEnd[1]) {
2185
- // log(`${deleteLogPrefix} No selection to delete. Allowing default delete.`);
2186
- return; // Allow default - nothing to delete
2198
+ // Android Chrome IME: collapsed backspace/forward-delete often comes via beforeinput
2199
+ const isAndroidChrome = isAndroidChromiumUA();
2200
+ const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
2201
+
2202
+ // Handle collapsed deletes on Android Chrome inside a table line to protect delimiters
2203
+ const isCollapsed = (selStart[0] === selEnd[0] && selStart[1] === selEnd[1]);
2204
+ if (isCollapsed && isAndroidChrome && (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward')) {
2205
+ // Resolve metadata for this line
2206
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2207
+ let tableMetadata = null;
2208
+ if (lineAttrString) { try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {} }
2209
+ if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
2210
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
2211
+ return; // Not a table line; allow default
2212
+ }
2213
+
2214
+ // Compute current cell and boundaries
2215
+ const lineText = rep.lines.atIndex(lineNum)?.text || '';
2216
+ const cells = lineText.split(DELIMITER);
2217
+ let currentOffset = 0;
2218
+ let targetCellIndex = -1;
2219
+ let cellStartCol = 0;
2220
+ let cellEndCol = 0;
2221
+ for (let i = 0; i < cells.length; i++) {
2222
+ const cellLength = cells[i]?.length ?? 0;
2223
+ const cellEndColThisIteration = currentOffset + cellLength;
2224
+ if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
2225
+ targetCellIndex = i;
2226
+ cellStartCol = currentOffset;
2227
+ cellEndCol = cellEndColThisIteration;
2228
+ break;
2229
+ }
2230
+ currentOffset += cellLength + DELIMITER.length;
2231
+ }
2232
+
2233
+ if (targetCellIndex === -1) return; // Allow default if not within a cell
2234
+
2235
+ const isBackward = inputType === 'deleteContentBackward';
2236
+ const caretCol = selStart[1];
2237
+
2238
+ // Prevent deletion across delimiters or line boundaries
2239
+ if ((isBackward && caretCol === cellStartCol) || (!isBackward && caretCol === cellEndCol)) {
2240
+ evt.preventDefault();
2241
+ return;
2242
+ }
2243
+
2244
+ // Intercept and perform one-character deletion via Ace
2245
+ evt.preventDefault();
2246
+ try {
2247
+ ed.ace_callWithAce((aceInstance) => {
2248
+ const delStart = isBackward ? [lineNum, caretCol - 1] : [lineNum, caretCol];
2249
+ const delEnd = isBackward ? [lineNum, caretCol] : [lineNum, caretCol + 1];
2250
+ aceInstance.ace_performDocumentReplaceRange(delStart, delEnd, '');
2251
+
2252
+ // Re-apply table metadata attribute to ensure renderer stability
2253
+ const repAfter = aceInstance.ace_getRep();
2254
+ ed.ep_data_tables_applyMeta(
2255
+ lineNum,
2256
+ tableMetadata.tblId,
2257
+ tableMetadata.row,
2258
+ tableMetadata.cols,
2259
+ repAfter,
2260
+ ed,
2261
+ null,
2262
+ docManager
2263
+ );
2264
+
2265
+ // Set caret position after deletion
2266
+ const newCaretCol = isBackward ? caretCol - 1 : caretCol;
2267
+ const newCaretPos = [lineNum, newCaretCol];
2268
+ aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2269
+ aceInstance.ace_fastIncorp(10);
2270
+
2271
+ // Update last-clicked state if available
2272
+ if (ed.ep_data_tables_editor && ed.ep_data_tables_editor.ep_data_tables_last_clicked && ed.ep_data_tables_editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
2273
+ const newRelativePos = newCaretCol - cellStartCol;
2274
+ ed.ep_data_tables_editor.ep_data_tables_last_clicked = {
2275
+ lineNum: lineNum,
2276
+ tblId: tableMetadata.tblId,
2277
+ cellIndex: targetCellIndex,
2278
+ relativePos: newRelativePos < 0 ? 0 : newRelativePos,
2279
+ };
2280
+ }
2281
+ }, 'tableCollapsedDeleteHandler', true);
2282
+ } catch (error) {
2283
+ console.error(`${deleteLogPrefix} ERROR handling collapsed delete:`, error);
2284
+ }
2285
+ return;
2286
+ }
2287
+
2288
+ // Non-Android or non-collapsed: require an actual selection to handle here; otherwise allow default
2289
+ if (isCollapsed) {
2290
+ return; // Allow default
2187
2291
  }
2188
2292
 
2189
2293
  // Check if selection spans multiple lines
@@ -2337,6 +2441,319 @@ exports.aceInitialized = (h, ctx) => {
2337
2441
  }
2338
2442
  });
2339
2443
 
2444
+ // Android Chrome IME insert handling (targeted fix)
2445
+ $inner.on('beforeinput', (evt) => {
2446
+ const insertLogPrefix = '[ep_data_tables:beforeinputInsertHandler]';
2447
+ const inputType = evt.originalEvent && evt.originalEvent.inputType || '';
2448
+
2449
+ // Only intercept insert types
2450
+ if (!inputType || !inputType.startsWith('insert')) return;
2451
+
2452
+ // Target only Android Chromium-family browsers (exclude iOS and Firefox)
2453
+ if (!isAndroidChromiumUA()) return;
2454
+
2455
+ // Get current selection and ensure we are inside a table line
2456
+ const rep = ed.ace_getRep();
2457
+ if (!rep || !rep.selStart) return;
2458
+ const selStart = rep.selStart;
2459
+ const selEnd = rep.selEnd;
2460
+ const lineNum = selStart[0];
2461
+
2462
+ // Resolve table metadata for this line
2463
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2464
+ let tableMetadata = null;
2465
+ if (lineAttrString) {
2466
+ try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {}
2467
+ }
2468
+ if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
2469
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
2470
+ return; // not a table line
2471
+ }
2472
+
2473
+ // Compute cell boundaries and target cell index
2474
+ const lineText = rep.lines.atIndex(lineNum)?.text || '';
2475
+ const cells = lineText.split(DELIMITER);
2476
+ let currentOffset = 0;
2477
+ let targetCellIndex = -1;
2478
+ let cellStartCol = 0;
2479
+ let cellEndCol = 0;
2480
+ for (let i = 0; i < cells.length; i++) {
2481
+ const cellLength = cells[i]?.length ?? 0;
2482
+ const cellEndColThisIteration = currentOffset + cellLength;
2483
+ if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
2484
+ targetCellIndex = i;
2485
+ cellStartCol = currentOffset;
2486
+ cellEndCol = cellEndColThisIteration;
2487
+ break;
2488
+ }
2489
+ currentOffset += cellLength + DELIMITER.length;
2490
+ }
2491
+
2492
+ // Clamp selection end if it includes a trailing delimiter of the same cell
2493
+ if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
2494
+ selEnd[1] = cellEndCol;
2495
+ }
2496
+
2497
+ // If selection spills outside the cell boundaries, block to protect structure
2498
+ if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
2499
+ evt.preventDefault();
2500
+ return;
2501
+ }
2502
+
2503
+ // Handle line breaks by routing to Enter behavior
2504
+ if (inputType === 'insertParagraph' || inputType === 'insertLineBreak') {
2505
+ evt.preventDefault();
2506
+ try {
2507
+ navigateToCellBelow(lineNum, targetCellIndex, tableMetadata, ed, docManager);
2508
+ } catch (e) { console.error(`${insertLogPrefix} Error navigating on line break:`, e); }
2509
+ return;
2510
+ }
2511
+
2512
+ // If we are in Android Chrome composition flow, suppress all insertText until composition ends
2513
+ if (suppressBeforeInputInsertTextDuringComposition && inputType === 'insertText') {
2514
+ evt.preventDefault();
2515
+ return;
2516
+ }
2517
+
2518
+ // If we already handled one insertion via composition handler, skip once (legacy single-shot)
2519
+ if (suppressNextBeforeInputInsertTextOnce && inputType === 'insertText') {
2520
+ suppressNextBeforeInputInsertTextOnce = false;
2521
+ evt.preventDefault();
2522
+ return;
2523
+ }
2524
+
2525
+ // If composition session is active, let composition handler manage
2526
+ if (isAndroidChromeComposition) return;
2527
+
2528
+ // Only proceed for textual insertions we can retrieve
2529
+ const rawData = evt.originalEvent && typeof evt.originalEvent.data === 'string' ? evt.originalEvent.data : '';
2530
+ // If no data for this insert type, allow default (paste/drop paths are handled elsewhere)
2531
+ if (!rawData) return;
2532
+
2533
+ // Sanitize inserted text: remove our delimiter and zero-width characters
2534
+ let insertedText = rawData
2535
+ .replace(new RegExp(DELIMITER, 'g'), ' ')
2536
+ .replace(/[\u200B\u200C\u200D\uFEFF]/g, '')
2537
+ .replace(/\s+/g, ' ');
2538
+
2539
+ if (insertedText.length === 0) {
2540
+ evt.preventDefault();
2541
+ return;
2542
+ }
2543
+
2544
+ // Intercept the browser default and perform the edit via Ace APIs
2545
+ evt.preventDefault();
2546
+ evt.stopPropagation();
2547
+ if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
2548
+
2549
+ try {
2550
+ // Defer to next tick to avoid racing with browser internal composition state
2551
+ setTimeout(() => {
2552
+ ed.ace_callWithAce((aceInstance) => {
2553
+ aceInstance.ace_fastIncorp(10);
2554
+ const freshRep = aceInstance.ace_getRep();
2555
+ const freshSelStart = freshRep.selStart;
2556
+ const freshSelEnd = freshRep.selEnd;
2557
+
2558
+ // Replace selection with sanitized text
2559
+ aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
2560
+
2561
+ // Re-apply cell attribute to the newly inserted text (clamped to line length)
2562
+ const repAfterReplace = aceInstance.ace_getRep();
2563
+ const freshLineIndex = freshSelStart[0];
2564
+ const freshLineEntry = repAfterReplace.lines.atIndex(freshLineIndex);
2565
+ const maxLen = Math.max(0, (freshLineEntry && freshLineEntry.text) ? freshLineEntry.text.length : 0);
2566
+ const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
2567
+ const endColRaw = startCol + insertedText.length;
2568
+ const endCol = Math.min(endColRaw, maxLen);
2569
+ if (endCol > startCol) {
2570
+ aceInstance.ace_performDocumentApplyAttributesToRange(
2571
+ [freshLineIndex, startCol], [freshLineIndex, endCol], [[ATTR_CELL, String(targetCellIndex)]]
2572
+ );
2573
+ }
2574
+
2575
+ // Re-apply table metadata line attribute
2576
+ ed.ep_data_tables_applyMeta(
2577
+ freshLineIndex,
2578
+ tableMetadata.tblId,
2579
+ tableMetadata.row,
2580
+ tableMetadata.cols,
2581
+ repAfterReplace,
2582
+ ed,
2583
+ null,
2584
+ docManager
2585
+ );
2586
+
2587
+ // Move caret to end of inserted text and update last-click state
2588
+ const newCaretCol = endCol;
2589
+ const newCaretPos = [freshLineIndex, newCaretCol];
2590
+ aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2591
+ aceInstance.ace_fastIncorp(10);
2592
+
2593
+ if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
2594
+ // Recompute cellStartCol for the fresh line to avoid mismatches
2595
+ const freshLineText = (freshLineEntry && freshLineEntry.text) || '';
2596
+ const freshCells = freshLineText.split(DELIMITER);
2597
+ let freshOffset = 0;
2598
+ for (let i = 0; i < targetCellIndex; i++) {
2599
+ freshOffset += (freshCells[i]?.length ?? 0) + DELIMITER.length;
2600
+ }
2601
+ const newRelativePos = newCaretCol - freshOffset;
2602
+ editor.ep_data_tables_last_clicked = {
2603
+ lineNum: freshLineIndex,
2604
+ tblId: tableMetadata.tblId,
2605
+ cellIndex: targetCellIndex,
2606
+ relativePos: newRelativePos < 0 ? 0 : newRelativePos,
2607
+ };
2608
+ }
2609
+ }, 'tableInsertTextOperations', true);
2610
+ }, 0);
2611
+ } catch (error) {
2612
+ console.error(`${insertLogPrefix} ERROR during insert handling:`, error);
2613
+ }
2614
+ });
2615
+
2616
+ // Composition start marker (Android Chrome/table only)
2617
+ $inner.on('compositionstart', (evt) => {
2618
+ if (!isAndroidChromiumUA()) return;
2619
+ const rep = ed.ace_getRep();
2620
+ if (!rep || !rep.selStart) return;
2621
+ const lineNum = rep.selStart[0];
2622
+ let meta = null; let s = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2623
+ if (s) { try { meta = JSON.parse(s); } catch (_) {} }
2624
+ if (!meta) meta = getTableLineMetadata(lineNum, ed, docManager);
2625
+ if (!meta || typeof meta.cols !== 'number') return;
2626
+ isAndroidChromeComposition = true;
2627
+ handledCurrentComposition = false;
2628
+ suppressBeforeInputInsertTextDuringComposition = false;
2629
+ });
2630
+
2631
+ // Android Chrome composition handling for whitespace (space) to prevent DOM mutation breaking delimiters
2632
+ $inner.on('compositionupdate', (evt) => {
2633
+ const compLogPrefix = '[ep_data_tables:compositionHandler]';
2634
+
2635
+ if (!isAndroidChromiumUA()) return;
2636
+
2637
+ const rep = ed.ace_getRep();
2638
+ if (!rep || !rep.selStart) return;
2639
+ const selStart = rep.selStart;
2640
+ const selEnd = rep.selEnd;
2641
+ const lineNum = selStart[0];
2642
+
2643
+ // Ensure we are inside a table line
2644
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2645
+ let tableMetadata = null;
2646
+ if (lineAttrString) { try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {} }
2647
+ if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
2648
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number') return;
2649
+
2650
+ // Only act on whitespace-only composition updates (space/non-breaking space)
2651
+ const d = evt.originalEvent && typeof evt.originalEvent.data === 'string' ? evt.originalEvent.data : '';
2652
+ if (evt.type === 'compositionupdate') {
2653
+ const isWhitespaceOnly = d && d.replace(/\u00A0/g, ' ').trim() === '';
2654
+ if (!isWhitespaceOnly) return;
2655
+
2656
+ // Compute target cell and clamp selection to cell boundaries
2657
+ const lineText = rep.lines.atIndex(lineNum)?.text || '';
2658
+ const cells = lineText.split(DELIMITER);
2659
+ let currentOffset = 0;
2660
+ let targetCellIndex = -1;
2661
+ let cellStartCol = 0;
2662
+ let cellEndCol = 0;
2663
+ for (let i = 0; i < cells.length; i++) {
2664
+ const cellLength = cells[i]?.length ?? 0;
2665
+ const cellEndColThisIteration = currentOffset + cellLength;
2666
+ if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
2667
+ targetCellIndex = i;
2668
+ cellStartCol = currentOffset;
2669
+ cellEndCol = cellEndColThisIteration;
2670
+ break;
2671
+ }
2672
+ currentOffset += cellLength + DELIMITER.length;
2673
+ }
2674
+ if (targetCellIndex === -1 || selEnd[1] > cellEndCol) return;
2675
+
2676
+ // Prevent composition DOM mutation and insert sanitized space via Ace
2677
+ evt.preventDefault();
2678
+ evt.stopPropagation();
2679
+ if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
2680
+
2681
+ let insertedText = d.replace(/\u00A0/g, ' ');
2682
+ if (insertedText.length === 0) insertedText = ' ';
2683
+
2684
+ try {
2685
+ setTimeout(() => {
2686
+ ed.ace_callWithAce((aceInstance) => {
2687
+ aceInstance.ace_fastIncorp(10);
2688
+ const freshRep = aceInstance.ace_getRep();
2689
+ const freshSelStart = freshRep.selStart;
2690
+ const freshSelEnd = freshRep.selEnd;
2691
+ aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
2692
+
2693
+ // Clamp attribute application to current line bounds and use fresh line index
2694
+ const repAfterReplace = aceInstance.ace_getRep();
2695
+ const freshLineIndex = freshSelStart[0];
2696
+ const freshLineEntry = repAfterReplace.lines.atIndex(freshLineIndex);
2697
+ const maxLen = Math.max(0, (freshLineEntry && freshLineEntry.text) ? freshLineEntry.text.length : 0);
2698
+ const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
2699
+ const endColRaw = startCol + insertedText.length;
2700
+ const endCol = Math.min(endColRaw, maxLen);
2701
+ if (endCol > startCol) {
2702
+ aceInstance.ace_performDocumentApplyAttributesToRange(
2703
+ [freshLineIndex, startCol], [freshLineIndex, endCol], [[ATTR_CELL, String(targetCellIndex)]]
2704
+ );
2705
+ }
2706
+
2707
+ ed.ep_data_tables_applyMeta(
2708
+ freshLineIndex,
2709
+ tableMetadata.tblId,
2710
+ tableMetadata.row,
2711
+ tableMetadata.cols,
2712
+ repAfterReplace,
2713
+ ed,
2714
+ null,
2715
+ docManager
2716
+ );
2717
+
2718
+ const newCaretCol = endCol;
2719
+ const newCaretPos = [freshLineIndex, newCaretCol];
2720
+ aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2721
+ aceInstance.ace_fastIncorp(10);
2722
+ if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
2723
+ // Recompute cellStartCol for fresh line
2724
+ const freshLineText = (freshLineEntry && freshLineEntry.text) || '';
2725
+ const freshCells = freshLineText.split(DELIMITER);
2726
+ let freshOffset = 0;
2727
+ for (let i = 0; i < targetCellIndex; i++) {
2728
+ freshOffset += (freshCells[i]?.length ?? 0) + DELIMITER.length;
2729
+ }
2730
+ const newRelativePos = newCaretCol - freshOffset;
2731
+ editor.ep_data_tables_last_clicked = {
2732
+ lineNum: freshLineIndex,
2733
+ tblId: tableMetadata.tblId,
2734
+ cellIndex: targetCellIndex,
2735
+ relativePos: newRelativePos < 0 ? 0 : newRelativePos,
2736
+ };
2737
+ }
2738
+ }, 'tableCompositionSpaceInsert', true);
2739
+ }, 0);
2740
+ // Suppress all subsequent beforeinput insertText events in this composition session
2741
+ suppressBeforeInputInsertTextDuringComposition = true;
2742
+ } catch (error) {
2743
+ console.error(`${compLogPrefix} ERROR inserting space during composition:`, error);
2744
+ }
2745
+ }
2746
+ });
2747
+
2748
+ // Composition end cleanup
2749
+ $inner.on('compositionend', () => {
2750
+ if (isAndroidChromeComposition) {
2751
+ isAndroidChromeComposition = false;
2752
+ handledCurrentComposition = false;
2753
+ suppressBeforeInputInsertTextDuringComposition = false;
2754
+ }
2755
+ });
2756
+
2340
2757
  // *** DRAG AND DROP EVENT LISTENERS ***
2341
2758
  // log(`${callWithAceLogPrefix} Attaching drag and drop event listeners to $inner (inner iframe body).`);
2342
2759