ep_data_tables 0.0.2 → 0.0.3

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.
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ // Ensure settings.toolbar exists early to avoid load-order error with ep_font_color
4
+ const settings = require('ep_etherpad-lite/node/utils/Settings');
5
+ if (!settings.toolbar) settings.toolbar = {};
6
+
3
7
  // Using console.log for server-side logging, similar to other Etherpad server-side files.
4
8
  const log = (...m) => console.log('[ep_data_tables:collectContentPre_SERVER]', ...m);
5
9
 
package/ep.json CHANGED
@@ -10,8 +10,6 @@
10
10
  },
11
11
  "client_hooks": {
12
12
  "collectContentPre" : "ep_data_tables/static/js/client_hooks:collectContentPre",
13
- "collectContentLineBreak": "ep_data_tables/static/js/collector_hooks:collectContentLineBreak",
14
- "collectContentLineText" : "ep_data_tables/static/js/collector_hooks:collectContentLineText",
15
13
  "aceKeyEvent" : "ep_data_tables/static/js/client_hooks:aceKeyEvent",
16
14
  "aceStartLineAndCharForPoint" : "ep_data_tables/static/js/client_hooks:aceStartLineAndCharForPoint",
17
15
  "aceEndLineAndCharForPoint" : "ep_data_tables/static/js/client_hooks:aceEndLineAndCharForPoint",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_data_tables",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "BETA - etherpad tables plugin, compatible with other character/line based styling and other features",
5
5
  "author": {
6
6
  "name": "DCastelone",
@@ -472,6 +472,12 @@ exports.collectContentPre = (hook, ctx) => {
472
472
  // the span would be mistaken for a real cell boundary.
473
473
  const hiddenDelimRegexPrimary = /<span class="ep-data_tables-delim"[^>]*>.*?<\/span>/ig;
474
474
  segmentHTML = segmentHTML.replace(hiddenDelimRegexPrimary, '');
475
+ // Remove caret-anchor spans (invisible, non-semantic)
476
+ const caretAnchorRegex = /<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig;
477
+ segmentHTML = segmentHTML.replace(caretAnchorRegex, '');
478
+ // If, after stripping tags/entities, the content is empty, serialize as empty string
479
+ const textCheck = segmentHTML.replace(/<[^>]*>/g, '').replace(/&nbsp;/ig, ' ').trim();
480
+ if (textCheck === '') segmentHTML = '';
475
481
 
476
482
  const hidden = index === 0 ? '' :
477
483
  /* keep the char in the DOM but make it visually disappear and non-editable */
@@ -482,7 +488,7 @@ exports.collectContentPre = (hook, ctx) => {
482
488
 
483
489
  if (cellHTMLSegments.length !== existingMetadata.cols) {
484
490
  // 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('&nbsp;');
491
+ while (cellHTMLSegments.length < existingMetadata.cols) cellHTMLSegments.push('');
486
492
  if (cellHTMLSegments.length > existingMetadata.cols) cellHTMLSegments.length = existingMetadata.cols;
487
493
  }
488
494
 
@@ -520,15 +526,21 @@ exports.collectContentPre = (hook, ctx) => {
520
526
  const resizeHandleRegex = /<div class="ep-data_tables-resize-handle"[^>]*><\/div>/ig;
521
527
  segmentHTML = segmentHTML.replace(resizeHandleRegex, '');
522
528
  if (index > 0) {
523
- const hiddenDelimRegex = new RegExp(`^<span class="ep-data_tables-delim" contenteditable="false">${DELIMITER}(<\\/span>)?<\\/span>`, 'i');
529
+ const hiddenDelimRegex = new RegExp('^<span class="ep-data_tables-delim" contenteditable="false">' + DELIMITER + '(<\\/span>)?<\\/span>', 'i');
524
530
  segmentHTML = segmentHTML.replace(hiddenDelimRegex, '');
525
531
  }
532
+ // Remove caret-anchor spans (invisible, non-semantic)
533
+ const caretAnchorRegex = /<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig;
534
+ segmentHTML = segmentHTML.replace(caretAnchorRegex, '');
535
+ // If, after stripping tags/entities, the content is empty, serialize as empty string
536
+ const textCheck = segmentHTML.replace(/<[^>]*>/g, '').replace(/&nbsp;/ig, ' ').trim();
537
+ if (textCheck === '') segmentHTML = '';
526
538
  return segmentHTML;
527
539
  });
528
540
 
529
541
  if (cellHTMLSegments.length !== domCols) {
530
542
  // log(`${funcName}: WARNING Line ${lineNum} (Fallback): Reconstructed cell count (${cellHTMLSegments.length}) does not match DOM cols (${domCols}).`);
531
- while(cellHTMLSegments.length < domCols) cellHTMLSegments.push('&nbsp;');
543
+ while(cellHTMLSegments.length < domCols) cellHTMLSegments.push('');
532
544
  if(cellHTMLSegments.length > domCols) cellHTMLSegments.length = domCols;
533
545
  }
534
546
 
@@ -672,24 +684,41 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
672
684
  // Basic styling - can be moved to CSS later
673
685
  const tdStyle = `padding: 5px 7px; word-wrap:break-word; vertical-align: top; border: 1px solid #000; position: relative;`; // Added position: relative
674
686
 
687
+ // Precompute encoded tbljson class so empty cells can carry the same marker
688
+ let encodedTbljsonClass = '';
689
+ try {
690
+ encodedTbljsonClass = `tbljson-${enc(JSON.stringify(metadata))}`;
691
+ } catch (_) { encodedTbljsonClass = ''; }
692
+
675
693
  // Map the HTML segments directly into TD elements with column widths
676
694
  const cellsHtml = innerHTMLSegments.map((segment, index) => {
677
695
  // Build the hidden delimiter *inside* the first author span so the caret
678
- // cannot sit between delimiter and text.
679
- let modifiedSegment = segment || '&nbsp;';
696
+ // cannot sit between delimiter and text. For empty cells, synthesize a span
697
+ // that carries tbljson and tblCell-N so caret anchoring remains stable.
698
+ const textOnly = (segment || '').replace(/<[^>]*>/g, '').replace(/&nbsp;/ig, ' ').trim();
699
+ let modifiedSegment = segment || '';
700
+ const isEmpty = !segment || textOnly === '';
701
+ if (isEmpty) {
702
+ const cellClass = encodedTbljsonClass ? `${encodedTbljsonClass} tblCell-${index}` : `tblCell-${index}`;
703
+ modifiedSegment = `<span class="${cellClass}">&nbsp;</span>`;
704
+ }
680
705
  if (index > 0) {
681
706
  const delimSpan = `<span class="ep-data_tables-delim" contenteditable="false">${HIDDEN_DELIM}</span>`;
682
707
  // If the rendered segment already starts with a <span …> (which will be
683
708
  // the usual author-colour wrapper) inject the delimiter right after that
684
709
  // opening tag; otherwise just prefix it.
685
710
  modifiedSegment = modifiedSegment.replace(/^(<span[^>]*>)/i, `$1${delimSpan}`);
686
- if (modifiedSegment === segment) modifiedSegment = `${delimSpan}${modifiedSegment}`;
711
+ if (!/^<span[^>]*>/i.test(modifiedSegment)) modifiedSegment = `${delimSpan}${modifiedSegment}`;
687
712
  }
688
713
 
689
- // --- NEW: Always embed the invisible caret-anchor as *last* child *within* the first author span ---
714
+ // --- NEW: Always embed the invisible caret-anchor as *last* child *within* the first span ---
690
715
  const caretAnchorSpan = '<span class="ep-data_tables-caret-anchor" contenteditable="false"></span>';
691
716
  const anchorInjected = modifiedSegment.replace(/<\/span>\s*$/i, `${caretAnchorSpan}</span>`);
692
- modifiedSegment = (anchorInjected !== modifiedSegment) ? anchorInjected : `${modifiedSegment}${caretAnchorSpan}`;
717
+ modifiedSegment = (anchorInjected !== modifiedSegment)
718
+ ? anchorInjected
719
+ : (isEmpty
720
+ ? `<span class="${encodedTbljsonClass ? `${encodedTbljsonClass} ` : ''}tblCell-${index}">${modifiedSegment}${caretAnchorSpan}</span>`
721
+ : `${modifiedSegment}${caretAnchorSpan}`);
693
722
 
694
723
  // Width & other decorations remain unchanged
695
724
  const widthPercent = columnWidths[index] || (100 / numCols);
@@ -922,7 +951,10 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
922
951
  // the embedded delimiter character they carry doesn't inflate or shrink
923
952
  // the segment count.
924
953
  const spanDelimRegex = new RegExp('<span class="ep-data_tables-delim"[^>]*>' + DELIMITER + '<\\/span>', 'ig');
925
- const sanitizedHTMLForSplit = (delimitedTextFromLine || '').replace(spanDelimRegex, '');
954
+ const sanitizedHTMLForSplit = (delimitedTextFromLine || '')
955
+ .replace(spanDelimRegex, '')
956
+ // strip caret anchors from raw line html before split
957
+ .replace(/<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig, '');
926
958
  const htmlSegments = sanitizedHTMLForSplit.split(DELIMITER);
927
959
 
928
960
  // log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT ANALYSIS ***`);
@@ -949,57 +981,83 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
949
981
  let finalHtmlSegments = htmlSegments;
950
982
 
951
983
  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}`);
984
+ // log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH DETECTED *** - Attempting reconstruction.`);
962
985
 
963
986
  // Check if this is an image selection issue
964
987
  const hasImageSelected = delimitedTextFromLine.includes('currently-selected');
965
988
  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
989
  if (hasImageSelected) {
970
990
  // log(`${logPrefix} NodeID#${nodeId}: *** POTENTIAL CAUSE: Image selection state may be affecting segment parsing ***`);
971
991
  }
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('&nbsp;');
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] || '&nbsp;');
992
+
993
+ // First attempt: reconstruct using DOM spans that carry tblCell-N classes
994
+ let usedClassReconstruction = false;
995
+ try {
996
+ const cols = Math.max(0, Number(rowMetadata.cols) || 0);
997
+ const grouped = Array.from({ length: cols }, () => '');
998
+ const candidates = Array.from(node.querySelectorAll('[class*="tblCell-"]'));
999
+
1000
+ const classNum = (el) => {
1001
+ if (!el || !el.classList) return -1;
1002
+ for (const cls of el.classList) {
1003
+ const m = /^tblCell-(\d+)$/.exec(cls);
1004
+ if (m) return parseInt(m[1], 10);
1005
+ }
1006
+ return -1;
1007
+ };
1008
+ const hasAncestorWithSameCell = (el, n) => {
1009
+ let p = el?.parentElement;
1010
+ while (p) {
1011
+ if (p.classList && p.classList.contains(`tblCell-${n}`)) return true;
1012
+ p = p.parentElement;
1013
+ }
1014
+ return false;
1015
+ };
1016
+
1017
+ for (const el of candidates) {
1018
+ const n = classNum(el);
1019
+ if (n >= 0 && n < cols) {
1020
+ if (!hasAncestorWithSameCell(el, n)) {
1021
+ grouped[n] += el.outerHTML || '';
1022
+ }
1023
+ }
1024
+ }
1025
+ const usable = grouped.some(s => s && s.trim() !== '');
1026
+ if (usable) {
1027
+ finalHtmlSegments = grouped.map(s => (s && s.trim() !== '') ? s : '&nbsp;');
1028
+ usedClassReconstruction = true;
1029
+ console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Reconstructed ${finalHtmlSegments.length} segments from tblCell-N classes.`);
1030
+ }
1031
+ } catch (e) {
1032
+ console.debug(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Class-based reconstruction error; falling back.`, e);
988
1033
  }
989
- // Merge remaining segments into last column
1034
+
1035
+ // Fallback: reconstruct from string segments
1036
+ if (!usedClassReconstruction) {
1037
+ const reconstructedSegments = [];
1038
+ if (htmlSegments.length === 1 && rowMetadata.cols > 1) {
1039
+ reconstructedSegments.push(htmlSegments[0]);
1040
+ for (let i = 1; i < rowMetadata.cols; i++) {
1041
+ reconstructedSegments.push('&nbsp;');
1042
+ }
1043
+ } else if (htmlSegments.length > rowMetadata.cols) {
1044
+ for (let i = 0; i < rowMetadata.cols - 1; i++) {
1045
+ reconstructedSegments.push(htmlSegments[i] || '&nbsp;');
1046
+ }
990
1047
  const remainingSegments = htmlSegments.slice(rowMetadata.cols - 1);
991
1048
  reconstructedSegments.push(remainingSegments.join('|') || '&nbsp;');
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] || '&nbsp;');
1049
+ } else {
1050
+ for (let i = 0; i < rowMetadata.cols; i++) {
1051
+ reconstructedSegments.push(htmlSegments[i] || '&nbsp;');
997
1052
  }
1053
+ }
1054
+ finalHtmlSegments = reconstructedSegments;
1055
+ }
1056
+
1057
+ // Only warn if we still don't have the right number of segments
1058
+ if (finalHtmlSegments.length !== rowMetadata.cols) {
1059
+ console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Could not reconstruct to expected ${rowMetadata.cols} segments. Got ${finalHtmlSegments.length}.`);
998
1060
  }
999
-
1000
- // log(`${logPrefix} NodeID#${nodeId}: Reconstructed ${reconstructedSegments.length} segments to match expected ${rowMetadata.cols} columns.`);
1001
- finalHtmlSegments = reconstructedSegments;
1002
-
1003
1061
  } else {
1004
1062
  // log(`${logPrefix} NodeID#${nodeId}: Segment count matches metadata cols (${rowMetadata.cols}). Using original segments.`);
1005
1063
  }
@@ -1533,12 +1591,12 @@ exports.aceKeyEvent = (h, ctx) => {
1533
1591
  // --- Check for Ctrl+X (Cut) key combination ---
1534
1592
  const isCutKey = (evt.ctrlKey || evt.metaKey) && (evt.key === 'x' || evt.key === 'X' || evt.keyCode === 88);
1535
1593
  if (isCutKey && hasSelection) {
1536
- // log(`${logPrefix} Ctrl+X (Cut) detected with selection. Letting cut event handler manage this.`);
1594
+ log(`${logPrefix} Ctrl+X (Cut) detected with selection. Letting cut event handler manage this.`);
1537
1595
  // Let the cut event handler handle this - we don't need to preventDefault here
1538
1596
  // as the cut event will handle the operation and prevent default
1539
1597
  return false; // Allow the cut event to be triggered
1540
1598
  } else if (isCutKey && !hasSelection) {
1541
- // log(`${logPrefix} Ctrl+X (Cut) detected but no selection. Allowing default.`);
1599
+ log(`${logPrefix} Ctrl+X (Cut) detected but no selection. Allowing default.`);
1542
1600
  return false; // Allow default - nothing to cut
1543
1601
  }
1544
1602
 
@@ -1892,38 +1950,37 @@ exports.aceInitialized = (h, ctx) => {
1892
1950
  }
1893
1951
 
1894
1952
  // *** CUT EVENT LISTENER ***
1895
- // log(`${callWithAceLogPrefix} Attaching cut event listener to $inner (inner iframe body).`);
1953
+ log(`${callWithAceLogPrefix} Attaching cut event listener to $inner (inner iframe body).`);
1896
1954
  $inner.on('cut', (evt) => {
1897
1955
  const cutLogPrefix = '[ep_data_tables:cutHandler]';
1898
- // log(`${cutLogPrefix} CUT EVENT TRIGGERED. Event object:`, evt);
1956
+ console.log(`${cutLogPrefix} CUT EVENT TRIGGERED. Event object:`, evt);
1899
1957
 
1900
- // log(`${cutLogPrefix} Getting current editor representation (rep).`);
1958
+ console.log(`${cutLogPrefix} Getting current editor representation (rep).`);
1901
1959
  const rep = ed.ace_getRep();
1902
1960
  if (!rep || !rep.selStart) {
1903
- // log(`${cutLogPrefix} WARNING: Could not get representation or selection. Allowing default cut.`);
1961
+ console.warn(`${cutLogPrefix} WARNING: Could not get representation or selection. Allowing default cut.`);
1904
1962
  console.warn(`${cutLogPrefix} Could not get rep or selStart.`);
1905
1963
  return; // Allow default
1906
1964
  }
1907
- // log(`${cutLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
1965
+ console.log(`${cutLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
1908
1966
  const selStart = rep.selStart;
1909
1967
  const selEnd = rep.selEnd;
1910
1968
  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
1969
+ console.log(`${cutLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
1970
+ // Determine if there is a selection in the editor representation
1971
+ const hasSelectionInRep = !(selStart[0] === selEnd[0] && selStart[1] === selEnd[1]);
1972
+ if (!hasSelectionInRep) {
1973
+ console.log(`${cutLogPrefix} No selection detected in rep; deferring decision until table-line check.`);
1917
1974
  }
1918
1975
 
1919
1976
  // Check if selection spans multiple lines
1920
1977
  if (selStart[0] !== selEnd[0]) {
1921
- // log(`${cutLogPrefix} WARNING: Selection spans multiple lines. Preventing cut to protect table structure.`);
1978
+ console.warn(`${cutLogPrefix} WARNING: Selection spans multiple lines. Preventing cut to protect table structure.`);
1922
1979
  evt.preventDefault();
1923
1980
  return;
1924
1981
  }
1925
1982
 
1926
- // log(`${cutLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
1983
+ console.log(`${cutLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
1927
1984
  let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
1928
1985
  let tableMetadata = null;
1929
1986
 
@@ -1940,11 +1997,18 @@ exports.aceInitialized = (h, ctx) => {
1940
1997
  }
1941
1998
 
1942
1999
  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.`);
2000
+ console.log(`${cutLogPrefix} Line ${lineNum} is NOT a recognised table line. Allowing default cut.`);
1944
2001
  return; // Not a table line
1945
2002
  }
1946
2003
 
1947
- // log(`${cutLogPrefix} Line ${lineNum} IS a table line. Metadata:`, tableMetadata);
2004
+ console.log(`${cutLogPrefix} Line ${lineNum} IS a table line. Metadata:`, tableMetadata);
2005
+
2006
+ // If inside a table line but the rep shows no selection, prevent default to protect structure
2007
+ if (!hasSelectionInRep) {
2008
+ console.log(`${cutLogPrefix} Preventing default CUT on table line with collapsed selection to protect delimiters.`);
2009
+ evt.preventDefault();
2010
+ return;
2011
+ }
1948
2012
 
1949
2013
  // Validate selection is within cell boundaries
1950
2014
  const lineText = rep.lines.atIndex(lineNum)?.text || '';
@@ -1994,37 +2058,37 @@ exports.aceInitialized = (h, ctx) => {
1994
2058
  selEnd[1] = cellEndCol; // clamp
1995
2059
  }
1996
2060
  if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
1997
- // log(`${cutLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing cut to protect table structure.`);
2061
+ console.warn(`${cutLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing cut to protect table structure.`);
1998
2062
  evt.preventDefault();
1999
2063
  return;
2000
2064
  }
2001
2065
 
2002
2066
  // 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.`);
2067
+ console.log(`${cutLogPrefix} Selection is entirely within cell ${targetCellIndex}. Intercepting cut to preserve table structure.`);
2004
2068
  evt.preventDefault();
2005
2069
 
2006
2070
  try {
2007
2071
  // Get the selected text to copy to clipboard
2008
2072
  const selectedText = lineText.substring(selStart[1], selEnd[1]);
2009
- // log(`${cutLogPrefix} Selected text to cut: "${selectedText}"`);
2073
+ console.log(`${cutLogPrefix} Selected text to cut: "${selectedText}"`);
2010
2074
 
2011
2075
  // Copy to clipboard manually
2012
2076
  if (navigator.clipboard && navigator.clipboard.writeText) {
2013
2077
  navigator.clipboard.writeText(selectedText).then(() => {
2014
- // log(`${cutLogPrefix} Successfully copied to clipboard via Navigator API.`);
2078
+ console.log(`${cutLogPrefix} Successfully copied to clipboard via Navigator API.`);
2015
2079
  }).catch((err) => {
2016
2080
  console.warn(`${cutLogPrefix} Failed to copy to clipboard via Navigator API:`, err);
2017
2081
  });
2018
2082
  } else {
2019
2083
  // Fallback for older browsers
2020
- // log(`${cutLogPrefix} Using fallback clipboard method.`);
2084
+ console.log(`${cutLogPrefix} Using fallback clipboard method.`);
2021
2085
  const textArea = document.createElement('textarea');
2022
2086
  textArea.value = selectedText;
2023
2087
  document.body.appendChild(textArea);
2024
2088
  textArea.select();
2025
2089
  try {
2026
2090
  document.execCommand('copy');
2027
- // log(`${cutLogPrefix} Successfully copied to clipboard via execCommand fallback.`);
2091
+ console.log(`${cutLogPrefix} Successfully copied to clipboard via execCommand fallback.`);
2028
2092
  } catch (err) {
2029
2093
  console.warn(`${cutLogPrefix} Failed to copy to clipboard via fallback:`, err);
2030
2094
  }
@@ -2032,14 +2096,14 @@ exports.aceInitialized = (h, ctx) => {
2032
2096
  }
2033
2097
 
2034
2098
  // Now perform the deletion within the cell using ace operations
2035
- // log(`${cutLogPrefix} Performing deletion via ed.ace_callWithAce.`);
2099
+ console.log(`${cutLogPrefix} Performing deletion via ed.ace_callWithAce.`);
2036
2100
  ed.ace_callWithAce((aceInstance) => {
2037
2101
  const callAceLogPrefix = `${cutLogPrefix}[ace_callWithAceOps]`;
2038
- // log(`${callAceLogPrefix} Entered ace_callWithAce for cut operations. selStart:`, selStart, `selEnd:`, selEnd);
2102
+ console.log(`${callAceLogPrefix} Entered ace_callWithAce for cut operations. selStart:`, selStart, `selEnd:`, selEnd);
2039
2103
 
2040
- // log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to delete selected text.`);
2104
+ console.log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to delete selected text.`);
2041
2105
  aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, '');
2042
- // log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
2106
+ console.log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
2043
2107
 
2044
2108
  // --- Ensure cell is not left empty (zero-length) ---
2045
2109
  const repAfterDeletion = aceInstance.ace_getRep();
@@ -2048,7 +2112,7 @@ exports.aceInitialized = (h, ctx) => {
2048
2112
  const cellTextAfterDeletion = cellsAfterDeletion[targetCellIndex] || '';
2049
2113
 
2050
2114
  if (cellTextAfterDeletion.length === 0) {
2051
- // log(`${callAceLogPrefix} Cell ${targetCellIndex} became empty after cut – inserting single space to preserve structure.`);
2115
+ console.log(`${callAceLogPrefix} Cell ${targetCellIndex} became empty after cut – inserting single space to preserve structure.`);
2052
2116
  const insertPos = [lineNum, selStart[1]]; // Start of the now-empty cell
2053
2117
  aceInstance.ace_performDocumentReplaceRange(insertPos, insertPos, ' ');
2054
2118
 
@@ -2060,9 +2124,9 @@ exports.aceInitialized = (h, ctx) => {
2060
2124
  );
2061
2125
  }
2062
2126
 
2063
- // log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
2127
+ console.log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
2064
2128
  const repAfterCut = aceInstance.ace_getRep();
2065
- // log(`${callAceLogPrefix} Fetched rep after cut for applyMeta. Line ${lineNum} text now: "${repAfterCut.lines.atIndex(lineNum).text}"`);
2129
+ console.log(`${callAceLogPrefix} Fetched rep after cut for applyMeta. Line ${lineNum} text now: "${repAfterCut.lines.atIndex(lineNum).text}"`);
2066
2130
 
2067
2131
  ed.ep_data_tables_applyMeta(
2068
2132
  lineNum,
@@ -2074,20 +2138,20 @@ exports.aceInitialized = (h, ctx) => {
2074
2138
  null,
2075
2139
  docManager
2076
2140
  );
2077
- // log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
2141
+ console.log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
2078
2142
 
2079
2143
  const newCaretPos = [lineNum, selStart[1]];
2080
- // log(`${callAceLogPrefix} Setting caret position to: [${newCaretPos}].`);
2144
+ console.log(`${callAceLogPrefix} Setting caret position to: [${newCaretPos}].`);
2081
2145
  aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2082
- // log(`${callAceLogPrefix} Selection change successful.`);
2146
+ console.log(`${callAceLogPrefix} Selection change successful.`);
2083
2147
 
2084
- // log(`${callAceLogPrefix} Cut operations within ace_callWithAce completed successfully.`);
2148
+ console.log(`${callAceLogPrefix} Cut operations within ace_callWithAce completed successfully.`);
2085
2149
  }, 'tableCutTextOperations', true);
2086
2150
 
2087
- // log(`${cutLogPrefix} Cut operation completed successfully.`);
2151
+ console.log(`${cutLogPrefix} Cut operation completed successfully.`);
2088
2152
  } catch (error) {
2089
2153
  console.error(`${cutLogPrefix} ERROR during cut operation:`, error);
2090
- // log(`${cutLogPrefix} Cut operation failed. Error details:`, { message: error.message, stack: error.stack });
2154
+ console.log(`${cutLogPrefix} Cut operation failed. Error details:`, { message: error.message, stack: error.stack });
2091
2155
  }
2092
2156
  });
2093
2157
 
@@ -2281,6 +2345,18 @@ exports.aceInitialized = (h, ctx) => {
2281
2345
  const dropLogPrefix = '[ep_data_tables:dropHandler]';
2282
2346
  // log(`${dropLogPrefix} DROP EVENT TRIGGERED. Event object:`, evt);
2283
2347
 
2348
+ // Block drops directly targeted at a table element regardless of selection state
2349
+ const targetEl = evt.target;
2350
+ if (targetEl && typeof targetEl.closest === 'function' && targetEl.closest('table.dataTable')) {
2351
+ evt.preventDefault();
2352
+ evt.stopPropagation();
2353
+ if (evt.originalEvent && evt.originalEvent.dataTransfer) {
2354
+ try { evt.originalEvent.dataTransfer.dropEffect = 'none'; } catch (_) {}
2355
+ }
2356
+ console.warn('[ep_data_tables] Drop prevented on table to protect structure.');
2357
+ return;
2358
+ }
2359
+
2284
2360
  // log(`${dropLogPrefix} Getting current editor representation (rep).`);
2285
2361
  const rep = ed.ace_getRep();
2286
2362
  if (!rep || !rep.selStart) {
@@ -2314,6 +2390,17 @@ exports.aceInitialized = (h, ctx) => {
2314
2390
  $inner.on('dragover', (evt) => {
2315
2391
  const dragLogPrefix = '[ep_data_tables:dragoverHandler]';
2316
2392
 
2393
+ // If hovering over a table, signal not-allowed and block default
2394
+ const targetEl = evt.target;
2395
+ if (targetEl && typeof targetEl.closest === 'function' && targetEl.closest('table.dataTable')) {
2396
+ if (evt.originalEvent && evt.originalEvent.dataTransfer) {
2397
+ try { evt.originalEvent.dataTransfer.dropEffect = 'none'; } catch (_) {}
2398
+ }
2399
+ evt.preventDefault();
2400
+ evt.stopPropagation();
2401
+ return;
2402
+ }
2403
+
2317
2404
  const rep = ed.ace_getRep();
2318
2405
  if (!rep || !rep.selStart) {
2319
2406
  return; // Allow default
@@ -2337,6 +2424,18 @@ exports.aceInitialized = (h, ctx) => {
2337
2424
  }
2338
2425
  });
2339
2426
 
2427
+ // Guard against dragenter into table areas
2428
+ $inner.on('dragenter', (evt) => {
2429
+ const targetEl = evt.target;
2430
+ if (targetEl && typeof targetEl.closest === 'function' && targetEl.closest('table.dataTable')) {
2431
+ if (evt.originalEvent && evt.originalEvent.dataTransfer) {
2432
+ try { evt.originalEvent.dataTransfer.dropEffect = 'none'; } catch (_) {}
2433
+ }
2434
+ evt.preventDefault();
2435
+ evt.stopPropagation();
2436
+ }
2437
+ });
2438
+
2340
2439
  // *** EXISTING PASTE LISTENER ***
2341
2440
  // log(`${callWithAceLogPrefix} Attaching paste event listener to $inner (inner iframe body).`);
2342
2441
  $inner.on('paste', (evt) => {
@@ -2791,14 +2890,15 @@ exports.aceInitialized = (h, ctx) => {
2791
2890
  // *** DRAG PREVENTION FOR TABLE ELEMENTS ***
2792
2891
  const preventTableDrag = (evt) => {
2793
2892
  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);
2893
+ // Block ANY drag gesture that originates within a table (including spans/text inside cells)
2894
+ const inTable = target && typeof target.closest === 'function' && target.closest('table.dataTable');
2895
+ if (inTable) {
2896
+ // log('[ep_data_tables:dragPrevention] Preventing drag operation originating from inside table');
2800
2897
  evt.preventDefault();
2801
2898
  evt.stopPropagation();
2899
+ if (evt.originalEvent && evt.originalEvent.dataTransfer) {
2900
+ try { evt.originalEvent.dataTransfer.effectAllowed = 'none'; } catch (_) {}
2901
+ }
2802
2902
  return false;
2803
2903
  }
2804
2904
  };
@@ -2808,6 +2908,21 @@ exports.aceInitialized = (h, ctx) => {
2808
2908
  $inner.on('drag', preventTableDrag);
2809
2909
  $inner.on('dragend', preventTableDrag);
2810
2910
  // log(`${callWithAceLogPrefix} Attached drag prevention handlers to inner body`);
2911
+
2912
+ // Attach drag prevention broadly to cover iframe boundaries and the host document
2913
+ if (innerDoc.length > 0) {
2914
+ innerDoc.on('dragstart', preventTableDrag);
2915
+ innerDoc.on('drag', preventTableDrag);
2916
+ innerDoc.on('dragend', preventTableDrag);
2917
+ }
2918
+ if (outerDoc.length > 0) {
2919
+ outerDoc.on('dragstart', preventTableDrag);
2920
+ outerDoc.on('drag', preventTableDrag);
2921
+ outerDoc.on('dragend', preventTableDrag);
2922
+ }
2923
+ $(document).on('dragstart', preventTableDrag);
2924
+ $(document).on('drag', preventTableDrag);
2925
+ $(document).on('dragend', preventTableDrag);
2811
2926
  };
2812
2927
 
2813
2928
  // Setup the global handlers
@@ -4239,203 +4354,4 @@ exports.aceUndoRedo = (hook, ctx) => {
4239
4354
  console.error(`${logPrefix} Error during undo/redo validation:`, e);
4240
4355
  // log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
4241
4356
  }
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
4357
+ };
@@ -1,13 +0,0 @@
1
- const log = (...m) => console.debug('[ep_data_tables:collector_hooks]', ...m);
2
-
3
- exports.collectContentPre = (hook, ctx) => {
4
- return;
5
- };
6
-
7
- exports.collectContentLineBreak = (hook, ctx) => {
8
- return true;
9
- };
10
-
11
- exports.collectContentLineText = (_hook, ctx) => {
12
- return ctx.text || '';
13
- };