@trustquery/browser 0.2.6 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -764,8 +764,123 @@ class BubbleManager {
764
764
  }
765
765
  }
766
766
 
767
+ // EdgeDetectionHelper - Calculates dropdown positioning to prevent overflow off viewport edges
768
+
769
+ class EdgeDetectionHelper {
770
+ /**
771
+ * Calculate optimal position for dropdown to prevent viewport overflow
772
+ * @param {Object} options - Positioning options
773
+ * @param {DOMRect} options.matchRect - Bounding rect of the match element
774
+ * @param {DOMRect} options.dropdownRect - Bounding rect of the dropdown
775
+ * @param {number} options.offset - Offset from match element (default: 28)
776
+ * @param {number} options.padding - Padding from viewport edges (default: 10)
777
+ * @returns {Object} - { top, left } position in pixels
778
+ */
779
+ static calculatePosition({ matchRect, dropdownRect, offset = 28, padding = 10 }) {
780
+ const viewportWidth = window.innerWidth;
781
+ const viewportHeight = window.innerHeight;
782
+
783
+ console.log('[EdgeDetectionHelper] Calculation inputs:');
784
+ console.log(' Match rect:', { left: matchRect.left, top: matchRect.top, width: matchRect.width });
785
+ console.log(' Dropdown rect:', { width: dropdownRect.width, height: dropdownRect.height });
786
+ console.log(' Viewport:', { width: viewportWidth, height: viewportHeight });
787
+ console.log(' Scroll:', { x: window.scrollX, y: window.scrollY });
788
+
789
+ // Calculate initial position (above match by default, since input is at bottom)
790
+ let top = matchRect.top + window.scrollY - dropdownRect.height - offset;
791
+ let left = matchRect.left + window.scrollX;
792
+
793
+ console.log(' Initial position:', { left, top });
794
+
795
+ // Vertical positioning: Check if dropdown goes off top edge
796
+ if (top < window.scrollY) {
797
+ // Position below match instead
798
+ top = matchRect.bottom + window.scrollY + offset;
799
+ console.log(' Adjusted for top overflow, new top:', top);
800
+ }
801
+
802
+ // Horizontal positioning: Check if dropdown goes off right edge
803
+ const rightEdge = left + dropdownRect.width;
804
+ const viewportRightEdge = viewportWidth + window.scrollX;
805
+
806
+ console.log(' Right edge check:', { rightEdge, viewportRightEdge: viewportRightEdge - padding });
807
+
808
+ if (rightEdge > viewportRightEdge - padding) {
809
+ // Calculate how much we overflow past the right edge
810
+ const overflow = rightEdge - (viewportRightEdge - padding);
811
+ console.log(' Right overflow detected:', overflow, 'px');
812
+
813
+ // Shift left by the overflow amount
814
+ left = left - overflow;
815
+ console.log(' Adjusted left position:', left);
816
+
817
+ // Ensure we don't go off the left edge either
818
+ const minLeft = window.scrollX + padding;
819
+ if (left < minLeft) {
820
+ console.log(' Hit left edge, clamping to:', minLeft);
821
+ left = minLeft;
822
+ }
823
+ }
824
+
825
+ // Also check left edge (in case match is near left edge)
826
+ const minLeft = window.scrollX + padding;
827
+ if (left < minLeft) {
828
+ console.log(' Left edge adjustment:', minLeft);
829
+ left = minLeft;
830
+ }
831
+
832
+ console.log('[EdgeDetectionHelper] Final position:', { left, top });
833
+
834
+ return { top, left };
835
+ }
836
+
837
+ /**
838
+ * Check if dropdown would overflow the right edge
839
+ * @param {DOMRect} matchRect - Match element rect
840
+ * @param {DOMRect} dropdownRect - Dropdown rect
841
+ * @param {number} padding - Padding from edge
842
+ * @returns {boolean} - True if would overflow
843
+ */
844
+ static wouldOverflowRight(matchRect, dropdownRect, padding = 10) {
845
+ const left = matchRect.left + window.scrollX;
846
+ const rightEdge = left + dropdownRect.width;
847
+ const viewportRightEdge = window.innerWidth + window.scrollX;
848
+
849
+ return rightEdge > viewportRightEdge - padding;
850
+ }
851
+
852
+ /**
853
+ * Check if dropdown would overflow the top edge
854
+ * @param {DOMRect} matchRect - Match element rect
855
+ * @param {DOMRect} dropdownRect - Dropdown rect
856
+ * @param {number} offset - Offset from match
857
+ * @returns {boolean} - True if would overflow
858
+ */
859
+ static wouldOverflowTop(matchRect, dropdownRect, offset = 28) {
860
+ const top = matchRect.top + window.scrollY - dropdownRect.height - offset;
861
+ return top < window.scrollY;
862
+ }
863
+
864
+ /**
865
+ * Calculate overflow amount on the right edge
866
+ * @param {DOMRect} matchRect - Match element rect
867
+ * @param {DOMRect} dropdownRect - Dropdown rect
868
+ * @param {number} padding - Padding from edge
869
+ * @returns {number} - Overflow amount in pixels (0 if no overflow)
870
+ */
871
+ static calculateRightOverflow(matchRect, dropdownRect, padding = 10) {
872
+ const left = matchRect.left + window.scrollX;
873
+ const rightEdge = left + dropdownRect.width;
874
+ const viewportRightEdge = window.innerWidth + window.scrollX;
875
+
876
+ const overflow = rightEdge - (viewportRightEdge - padding);
877
+ return Math.max(0, overflow);
878
+ }
879
+ }
880
+
767
881
  // DropdownManager - Handles dropdown menus with filtering, keyboard navigation, and selection
768
882
 
883
+
769
884
  class DropdownManager {
770
885
  /**
771
886
  * Create dropdown manager
@@ -819,9 +934,10 @@ class DropdownManager {
819
934
  const dropdown = document.createElement('div');
820
935
  dropdown.className = 'tq-dropdown';
821
936
 
822
- // Apply inline styles via StyleManager
937
+ // Apply inline styles via StyleManager (pass category for width adjustment)
938
+ const category = matchData.intent?.category || '';
823
939
  if (this.options.styleManager) {
824
- this.options.styleManager.applyDropdownStyles(dropdown);
940
+ this.options.styleManager.applyDropdownStyles(dropdown, category);
825
941
  }
826
942
 
827
943
  // Add header container based on message-state
@@ -1005,6 +1121,10 @@ class DropdownManager {
1005
1121
  * @param {Object} matchData - Match data
1006
1122
  */
1007
1123
  createDropdownItems(dropdown, options, matchData) {
1124
+ // Check if this is a display-menu-with-uri category
1125
+ const category = matchData.intent?.category || '';
1126
+ const hasUriSupport = category === 'display-menu-with-uri';
1127
+
1008
1128
  options.forEach((option, index) => {
1009
1129
  // Check if this is a user-input option
1010
1130
  if (typeof option === 'object' && option['user-input'] === true) {
@@ -1014,7 +1134,82 @@ class DropdownManager {
1014
1134
 
1015
1135
  const item = document.createElement('div');
1016
1136
  item.className = 'tq-dropdown-item';
1017
- item.textContent = typeof option === 'string' ? option : option.label || option.value;
1137
+
1138
+ // Create label text
1139
+ const labelText = typeof option === 'string' ? option : option.label || option.value;
1140
+
1141
+ // If display-menu-with-uri and option has uri, create label + link icon
1142
+ if (hasUriSupport && typeof option === 'object' && option.uri) {
1143
+ // Create label span
1144
+ const labelSpan = document.createElement('span');
1145
+ labelSpan.className = 'tq-dropdown-item-label';
1146
+ labelSpan.textContent = labelText;
1147
+ labelSpan.style.flex = '1';
1148
+ labelSpan.style.cursor = 'pointer';
1149
+
1150
+ // Create link with truncated URI text
1151
+ const linkIcon = document.createElement('a');
1152
+ linkIcon.className = 'tq-dropdown-item-link';
1153
+ linkIcon.href = option.uri;
1154
+ linkIcon.target = '_blank';
1155
+ linkIcon.rel = 'noopener noreferrer';
1156
+
1157
+ // Truncate URI to 20 characters
1158
+ let displayUri = option.uri;
1159
+ if (displayUri.length > 20) {
1160
+ displayUri = displayUri.substring(0, 20) + '...';
1161
+ }
1162
+ linkIcon.textContent = displayUri;
1163
+
1164
+ linkIcon.style.marginLeft = '8px';
1165
+ linkIcon.style.fontSize = '12px';
1166
+ linkIcon.style.color = '#3b82f6';
1167
+ linkIcon.style.textDecoration = 'underline';
1168
+ linkIcon.style.opacity = '0.7';
1169
+ linkIcon.style.transition = 'opacity 0.2s';
1170
+ linkIcon.style.whiteSpace = 'nowrap';
1171
+ linkIcon.title = option.uri;
1172
+ linkIcon.setAttribute('aria-label', `Open ${labelText} documentation`);
1173
+
1174
+ // Hover effect for link icon
1175
+ linkIcon.addEventListener('mouseenter', () => {
1176
+ linkIcon.style.opacity = '1';
1177
+ });
1178
+ linkIcon.addEventListener('mouseleave', () => {
1179
+ linkIcon.style.opacity = '0.6';
1180
+ });
1181
+
1182
+ // Prevent link click from selecting the option
1183
+ linkIcon.addEventListener('click', (e) => {
1184
+ e.stopPropagation();
1185
+ // Link will open naturally via href
1186
+ });
1187
+
1188
+ // Set item to flex layout
1189
+ item.style.display = 'flex';
1190
+ item.style.alignItems = 'center';
1191
+ item.style.justifyContent = 'space-between';
1192
+
1193
+ item.appendChild(labelSpan);
1194
+ item.appendChild(linkIcon);
1195
+
1196
+ // Only label click should select the option
1197
+ labelSpan.addEventListener('click', (e) => {
1198
+ e.stopPropagation();
1199
+ this.handleDropdownSelect(option, matchData);
1200
+ this.hideDropdown();
1201
+ });
1202
+ } else {
1203
+ // Regular option without URI
1204
+ item.textContent = labelText;
1205
+
1206
+ // Click anywhere on item to select
1207
+ item.addEventListener('click', (e) => {
1208
+ e.stopPropagation();
1209
+ this.handleDropdownSelect(option, matchData);
1210
+ this.hideDropdown();
1211
+ });
1212
+ }
1018
1213
 
1019
1214
  // Highlight first item by default
1020
1215
  if (index === 0) {
@@ -1026,11 +1221,6 @@ class DropdownManager {
1026
1221
  this.options.styleManager.applyDropdownItemStyles(item);
1027
1222
  }
1028
1223
 
1029
- item.addEventListener('click', (e) => {
1030
- e.stopPropagation();
1031
- this.handleDropdownSelect(option, matchData);
1032
- this.hideDropdown();
1033
- });
1034
1224
  dropdown.appendChild(item);
1035
1225
  });
1036
1226
  }
@@ -1138,26 +1328,49 @@ class DropdownManager {
1138
1328
  * @param {HTMLElement} matchEl - Match element
1139
1329
  */
1140
1330
  positionDropdown(dropdown, matchEl) {
1141
- const rect = matchEl.getBoundingClientRect();
1331
+ const matchRect = matchEl.getBoundingClientRect();
1332
+
1333
+ // Force a layout calculation by accessing offsetWidth
1334
+ // This ensures we get the correct dropdown dimensions after styles are applied
1335
+ dropdown.offsetWidth;
1336
+
1142
1337
  const dropdownRect = dropdown.getBoundingClientRect();
1143
- const offset = this.options.dropdownOffset;
1144
1338
 
1145
- // Position above match by default (since input is at bottom)
1146
- let top = rect.top + window.scrollY - dropdownRect.height - offset;
1147
- let left = rect.left + window.scrollX;
1339
+ console.log('[DropdownManager] Positioning Debug:');
1340
+ console.log(' Trigger position (x, y):', matchRect.left, matchRect.top);
1341
+ console.log(' Dropdown width:', dropdownRect.width);
1342
+ console.log(' Viewport width:', window.innerWidth);
1343
+
1344
+ // Use EdgeDetectionHelper to calculate optimal position
1345
+ // Increased padding to account for body margins/transforms and ensure visible clearance
1346
+ const position = EdgeDetectionHelper.calculatePosition({
1347
+ matchRect,
1348
+ dropdownRect,
1349
+ offset: this.options.dropdownOffset,
1350
+ padding: 35
1351
+ });
1148
1352
 
1149
- // If dropdown goes off top edge, position below instead
1150
- if (top < window.scrollY) {
1151
- top = rect.bottom + window.scrollY + offset;
1152
- }
1353
+ console.log(' Calculated dropdown position (x, y):', position.left, position.top);
1153
1354
 
1154
- // Check if dropdown goes off right edge
1155
- if (left + dropdownRect.width > window.innerWidth) {
1156
- left = window.innerWidth - dropdownRect.width - 10;
1157
- }
1355
+ dropdown.style.top = `${position.top}px`;
1356
+ dropdown.style.left = `${position.left}px`;
1158
1357
 
1159
- dropdown.style.top = `${top}px`;
1160
- dropdown.style.left = `${left}px`;
1358
+ // Verify the position was actually applied
1359
+ const computedStyle = window.getComputedStyle(dropdown);
1360
+ const actualRect = dropdown.getBoundingClientRect();
1361
+ console.log(' Verified applied styles:');
1362
+ console.log(' styleTop:', dropdown.style.top);
1363
+ console.log(' styleLeft:', dropdown.style.left);
1364
+ console.log(' computedPosition:', computedStyle.position);
1365
+ console.log(' actualBoundingRect:', JSON.stringify({
1366
+ left: actualRect.left,
1367
+ top: actualRect.top,
1368
+ right: actualRect.right,
1369
+ bottom: actualRect.bottom,
1370
+ width: actualRect.width,
1371
+ height: actualRect.height
1372
+ }));
1373
+ console.log(' Dropdown right edge:', actualRect.left + actualRect.width, 'vs viewport width:', window.innerWidth);
1161
1374
  }
1162
1375
 
1163
1376
  /**
@@ -1506,16 +1719,42 @@ class InteractionHandler {
1506
1719
  const behavior = matchEl.getAttribute('data-behavior');
1507
1720
  const matchData = this.getMatchData(matchEl);
1508
1721
 
1509
- console.log('[InteractionHandler] Match clicked:', matchData);
1722
+ console.log('[InteractionHandler] ===== CLICK EVENT START =====');
1723
+ console.log('[InteractionHandler] Click details:', {
1724
+ behavior: behavior,
1725
+ matchText: matchData.text,
1726
+ eventType: e.type,
1727
+ target: e.target.tagName,
1728
+ currentTarget: e.currentTarget.tagName,
1729
+ button: e.button,
1730
+ buttons: e.buttons,
1731
+ clientX: e.clientX,
1732
+ clientY: e.clientY,
1733
+ offsetX: e.offsetX,
1734
+ offsetY: e.offsetY
1735
+ });
1736
+ console.log('[InteractionHandler] Match element:', {
1737
+ textContent: matchEl.textContent,
1738
+ offsetWidth: matchEl.offsetWidth,
1739
+ offsetHeight: matchEl.offsetHeight,
1740
+ attributes: {
1741
+ 'data-line': matchEl.getAttribute('data-line'),
1742
+ 'data-col': matchEl.getAttribute('data-col'),
1743
+ 'data-behavior': behavior
1744
+ }
1745
+ });
1746
+ console.log('[InteractionHandler] Active element before click:', document.activeElement.tagName, document.activeElement.id || '(no id)');
1510
1747
 
1511
1748
  // For non-interactive elements (bubbles), manually pass click to textarea
1512
1749
  if (behavior !== 'dropdown' && behavior !== 'action') {
1750
+ console.log('[InteractionHandler] Non-interactive match - manually focusing textarea');
1513
1751
  e.preventDefault();
1514
1752
  e.stopPropagation();
1515
1753
 
1516
1754
  // Focus textarea and position cursor at click location
1517
1755
  if (this.options.textarea) {
1518
1756
  this.options.textarea.focus();
1757
+ console.log('[InteractionHandler] Textarea focused. Active element now:', document.activeElement.tagName, document.activeElement.id || '(no id)');
1519
1758
 
1520
1759
  // Get the character offset by finding the match position
1521
1760
  const line = parseInt(matchEl.getAttribute('data-line'));
@@ -1527,15 +1766,33 @@ class InteractionHandler {
1527
1766
  for (let i = 0; i < line; i++) {
1528
1767
  offset += lines[i].length + 1; // +1 for newline
1529
1768
  }
1530
- offset += col + (e.offsetX / matchEl.offsetWidth * matchEl.textContent.length);
1769
+ const clickOffsetInMatch = (e.offsetX / matchEl.offsetWidth * matchEl.textContent.length);
1770
+ offset += col + clickOffsetInMatch;
1771
+
1772
+ console.log('[InteractionHandler] Cursor positioning:', {
1773
+ line: line,
1774
+ col: col,
1775
+ clickOffsetInMatch: clickOffsetInMatch,
1776
+ finalOffset: offset,
1777
+ textareaValue: this.options.textarea.value,
1778
+ textareaValueLength: this.options.textarea.value.length
1779
+ });
1531
1780
 
1532
1781
  // Set cursor position
1533
1782
  this.options.textarea.setSelectionRange(offset, offset);
1783
+
1784
+ console.log('[InteractionHandler] Selection set:', {
1785
+ selectionStart: this.options.textarea.selectionStart,
1786
+ selectionEnd: this.options.textarea.selectionEnd,
1787
+ selectedText: this.options.textarea.value.substring(this.options.textarea.selectionStart, this.options.textarea.selectionEnd)
1788
+ });
1534
1789
  }
1535
1790
 
1791
+ console.log('[InteractionHandler] ===== CLICK EVENT END (non-interactive) =====');
1536
1792
  return; // Don't process further for bubbles
1537
1793
  }
1538
1794
 
1795
+ console.log('[InteractionHandler] Interactive match - handling dropdown/action');
1539
1796
  // Prevent default for interactive elements (dropdown/action)
1540
1797
  e.preventDefault();
1541
1798
  e.stopPropagation();
@@ -1559,6 +1816,8 @@ class InteractionHandler {
1559
1816
  if (this.options.onWordClick && !(behavior === 'dropdown' && this.dropdownManager.activeDropdownMatch === matchEl)) {
1560
1817
  this.options.onWordClick(matchData);
1561
1818
  }
1819
+
1820
+ console.log('[InteractionHandler] ===== CLICK EVENT END (interactive) =====');
1562
1821
  }
1563
1822
 
1564
1823
  /**
@@ -1900,8 +2159,12 @@ class StyleManager {
1900
2159
  /**
1901
2160
  * Apply dropdown (menu) styles
1902
2161
  * @param {HTMLElement} dropdown - Dropdown element
2162
+ * @param {string} category - Optional category to adjust styles
1903
2163
  */
1904
- applyDropdownStyles(dropdown) {
2164
+ applyDropdownStyles(dropdown, category = '') {
2165
+ // Use wider maxWidth for display-menu-with-uri to accommodate links
2166
+ const maxWidth = category === 'display-menu-with-uri' ? '450px' : '300px';
2167
+
1905
2168
  Object.assign(dropdown.style, {
1906
2169
  position: 'absolute',
1907
2170
  background: '#ffffff',
@@ -1910,7 +2173,7 @@ class StyleManager {
1910
2173
  boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
1911
2174
  zIndex: '10000',
1912
2175
  minWidth: '150px',
1913
- maxWidth: '300px',
2176
+ maxWidth: maxWidth,
1914
2177
  overflow: 'hidden',
1915
2178
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1916
2179
  fontSize: '14px',
@@ -2169,6 +2432,8 @@ class CommandHandlerRegistry {
2169
2432
  this.register('user-select-oneOf', new UserSelectHandler());
2170
2433
  this.register('api-json-table', new ApiJsonTableHandler());
2171
2434
  this.register('api-md-table', new ApiMdTableHandler());
2435
+ this.register('display-menu', new DisplayMenuHandler());
2436
+ this.register('display-menu-with-uri', new DisplayMenuWithUriHandler());
2172
2437
  }
2173
2438
 
2174
2439
  /**
@@ -2499,6 +2764,48 @@ class ApiMdTableHandler extends CommandHandler {
2499
2764
  }
2500
2765
  }
2501
2766
 
2767
+ /**
2768
+ * Handler for display-menu category
2769
+ * Shows dropdown menu with selectable options
2770
+ */
2771
+ class DisplayMenuHandler extends CommandHandler {
2772
+ getStyles() {
2773
+ return {
2774
+ backgroundColor: 'rgba(16, 185, 129, 0.15)', // Green background
2775
+ color: '#065f46', // Dark green text
2776
+ textDecoration: 'none',
2777
+ borderBottom: '2px solid #10b981', // Green underline
2778
+ cursor: 'pointer'
2779
+ };
2780
+ }
2781
+
2782
+ getBubbleContent(matchData) {
2783
+ // Display menu shows dropdown, not bubble
2784
+ return null;
2785
+ }
2786
+ }
2787
+
2788
+ /**
2789
+ * Handler for display-menu-with-uri category
2790
+ * Shows dropdown menu with selectable options and clickable URI links
2791
+ */
2792
+ class DisplayMenuWithUriHandler extends CommandHandler {
2793
+ getStyles() {
2794
+ return {
2795
+ backgroundColor: 'rgba(16, 185, 129, 0.15)', // Green background
2796
+ color: '#065f46', // Dark green text
2797
+ textDecoration: 'none',
2798
+ borderBottom: '2px solid #10b981', // Green underline
2799
+ cursor: 'pointer'
2800
+ };
2801
+ }
2802
+
2803
+ getBubbleContent(matchData) {
2804
+ // Display menu with URI shows dropdown, not bubble
2805
+ return null;
2806
+ }
2807
+ }
2808
+
2502
2809
  // AutoGrow - Automatically grows textarea height based on content
2503
2810
 
2504
2811
  class AutoGrow {
@@ -2974,12 +3281,121 @@ class TrustQuery {
2974
3281
  this.overlay.scrollLeft = this.textarea.scrollLeft;
2975
3282
  });
2976
3283
 
3284
+ // Keyboard event logging for debugging selection issues
3285
+ this.textarea.addEventListener('keydown', (e) => {
3286
+ const isCmdOrCtrl = e.metaKey || e.ctrlKey;
3287
+ const isSelectAll = (e.metaKey || e.ctrlKey) && e.key === 'a';
3288
+
3289
+ if (isCmdOrCtrl || isSelectAll) {
3290
+ console.log('[TrustQuery] ===== KEYBOARD EVENT =====');
3291
+ console.log('[TrustQuery] Key pressed:', {
3292
+ key: e.key,
3293
+ code: e.code,
3294
+ metaKey: e.metaKey,
3295
+ ctrlKey: e.ctrlKey,
3296
+ shiftKey: e.shiftKey,
3297
+ altKey: e.altKey,
3298
+ isSelectAll: isSelectAll
3299
+ });
3300
+ console.log('[TrustQuery] Active element:', document.activeElement.tagName, document.activeElement.id || '(no id)');
3301
+ console.log('[TrustQuery] Textarea state BEFORE:', {
3302
+ value: this.textarea.value,
3303
+ valueLength: this.textarea.value.length,
3304
+ selectionStart: this.textarea.selectionStart,
3305
+ selectionEnd: this.textarea.selectionEnd,
3306
+ selectedText: this.textarea.value.substring(this.textarea.selectionStart, this.textarea.selectionEnd)
3307
+ });
3308
+
3309
+ if (isSelectAll) {
3310
+ // Log state after select all (use setTimeout to let browser process the event)
3311
+ setTimeout(() => {
3312
+ console.log('[TrustQuery] Textarea state AFTER CMD+A:', {
3313
+ selectionStart: this.textarea.selectionStart,
3314
+ selectionEnd: this.textarea.selectionEnd,
3315
+ selectedText: this.textarea.value.substring(this.textarea.selectionStart, this.textarea.selectionEnd),
3316
+ selectedLength: this.textarea.selectionEnd - this.textarea.selectionStart
3317
+ });
3318
+ console.log('[TrustQuery] ===== KEYBOARD EVENT END =====');
3319
+ }, 0);
3320
+ } else {
3321
+ console.log('[TrustQuery] ===== KEYBOARD EVENT END =====');
3322
+ }
3323
+ }
3324
+ });
3325
+
3326
+ // Selection change event
3327
+ this.textarea.addEventListener('select', (e) => {
3328
+ console.log('[TrustQuery] ===== SELECTION CHANGE EVENT =====');
3329
+ console.log('[TrustQuery] Selection:', {
3330
+ selectionStart: this.textarea.selectionStart,
3331
+ selectionEnd: this.textarea.selectionEnd,
3332
+ selectedText: this.textarea.value.substring(this.textarea.selectionStart, this.textarea.selectionEnd),
3333
+ selectedLength: this.textarea.selectionEnd - this.textarea.selectionStart
3334
+ });
3335
+ });
3336
+
3337
+ // Context menu event - prevent keyboard-triggered context menu
3338
+ this.textarea.addEventListener('contextmenu', (e) => {
3339
+ console.log('[TrustQuery] ===== CONTEXTMENU EVENT =====');
3340
+ console.log('[TrustQuery] Context menu triggered:', {
3341
+ type: e.type,
3342
+ isTrusted: e.isTrusted,
3343
+ button: e.button,
3344
+ buttons: e.buttons,
3345
+ clientX: e.clientX,
3346
+ clientY: e.clientY,
3347
+ ctrlKey: e.ctrlKey,
3348
+ metaKey: e.metaKey,
3349
+ target: e.target.tagName
3350
+ });
3351
+
3352
+ // Prevent context menu if triggered by keyboard (button === -1)
3353
+ // This prevents the macOS context menu from opening after CMD+A
3354
+ if (e.button === -1 && e.buttons === 0) {
3355
+ console.log('[TrustQuery] Preventing keyboard-triggered context menu');
3356
+ e.preventDefault();
3357
+ e.stopPropagation();
3358
+ return;
3359
+ }
3360
+
3361
+ console.log('[TrustQuery] Allowing mouse-triggered context menu');
3362
+ });
3363
+
3364
+ // Also prevent context menu on overlay (it interferes with text selection)
3365
+ this.overlay.addEventListener('contextmenu', (e) => {
3366
+ console.log('[TrustQuery] ===== CONTEXTMENU EVENT ON OVERLAY =====');
3367
+ console.log('[TrustQuery] Context menu on overlay - preventing');
3368
+
3369
+ // Always prevent context menu on overlay
3370
+ // The overlay should be transparent to user interactions
3371
+ e.preventDefault();
3372
+ e.stopPropagation();
3373
+ });
3374
+
2977
3375
  // Focus/blur events - add/remove focus class
2978
- this.textarea.addEventListener('focus', () => {
3376
+ this.textarea.addEventListener('focus', (e) => {
3377
+ console.log('[TrustQuery] ===== FOCUS EVENT =====');
3378
+ console.log('[TrustQuery] Textarea focused. Active element:', document.activeElement.tagName, document.activeElement.id || '(no id)');
3379
+ console.log('[TrustQuery] Current selection:', {
3380
+ selectionStart: this.textarea.selectionStart,
3381
+ selectionEnd: this.textarea.selectionEnd
3382
+ });
2979
3383
  this.wrapper.classList.add('tq-focused');
2980
3384
  });
2981
3385
 
2982
3386
  this.textarea.addEventListener('blur', (e) => {
3387
+ console.log('[TrustQuery] ===== BLUR EVENT =====');
3388
+ console.log('[TrustQuery] Textarea blurred. Related target:', e.relatedTarget?.tagName || '(none)');
3389
+ console.log('[TrustQuery] Blur event details:', {
3390
+ type: e.type,
3391
+ isTrusted: e.isTrusted,
3392
+ eventPhase: e.eventPhase,
3393
+ target: e.target.tagName,
3394
+ currentTarget: e.currentTarget.tagName
3395
+ });
3396
+ console.log('[TrustQuery] Stack trace at blur:');
3397
+ console.trace();
3398
+
2983
3399
  // Close dropdown when textarea loses focus (unless interacting with dropdown)
2984
3400
  if (this.interactionHandler) {
2985
3401
  // Use setTimeout to let the new focus target be set and check if clicking on dropdown
@@ -2990,6 +3406,8 @@ class TrustQuery {
2990
3406
  activeElement.closest('.tq-dropdown') // Check if clicking anywhere in dropdown
2991
3407
  );
2992
3408
 
3409
+ console.log('[TrustQuery] After blur - active element:', activeElement?.tagName || '(none)', 'isDropdownRelated:', isDropdownRelated);
3410
+
2993
3411
  // Only close if not interacting with dropdown
2994
3412
  if (!isDropdownRelated) {
2995
3413
  this.interactionHandler.hideDropdown();