@trustquery/browser 0.2.8 → 0.2.10
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/dist/trustquery.js +548 -25
- package/dist/trustquery.js.map +1 -1
- package/package.json +2 -2
- package/src/CommandHandlers.js +44 -0
- package/src/DropdownManager.js +124 -24
- package/src/MobileKeyboardHandler.js +240 -0
- package/src/StyleManager.js +6 -2
- package/src/TrustQuery.js +22 -0
- package/src/dropdown-manager-helpers/EdgeDetectionHelper.js +113 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trustquery/browser",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
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",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/RonItelman/trustquery-browser.git"
|
|
39
|
+
"url": "git+https://github.com/RonItelman/trustquery-browser.git"
|
|
40
40
|
},
|
|
41
41
|
"bugs": {
|
|
42
42
|
"url": "https://github.com/RonItelman/trustquery-browser/issues"
|
package/src/CommandHandlers.js
CHANGED
|
@@ -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;
|
package/src/DropdownManager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
376
|
-
const dropdownRect = dropdown.getBoundingClientRect();
|
|
377
|
-
const offset = this.options.dropdownOffset;
|
|
452
|
+
const matchRect = matchEl.getBoundingClientRect();
|
|
378
453
|
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
384
|
-
if (top < window.scrollY) {
|
|
385
|
-
top = rect.bottom + window.scrollY + offset;
|
|
386
|
-
}
|
|
458
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
387
459
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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.
|
|
394
|
-
|
|
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
|
/**
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// MobileKeyboardHandler - Handles mobile virtual keyboard behavior
|
|
2
|
+
// Detects keyboard appearance and adjusts layout to keep textarea visible
|
|
3
|
+
|
|
4
|
+
export default class MobileKeyboardHandler {
|
|
5
|
+
/**
|
|
6
|
+
* Create mobile keyboard handler
|
|
7
|
+
* @param {Object} options - Configuration options
|
|
8
|
+
*/
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.options = {
|
|
11
|
+
textarea: options.textarea || null,
|
|
12
|
+
wrapper: options.wrapper || null,
|
|
13
|
+
debug: options.debug || false,
|
|
14
|
+
...options
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
this.isKeyboardVisible = false;
|
|
18
|
+
this.lastViewportHeight = window.innerHeight;
|
|
19
|
+
this.visualViewport = window.visualViewport;
|
|
20
|
+
|
|
21
|
+
if (this.options.debug) {
|
|
22
|
+
console.log('[MobileKeyboardHandler] Initialized');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize keyboard detection
|
|
28
|
+
*/
|
|
29
|
+
init() {
|
|
30
|
+
if (!this.options.textarea) {
|
|
31
|
+
console.warn('[MobileKeyboardHandler] No textarea provided');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Use Visual Viewport API if available (preferred method)
|
|
36
|
+
if (this.visualViewport) {
|
|
37
|
+
this.visualViewport.addEventListener('resize', this.handleViewportResize);
|
|
38
|
+
this.visualViewport.addEventListener('scroll', this.handleViewportScroll);
|
|
39
|
+
|
|
40
|
+
if (this.options.debug) {
|
|
41
|
+
console.log('[MobileKeyboardHandler] Using Visual Viewport API');
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
// Fallback to window resize
|
|
45
|
+
window.addEventListener('resize', this.handleWindowResize);
|
|
46
|
+
|
|
47
|
+
if (this.options.debug) {
|
|
48
|
+
console.log('[MobileKeyboardHandler] Using window resize fallback');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle focus events
|
|
53
|
+
this.options.textarea.addEventListener('focus', this.handleFocus);
|
|
54
|
+
this.options.textarea.addEventListener('blur', this.handleBlur);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle Visual Viewport resize (keyboard appearance/disappearance)
|
|
59
|
+
*/
|
|
60
|
+
handleViewportResize = () => {
|
|
61
|
+
if (!this.visualViewport) return;
|
|
62
|
+
|
|
63
|
+
const viewportHeight = this.visualViewport.height;
|
|
64
|
+
const windowHeight = window.innerHeight;
|
|
65
|
+
|
|
66
|
+
if (this.options.debug) {
|
|
67
|
+
console.log('[MobileKeyboardHandler] Viewport resize:', {
|
|
68
|
+
viewportHeight,
|
|
69
|
+
windowHeight,
|
|
70
|
+
scale: this.visualViewport.scale
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Keyboard is visible if viewport height is significantly smaller than window height
|
|
75
|
+
const wasKeyboardVisible = this.isKeyboardVisible;
|
|
76
|
+
this.isKeyboardVisible = viewportHeight < windowHeight * 0.75;
|
|
77
|
+
|
|
78
|
+
if (this.isKeyboardVisible !== wasKeyboardVisible) {
|
|
79
|
+
if (this.isKeyboardVisible) {
|
|
80
|
+
this.onKeyboardShow();
|
|
81
|
+
} else {
|
|
82
|
+
this.onKeyboardHide();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Always adjust layout when viewport changes
|
|
87
|
+
if (this.isKeyboardVisible) {
|
|
88
|
+
this.adjustLayout();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Handle Visual Viewport scroll
|
|
94
|
+
*/
|
|
95
|
+
handleViewportScroll = () => {
|
|
96
|
+
if (this.isKeyboardVisible) {
|
|
97
|
+
// Ensure textarea stays in view during scroll
|
|
98
|
+
this.ensureTextareaVisible();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handle window resize (fallback)
|
|
104
|
+
*/
|
|
105
|
+
handleWindowResize = () => {
|
|
106
|
+
const currentHeight = window.innerHeight;
|
|
107
|
+
const heightDifference = this.lastViewportHeight - currentHeight;
|
|
108
|
+
|
|
109
|
+
if (this.options.debug) {
|
|
110
|
+
console.log('[MobileKeyboardHandler] Window resize:', {
|
|
111
|
+
lastHeight: this.lastViewportHeight,
|
|
112
|
+
currentHeight,
|
|
113
|
+
difference: heightDifference
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Significant decrease in height suggests keyboard appeared
|
|
118
|
+
if (heightDifference > 150) {
|
|
119
|
+
if (!this.isKeyboardVisible) {
|
|
120
|
+
this.isKeyboardVisible = true;
|
|
121
|
+
this.onKeyboardShow();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Significant increase suggests keyboard hidden
|
|
125
|
+
else if (heightDifference < -150) {
|
|
126
|
+
if (this.isKeyboardVisible) {
|
|
127
|
+
this.isKeyboardVisible = false;
|
|
128
|
+
this.onKeyboardHide();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.lastViewportHeight = currentHeight;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle textarea focus
|
|
137
|
+
*/
|
|
138
|
+
handleFocus = () => {
|
|
139
|
+
if (this.options.debug) {
|
|
140
|
+
console.log('[MobileKeyboardHandler] Textarea focused');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Delay to allow keyboard to appear
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
this.ensureTextareaVisible();
|
|
146
|
+
}, 300);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Handle textarea blur
|
|
151
|
+
*/
|
|
152
|
+
handleBlur = () => {
|
|
153
|
+
if (this.options.debug) {
|
|
154
|
+
console.log('[MobileKeyboardHandler] Textarea blurred');
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Called when keyboard appears
|
|
160
|
+
*/
|
|
161
|
+
onKeyboardShow() {
|
|
162
|
+
if (this.options.debug) {
|
|
163
|
+
console.log('[MobileKeyboardHandler] Keyboard shown');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.adjustLayout();
|
|
167
|
+
this.ensureTextareaVisible();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Called when keyboard hides
|
|
172
|
+
*/
|
|
173
|
+
onKeyboardHide() {
|
|
174
|
+
if (this.options.debug) {
|
|
175
|
+
console.log('[MobileKeyboardHandler] Keyboard hidden');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Reset wrapper height to auto
|
|
179
|
+
if (this.options.wrapper) {
|
|
180
|
+
this.options.wrapper.style.maxHeight = '';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Adjust layout to accommodate keyboard
|
|
186
|
+
*/
|
|
187
|
+
adjustLayout() {
|
|
188
|
+
if (!this.visualViewport || !this.options.wrapper) return;
|
|
189
|
+
|
|
190
|
+
const viewportHeight = this.visualViewport.height;
|
|
191
|
+
|
|
192
|
+
// Set wrapper max-height to visible viewport height minus some padding
|
|
193
|
+
const maxHeight = viewportHeight - 20; // 20px padding
|
|
194
|
+
this.options.wrapper.style.maxHeight = `${maxHeight}px`;
|
|
195
|
+
this.options.wrapper.style.overflow = 'auto';
|
|
196
|
+
|
|
197
|
+
if (this.options.debug) {
|
|
198
|
+
console.log('[MobileKeyboardHandler] Adjusted wrapper height:', maxHeight);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Ensure textarea is visible above the keyboard
|
|
204
|
+
*/
|
|
205
|
+
ensureTextareaVisible() {
|
|
206
|
+
if (!this.options.textarea) return;
|
|
207
|
+
|
|
208
|
+
// Scroll textarea into view
|
|
209
|
+
this.options.textarea.scrollIntoView({
|
|
210
|
+
behavior: 'smooth',
|
|
211
|
+
block: 'center',
|
|
212
|
+
inline: 'nearest'
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (this.options.debug) {
|
|
216
|
+
console.log('[MobileKeyboardHandler] Scrolled textarea into view');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Cleanup event listeners
|
|
222
|
+
*/
|
|
223
|
+
destroy() {
|
|
224
|
+
if (this.visualViewport) {
|
|
225
|
+
this.visualViewport.removeEventListener('resize', this.handleViewportResize);
|
|
226
|
+
this.visualViewport.removeEventListener('scroll', this.handleViewportScroll);
|
|
227
|
+
} else {
|
|
228
|
+
window.removeEventListener('resize', this.handleWindowResize);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (this.options.textarea) {
|
|
232
|
+
this.options.textarea.removeEventListener('focus', this.handleFocus);
|
|
233
|
+
this.options.textarea.removeEventListener('blur', this.handleBlur);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (this.options.debug) {
|
|
237
|
+
console.log('[MobileKeyboardHandler] Destroyed');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
package/src/StyleManager.js
CHANGED
|
@@ -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:
|
|
289
|
+
maxWidth: maxWidth,
|
|
286
290
|
overflow: 'hidden',
|
|
287
291
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
288
292
|
fontSize: '14px',
|
package/src/TrustQuery.js
CHANGED
|
@@ -8,6 +8,7 @@ import StyleManager from './StyleManager.js';
|
|
|
8
8
|
import CommandHandlerRegistry from './CommandHandlers.js';
|
|
9
9
|
import AutoGrow from './AutoGrow.js';
|
|
10
10
|
import ValidationStateManager from './ValidationStateManager.js';
|
|
11
|
+
import MobileKeyboardHandler from './MobileKeyboardHandler.js';
|
|
11
12
|
|
|
12
13
|
export default class TrustQuery {
|
|
13
14
|
// Store all instances
|
|
@@ -212,6 +213,17 @@ export default class TrustQuery {
|
|
|
212
213
|
console.log('[TrustQuery] AutoGrow feature enabled');
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
// Mobile keyboard handler (enabled by default, can be disabled via options)
|
|
217
|
+
if (this.options.mobileKeyboard !== false) {
|
|
218
|
+
this.features.mobileKeyboard = new MobileKeyboardHandler({
|
|
219
|
+
textarea: this.textarea,
|
|
220
|
+
wrapper: this.wrapper,
|
|
221
|
+
debug: this.options.debug
|
|
222
|
+
});
|
|
223
|
+
this.features.mobileKeyboard.init();
|
|
224
|
+
console.log('[TrustQuery] Mobile keyboard handler enabled');
|
|
225
|
+
}
|
|
226
|
+
|
|
215
227
|
// Debug logging feature
|
|
216
228
|
if (this.options.debug) {
|
|
217
229
|
this.enableDebugLogging();
|
|
@@ -502,6 +514,16 @@ export default class TrustQuery {
|
|
|
502
514
|
this.interactionHandler.destroy();
|
|
503
515
|
}
|
|
504
516
|
|
|
517
|
+
// Cleanup mobile keyboard handler
|
|
518
|
+
if (this.features.mobileKeyboard) {
|
|
519
|
+
this.features.mobileKeyboard.destroy();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Cleanup auto-grow
|
|
523
|
+
if (this.features.autoGrow) {
|
|
524
|
+
this.features.autoGrow.destroy();
|
|
525
|
+
}
|
|
526
|
+
|
|
505
527
|
// Unwrap textarea
|
|
506
528
|
const parent = this.wrapper.parentNode;
|
|
507
529
|
parent.insertBefore(this.textarea, this.wrapper);
|
|
@@ -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
|
+
}
|