@trustquery/browser 0.2.8 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustquery/browser",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Turn any textarea into an interactive trigger-based editor with inline styling",
5
5
  "type": "module",
6
6
  "main": "dist/trustquery.js",
@@ -17,6 +17,8 @@ export class CommandHandlerRegistry {
17
17
  this.register('user-select-oneOf', new UserSelectHandler());
18
18
  this.register('api-json-table', new ApiJsonTableHandler());
19
19
  this.register('api-md-table', new ApiMdTableHandler());
20
+ this.register('display-menu', new DisplayMenuHandler());
21
+ this.register('display-menu-with-uri', new DisplayMenuWithUriHandler());
20
22
  }
21
23
 
22
24
  /**
@@ -347,4 +349,46 @@ class ApiMdTableHandler extends CommandHandler {
347
349
  }
348
350
  }
349
351
 
352
+ /**
353
+ * Handler for display-menu category
354
+ * Shows dropdown menu with selectable options
355
+ */
356
+ class DisplayMenuHandler extends CommandHandler {
357
+ getStyles() {
358
+ return {
359
+ backgroundColor: 'rgba(16, 185, 129, 0.15)', // Green background
360
+ color: '#065f46', // Dark green text
361
+ textDecoration: 'none',
362
+ borderBottom: '2px solid #10b981', // Green underline
363
+ cursor: 'pointer'
364
+ };
365
+ }
366
+
367
+ getBubbleContent(matchData) {
368
+ // Display menu shows dropdown, not bubble
369
+ return null;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Handler for display-menu-with-uri category
375
+ * Shows dropdown menu with selectable options and clickable URI links
376
+ */
377
+ class DisplayMenuWithUriHandler extends CommandHandler {
378
+ getStyles() {
379
+ return {
380
+ backgroundColor: 'rgba(16, 185, 129, 0.15)', // Green background
381
+ color: '#065f46', // Dark green text
382
+ textDecoration: 'none',
383
+ borderBottom: '2px solid #10b981', // Green underline
384
+ cursor: 'pointer'
385
+ };
386
+ }
387
+
388
+ getBubbleContent(matchData) {
389
+ // Display menu with URI shows dropdown, not bubble
390
+ return null;
391
+ }
392
+ }
393
+
350
394
  export default CommandHandlerRegistry;
@@ -1,5 +1,7 @@
1
1
  // DropdownManager - Handles dropdown menus with filtering, keyboard navigation, and selection
2
2
 
3
+ import EdgeDetectionHelper from './dropdown-manager-helpers/EdgeDetectionHelper.js';
4
+
3
5
  export default class DropdownManager {
4
6
  /**
5
7
  * Create dropdown manager
@@ -53,9 +55,10 @@ export default class DropdownManager {
53
55
  const dropdown = document.createElement('div');
54
56
  dropdown.className = 'tq-dropdown';
55
57
 
56
- // Apply inline styles via StyleManager
58
+ // Apply inline styles via StyleManager (pass category for width adjustment)
59
+ const category = matchData.intent?.category || '';
57
60
  if (this.options.styleManager) {
58
- this.options.styleManager.applyDropdownStyles(dropdown);
61
+ this.options.styleManager.applyDropdownStyles(dropdown, category);
59
62
  }
60
63
 
61
64
  // Add header container based on message-state
@@ -239,6 +242,10 @@ export default class DropdownManager {
239
242
  * @param {Object} matchData - Match data
240
243
  */
241
244
  createDropdownItems(dropdown, options, matchData) {
245
+ // Check if this is a display-menu-with-uri category
246
+ const category = matchData.intent?.category || '';
247
+ const hasUriSupport = category === 'display-menu-with-uri';
248
+
242
249
  options.forEach((option, index) => {
243
250
  // Check if this is a user-input option
244
251
  if (typeof option === 'object' && option['user-input'] === true) {
@@ -248,7 +255,82 @@ export default class DropdownManager {
248
255
 
249
256
  const item = document.createElement('div');
250
257
  item.className = 'tq-dropdown-item';
251
- item.textContent = typeof option === 'string' ? option : option.label || option.value;
258
+
259
+ // Create label text
260
+ const labelText = typeof option === 'string' ? option : option.label || option.value;
261
+
262
+ // If display-menu-with-uri and option has uri, create label + link icon
263
+ if (hasUriSupport && typeof option === 'object' && option.uri) {
264
+ // Create label span
265
+ const labelSpan = document.createElement('span');
266
+ labelSpan.className = 'tq-dropdown-item-label';
267
+ labelSpan.textContent = labelText;
268
+ labelSpan.style.flex = '1';
269
+ labelSpan.style.cursor = 'pointer';
270
+
271
+ // Create link with truncated URI text
272
+ const linkIcon = document.createElement('a');
273
+ linkIcon.className = 'tq-dropdown-item-link';
274
+ linkIcon.href = option.uri;
275
+ linkIcon.target = '_blank';
276
+ linkIcon.rel = 'noopener noreferrer';
277
+
278
+ // Truncate URI to 20 characters
279
+ let displayUri = option.uri;
280
+ if (displayUri.length > 20) {
281
+ displayUri = displayUri.substring(0, 20) + '...';
282
+ }
283
+ linkIcon.textContent = displayUri;
284
+
285
+ linkIcon.style.marginLeft = '8px';
286
+ linkIcon.style.fontSize = '12px';
287
+ linkIcon.style.color = '#3b82f6';
288
+ linkIcon.style.textDecoration = 'underline';
289
+ linkIcon.style.opacity = '0.7';
290
+ linkIcon.style.transition = 'opacity 0.2s';
291
+ linkIcon.style.whiteSpace = 'nowrap';
292
+ linkIcon.title = option.uri;
293
+ linkIcon.setAttribute('aria-label', `Open ${labelText} documentation`);
294
+
295
+ // Hover effect for link icon
296
+ linkIcon.addEventListener('mouseenter', () => {
297
+ linkIcon.style.opacity = '1';
298
+ });
299
+ linkIcon.addEventListener('mouseleave', () => {
300
+ linkIcon.style.opacity = '0.6';
301
+ });
302
+
303
+ // Prevent link click from selecting the option
304
+ linkIcon.addEventListener('click', (e) => {
305
+ e.stopPropagation();
306
+ // Link will open naturally via href
307
+ });
308
+
309
+ // Set item to flex layout
310
+ item.style.display = 'flex';
311
+ item.style.alignItems = 'center';
312
+ item.style.justifyContent = 'space-between';
313
+
314
+ item.appendChild(labelSpan);
315
+ item.appendChild(linkIcon);
316
+
317
+ // Only label click should select the option
318
+ labelSpan.addEventListener('click', (e) => {
319
+ e.stopPropagation();
320
+ this.handleDropdownSelect(option, matchData);
321
+ this.hideDropdown();
322
+ });
323
+ } else {
324
+ // Regular option without URI
325
+ item.textContent = labelText;
326
+
327
+ // Click anywhere on item to select
328
+ item.addEventListener('click', (e) => {
329
+ e.stopPropagation();
330
+ this.handleDropdownSelect(option, matchData);
331
+ this.hideDropdown();
332
+ });
333
+ }
252
334
 
253
335
  // Highlight first item by default
254
336
  if (index === 0) {
@@ -260,11 +342,6 @@ export default class DropdownManager {
260
342
  this.options.styleManager.applyDropdownItemStyles(item);
261
343
  }
262
344
 
263
- item.addEventListener('click', (e) => {
264
- e.stopPropagation();
265
- this.handleDropdownSelect(option, matchData);
266
- this.hideDropdown();
267
- });
268
345
  dropdown.appendChild(item);
269
346
  });
270
347
  }
@@ -372,26 +449,49 @@ export default class DropdownManager {
372
449
  * @param {HTMLElement} matchEl - Match element
373
450
  */
374
451
  positionDropdown(dropdown, matchEl) {
375
- const rect = matchEl.getBoundingClientRect();
376
- const dropdownRect = dropdown.getBoundingClientRect();
377
- const offset = this.options.dropdownOffset;
452
+ const matchRect = matchEl.getBoundingClientRect();
378
453
 
379
- // Position above match by default (since input is at bottom)
380
- let top = rect.top + window.scrollY - dropdownRect.height - offset;
381
- let left = rect.left + window.scrollX;
454
+ // Force a layout calculation by accessing offsetWidth
455
+ // This ensures we get the correct dropdown dimensions after styles are applied
456
+ const _ = dropdown.offsetWidth;
382
457
 
383
- // If dropdown goes off top edge, position below instead
384
- if (top < window.scrollY) {
385
- top = rect.bottom + window.scrollY + offset;
386
- }
458
+ const dropdownRect = dropdown.getBoundingClientRect();
387
459
 
388
- // Check if dropdown goes off right edge
389
- if (left + dropdownRect.width > window.innerWidth) {
390
- left = window.innerWidth - dropdownRect.width - 10;
391
- }
460
+ console.log('[DropdownManager] Positioning Debug:');
461
+ console.log(' Trigger position (x, y):', matchRect.left, matchRect.top);
462
+ console.log(' Dropdown width:', dropdownRect.width);
463
+ console.log(' Viewport width:', window.innerWidth);
464
+
465
+ // Use EdgeDetectionHelper to calculate optimal position
466
+ // Increased padding to account for body margins/transforms and ensure visible clearance
467
+ const position = EdgeDetectionHelper.calculatePosition({
468
+ matchRect,
469
+ dropdownRect,
470
+ offset: this.options.dropdownOffset,
471
+ padding: 35
472
+ });
392
473
 
393
- dropdown.style.top = `${top}px`;
394
- dropdown.style.left = `${left}px`;
474
+ console.log(' Calculated dropdown position (x, y):', position.left, position.top);
475
+
476
+ dropdown.style.top = `${position.top}px`;
477
+ dropdown.style.left = `${position.left}px`;
478
+
479
+ // Verify the position was actually applied
480
+ const computedStyle = window.getComputedStyle(dropdown);
481
+ const actualRect = dropdown.getBoundingClientRect();
482
+ console.log(' Verified applied styles:');
483
+ console.log(' styleTop:', dropdown.style.top);
484
+ console.log(' styleLeft:', dropdown.style.left);
485
+ console.log(' computedPosition:', computedStyle.position);
486
+ console.log(' actualBoundingRect:', JSON.stringify({
487
+ left: actualRect.left,
488
+ top: actualRect.top,
489
+ right: actualRect.right,
490
+ bottom: actualRect.bottom,
491
+ width: actualRect.width,
492
+ height: actualRect.height
493
+ }));
494
+ console.log(' Dropdown right edge:', actualRect.left + actualRect.width, 'vs viewport width:', window.innerWidth);
395
495
  }
396
496
 
397
497
  /**
@@ -272,8 +272,12 @@ export default class StyleManager {
272
272
  /**
273
273
  * Apply dropdown (menu) styles
274
274
  * @param {HTMLElement} dropdown - Dropdown element
275
+ * @param {string} category - Optional category to adjust styles
275
276
  */
276
- applyDropdownStyles(dropdown) {
277
+ applyDropdownStyles(dropdown, category = '') {
278
+ // Use wider maxWidth for display-menu-with-uri to accommodate links
279
+ const maxWidth = category === 'display-menu-with-uri' ? '450px' : '300px';
280
+
277
281
  Object.assign(dropdown.style, {
278
282
  position: 'absolute',
279
283
  background: '#ffffff',
@@ -282,7 +286,7 @@ export default class StyleManager {
282
286
  boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
283
287
  zIndex: '10000',
284
288
  minWidth: '150px',
285
- maxWidth: '300px',
289
+ maxWidth: maxWidth,
286
290
  overflow: 'hidden',
287
291
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
288
292
  fontSize: '14px',
@@ -0,0 +1,113 @@
1
+ // EdgeDetectionHelper - Calculates dropdown positioning to prevent overflow off viewport edges
2
+
3
+ export default class EdgeDetectionHelper {
4
+ /**
5
+ * Calculate optimal position for dropdown to prevent viewport overflow
6
+ * @param {Object} options - Positioning options
7
+ * @param {DOMRect} options.matchRect - Bounding rect of the match element
8
+ * @param {DOMRect} options.dropdownRect - Bounding rect of the dropdown
9
+ * @param {number} options.offset - Offset from match element (default: 28)
10
+ * @param {number} options.padding - Padding from viewport edges (default: 10)
11
+ * @returns {Object} - { top, left } position in pixels
12
+ */
13
+ static calculatePosition({ matchRect, dropdownRect, offset = 28, padding = 10 }) {
14
+ const viewportWidth = window.innerWidth;
15
+ const viewportHeight = window.innerHeight;
16
+
17
+ console.log('[EdgeDetectionHelper] Calculation inputs:');
18
+ console.log(' Match rect:', { left: matchRect.left, top: matchRect.top, width: matchRect.width });
19
+ console.log(' Dropdown rect:', { width: dropdownRect.width, height: dropdownRect.height });
20
+ console.log(' Viewport:', { width: viewportWidth, height: viewportHeight });
21
+ console.log(' Scroll:', { x: window.scrollX, y: window.scrollY });
22
+
23
+ // Calculate initial position (above match by default, since input is at bottom)
24
+ let top = matchRect.top + window.scrollY - dropdownRect.height - offset;
25
+ let left = matchRect.left + window.scrollX;
26
+
27
+ console.log(' Initial position:', { left, top });
28
+
29
+ // Vertical positioning: Check if dropdown goes off top edge
30
+ if (top < window.scrollY) {
31
+ // Position below match instead
32
+ top = matchRect.bottom + window.scrollY + offset;
33
+ console.log(' Adjusted for top overflow, new top:', top);
34
+ }
35
+
36
+ // Horizontal positioning: Check if dropdown goes off right edge
37
+ const rightEdge = left + dropdownRect.width;
38
+ const viewportRightEdge = viewportWidth + window.scrollX;
39
+
40
+ console.log(' Right edge check:', { rightEdge, viewportRightEdge: viewportRightEdge - padding });
41
+
42
+ if (rightEdge > viewportRightEdge - padding) {
43
+ // Calculate how much we overflow past the right edge
44
+ const overflow = rightEdge - (viewportRightEdge - padding);
45
+ console.log(' Right overflow detected:', overflow, 'px');
46
+
47
+ // Shift left by the overflow amount
48
+ left = left - overflow;
49
+ console.log(' Adjusted left position:', left);
50
+
51
+ // Ensure we don't go off the left edge either
52
+ const minLeft = window.scrollX + padding;
53
+ if (left < minLeft) {
54
+ console.log(' Hit left edge, clamping to:', minLeft);
55
+ left = minLeft;
56
+ }
57
+ }
58
+
59
+ // Also check left edge (in case match is near left edge)
60
+ const minLeft = window.scrollX + padding;
61
+ if (left < minLeft) {
62
+ console.log(' Left edge adjustment:', minLeft);
63
+ left = minLeft;
64
+ }
65
+
66
+ console.log('[EdgeDetectionHelper] Final position:', { left, top });
67
+
68
+ return { top, left };
69
+ }
70
+
71
+ /**
72
+ * Check if dropdown would overflow the right edge
73
+ * @param {DOMRect} matchRect - Match element rect
74
+ * @param {DOMRect} dropdownRect - Dropdown rect
75
+ * @param {number} padding - Padding from edge
76
+ * @returns {boolean} - True if would overflow
77
+ */
78
+ static wouldOverflowRight(matchRect, dropdownRect, padding = 10) {
79
+ const left = matchRect.left + window.scrollX;
80
+ const rightEdge = left + dropdownRect.width;
81
+ const viewportRightEdge = window.innerWidth + window.scrollX;
82
+
83
+ return rightEdge > viewportRightEdge - padding;
84
+ }
85
+
86
+ /**
87
+ * Check if dropdown would overflow the top edge
88
+ * @param {DOMRect} matchRect - Match element rect
89
+ * @param {DOMRect} dropdownRect - Dropdown rect
90
+ * @param {number} offset - Offset from match
91
+ * @returns {boolean} - True if would overflow
92
+ */
93
+ static wouldOverflowTop(matchRect, dropdownRect, offset = 28) {
94
+ const top = matchRect.top + window.scrollY - dropdownRect.height - offset;
95
+ return top < window.scrollY;
96
+ }
97
+
98
+ /**
99
+ * Calculate overflow amount on the right edge
100
+ * @param {DOMRect} matchRect - Match element rect
101
+ * @param {DOMRect} dropdownRect - Dropdown rect
102
+ * @param {number} padding - Padding from edge
103
+ * @returns {number} - Overflow amount in pixels (0 if no overflow)
104
+ */
105
+ static calculateRightOverflow(matchRect, dropdownRect, padding = 10) {
106
+ const left = matchRect.left + window.scrollX;
107
+ const rightEdge = left + dropdownRect.width;
108
+ const viewportRightEdge = window.innerWidth + window.scrollX;
109
+
110
+ const overflow = rightEdge - (viewportRightEdge - padding);
111
+ return Math.max(0, overflow);
112
+ }
113
+ }