@trustquery/browser 0.1.0
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/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/trustquery.js +2904 -0
- package/dist/trustquery.js.map +1 -0
- package/package.json +49 -0
- package/src/AutoGrow.js +66 -0
- package/src/BubbleManager.js +219 -0
- package/src/CommandHandlers.js +350 -0
- package/src/CommandScanner.js +285 -0
- package/src/DropdownManager.js +592 -0
- package/src/InteractionHandler.js +225 -0
- package/src/OverlayRenderer.js +241 -0
- package/src/StyleManager.js +523 -0
- package/src/TrustQuery.js +402 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
// DropdownManager - Handles dropdown menus with filtering, keyboard navigation, and selection
|
|
2
|
+
|
|
3
|
+
export default class DropdownManager {
|
|
4
|
+
/**
|
|
5
|
+
* Create dropdown manager
|
|
6
|
+
* @param {Object} options - Configuration
|
|
7
|
+
*/
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = {
|
|
10
|
+
styleManager: options.styleManager || null,
|
|
11
|
+
textarea: options.textarea || null,
|
|
12
|
+
onWordClick: options.onWordClick || null,
|
|
13
|
+
dropdownOffset: options.dropdownOffset || 10, // Configurable offset from trigger word
|
|
14
|
+
...options
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
this.activeDropdown = null;
|
|
18
|
+
this.activeDropdownMatch = null;
|
|
19
|
+
this.dropdownOptions = null;
|
|
20
|
+
this.dropdownMatchData = null;
|
|
21
|
+
this.selectedDropdownIndex = 0;
|
|
22
|
+
this.keyboardHandler = null;
|
|
23
|
+
|
|
24
|
+
console.log('[DropdownManager] Initialized with offset:', this.options.dropdownOffset);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Show dropdown for a match
|
|
29
|
+
* @param {HTMLElement} matchEl - Match element
|
|
30
|
+
* @param {Object} matchData - Match data
|
|
31
|
+
*/
|
|
32
|
+
showDropdown(matchEl, matchData) {
|
|
33
|
+
// Close any existing dropdown
|
|
34
|
+
this.hideDropdown();
|
|
35
|
+
|
|
36
|
+
const command = matchData.command;
|
|
37
|
+
|
|
38
|
+
// Get dropdown options - check intent.handler.options first (new format)
|
|
39
|
+
const options = matchData.intent?.handler?.options || command.options || command.dropdownOptions || [];
|
|
40
|
+
|
|
41
|
+
if (options.length === 0) {
|
|
42
|
+
console.warn('[DropdownManager] No dropdown options for:', matchData.text);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Store reference to match element
|
|
47
|
+
this.activeDropdownMatch = matchEl;
|
|
48
|
+
this.selectedDropdownIndex = 0;
|
|
49
|
+
|
|
50
|
+
// Create dropdown
|
|
51
|
+
const dropdown = document.createElement('div');
|
|
52
|
+
dropdown.className = 'tq-dropdown';
|
|
53
|
+
|
|
54
|
+
// Apply inline styles via StyleManager
|
|
55
|
+
if (this.options.styleManager) {
|
|
56
|
+
this.options.styleManager.applyDropdownStyles(dropdown);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add header container based on message-state
|
|
60
|
+
const messageState = matchData.intent?.handler?.['message-state'] || 'info';
|
|
61
|
+
this.createHeaderContainer(dropdown, messageState);
|
|
62
|
+
|
|
63
|
+
// Add description row if message exists
|
|
64
|
+
const message = matchData.intent?.handler?.message;
|
|
65
|
+
if (message) {
|
|
66
|
+
this.createDescriptionRow(dropdown, message, messageState);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check if filter is enabled
|
|
70
|
+
const hasFilter = matchData.intent?.handler?.filter === true;
|
|
71
|
+
|
|
72
|
+
// Add filter input if enabled
|
|
73
|
+
if (hasFilter) {
|
|
74
|
+
this.createFilterInput(dropdown, matchData);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create dropdown items
|
|
78
|
+
this.createDropdownItems(dropdown, options, matchData);
|
|
79
|
+
|
|
80
|
+
// Prevent textarea blur when clicking dropdown (except filter input)
|
|
81
|
+
dropdown.addEventListener('mousedown', (e) => {
|
|
82
|
+
// Allow filter input to receive focus naturally
|
|
83
|
+
if (!e.target.classList.contains('tq-dropdown-filter')) {
|
|
84
|
+
e.preventDefault(); // Prevent blur on textarea for items
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Add to document
|
|
89
|
+
document.body.appendChild(dropdown);
|
|
90
|
+
this.activeDropdown = dropdown;
|
|
91
|
+
this.dropdownOptions = options;
|
|
92
|
+
this.dropdownMatchData = matchData;
|
|
93
|
+
|
|
94
|
+
// Position dropdown
|
|
95
|
+
this.positionDropdown(dropdown, matchEl);
|
|
96
|
+
|
|
97
|
+
// Setup keyboard navigation
|
|
98
|
+
this.setupKeyboardHandlers();
|
|
99
|
+
|
|
100
|
+
// Close on click outside
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
document.addEventListener('click', this.closeDropdownHandler);
|
|
103
|
+
}, 0);
|
|
104
|
+
|
|
105
|
+
console.log('[DropdownManager] Dropdown shown with', options.length, 'options');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create header container for dropdown
|
|
110
|
+
* @param {HTMLElement} dropdown - Dropdown element
|
|
111
|
+
* @param {string} messageState - Message state (error, warning, info)
|
|
112
|
+
*/
|
|
113
|
+
createHeaderContainer(dropdown, messageState) {
|
|
114
|
+
const headerContainer = document.createElement('div');
|
|
115
|
+
headerContainer.className = 'bubble-header-container';
|
|
116
|
+
headerContainer.setAttribute('data-type', messageState);
|
|
117
|
+
|
|
118
|
+
// Create image
|
|
119
|
+
const img = document.createElement('img');
|
|
120
|
+
img.src = `./assets/trustquery-${messageState}.svg`;
|
|
121
|
+
img.style.height = '24px';
|
|
122
|
+
img.style.width = 'auto';
|
|
123
|
+
|
|
124
|
+
// Create text span
|
|
125
|
+
const span = document.createElement('span');
|
|
126
|
+
const textMap = {
|
|
127
|
+
'error': 'TrustQuery Stop',
|
|
128
|
+
'warning': 'TrustQuery Clarify',
|
|
129
|
+
'info': 'TrustQuery Quick Link'
|
|
130
|
+
};
|
|
131
|
+
span.textContent = textMap[messageState] || 'TrustQuery';
|
|
132
|
+
|
|
133
|
+
// Append to header
|
|
134
|
+
headerContainer.appendChild(img);
|
|
135
|
+
headerContainer.appendChild(span);
|
|
136
|
+
|
|
137
|
+
// Apply styles to header via StyleManager
|
|
138
|
+
if (this.options.styleManager) {
|
|
139
|
+
this.options.styleManager.applyDropdownHeaderStyles(headerContainer, messageState);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
dropdown.appendChild(headerContainer);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create description row for dropdown
|
|
147
|
+
* @param {HTMLElement} dropdown - Dropdown element
|
|
148
|
+
* @param {string} message - Description message
|
|
149
|
+
* @param {string} messageState - Message state (error, warning, info)
|
|
150
|
+
*/
|
|
151
|
+
createDescriptionRow(dropdown, message, messageState) {
|
|
152
|
+
const descriptionRow = document.createElement('div');
|
|
153
|
+
descriptionRow.className = 'tq-dropdown-description';
|
|
154
|
+
descriptionRow.textContent = message;
|
|
155
|
+
|
|
156
|
+
// Apply styles to description via StyleManager
|
|
157
|
+
if (this.options.styleManager) {
|
|
158
|
+
this.options.styleManager.applyDropdownDescriptionStyles(descriptionRow, messageState);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
dropdown.appendChild(descriptionRow);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create filter input for dropdown
|
|
166
|
+
* @param {HTMLElement} dropdown - Dropdown element
|
|
167
|
+
* @param {Object} matchData - Match data
|
|
168
|
+
*/
|
|
169
|
+
createFilterInput(dropdown, matchData) {
|
|
170
|
+
const filterInput = document.createElement('input');
|
|
171
|
+
filterInput.type = 'text';
|
|
172
|
+
filterInput.className = 'tq-dropdown-filter';
|
|
173
|
+
filterInput.placeholder = 'Filter options...';
|
|
174
|
+
|
|
175
|
+
// Apply inline styles via StyleManager
|
|
176
|
+
if (this.options.styleManager) {
|
|
177
|
+
this.options.styleManager.applyDropdownFilterStyles(filterInput);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Filter click handling is managed by dropdown and closeDropdownHandler
|
|
181
|
+
// No special mousedown/blur handling needed - filter focuses naturally
|
|
182
|
+
|
|
183
|
+
// Filter options as user types
|
|
184
|
+
filterInput.addEventListener('input', (e) => {
|
|
185
|
+
const query = e.target.value.toLowerCase();
|
|
186
|
+
const items = dropdown.querySelectorAll('.tq-dropdown-item');
|
|
187
|
+
let firstVisibleIndex = -1;
|
|
188
|
+
|
|
189
|
+
items.forEach((item, index) => {
|
|
190
|
+
const text = item.textContent.toLowerCase();
|
|
191
|
+
const matches = text.includes(query);
|
|
192
|
+
item.style.display = matches ? 'block' : 'none';
|
|
193
|
+
|
|
194
|
+
// Track first visible item for selection
|
|
195
|
+
if (matches && firstVisibleIndex === -1) {
|
|
196
|
+
firstVisibleIndex = index;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Update selected index to first visible item
|
|
201
|
+
if (firstVisibleIndex !== -1) {
|
|
202
|
+
this.selectedDropdownIndex = firstVisibleIndex;
|
|
203
|
+
this.updateDropdownSelection();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Prevent dropdown keyboard navigation from affecting filter input
|
|
208
|
+
filterInput.addEventListener('keydown', (e) => {
|
|
209
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
// Move focus to dropdown items
|
|
213
|
+
filterInput.blur();
|
|
214
|
+
this.options.textarea.focus();
|
|
215
|
+
} else if (e.key === 'Enter') {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
e.stopPropagation();
|
|
218
|
+
this.selectCurrentDropdownItem();
|
|
219
|
+
} else if (e.key === 'Escape') {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
e.stopPropagation();
|
|
222
|
+
this.hideDropdown();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
dropdown.appendChild(filterInput);
|
|
227
|
+
|
|
228
|
+
// Don't auto-focus - keep focus on textarea for natural typing/arrow key flow
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create dropdown items
|
|
233
|
+
* @param {HTMLElement} dropdown - Dropdown element
|
|
234
|
+
* @param {Array} options - Dropdown options
|
|
235
|
+
* @param {Object} matchData - Match data
|
|
236
|
+
*/
|
|
237
|
+
createDropdownItems(dropdown, options, matchData) {
|
|
238
|
+
options.forEach((option, index) => {
|
|
239
|
+
// Check if this is a user-input option
|
|
240
|
+
if (typeof option === 'object' && option['user-input'] === true) {
|
|
241
|
+
this.createUserInputOption(dropdown, option, matchData);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const item = document.createElement('div');
|
|
246
|
+
item.className = 'tq-dropdown-item';
|
|
247
|
+
item.textContent = typeof option === 'string' ? option : option.label || option.value;
|
|
248
|
+
|
|
249
|
+
// Highlight first item by default
|
|
250
|
+
if (index === 0) {
|
|
251
|
+
item.classList.add('tq-dropdown-item-selected');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Apply inline styles to item via StyleManager
|
|
255
|
+
if (this.options.styleManager) {
|
|
256
|
+
this.options.styleManager.applyDropdownItemStyles(item);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
item.addEventListener('click', (e) => {
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
this.handleDropdownSelect(option, matchData);
|
|
262
|
+
this.hideDropdown();
|
|
263
|
+
});
|
|
264
|
+
dropdown.appendChild(item);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create user input option
|
|
270
|
+
* @param {HTMLElement} dropdown - Dropdown element
|
|
271
|
+
* @param {Object} option - Option with user-input flag
|
|
272
|
+
* @param {Object} matchData - Match data
|
|
273
|
+
*/
|
|
274
|
+
createUserInputOption(dropdown, option, matchData) {
|
|
275
|
+
const container = document.createElement('div');
|
|
276
|
+
container.className = 'tq-dropdown-user-input-container tq-dropdown-item';
|
|
277
|
+
container.setAttribute('data-user-input', 'true');
|
|
278
|
+
|
|
279
|
+
const input = document.createElement('input');
|
|
280
|
+
input.type = 'text';
|
|
281
|
+
input.className = 'tq-dropdown-user-input';
|
|
282
|
+
input.placeholder = option.placeholder || 'Enter custom value...';
|
|
283
|
+
|
|
284
|
+
container.appendChild(input);
|
|
285
|
+
|
|
286
|
+
// Apply styles via StyleManager
|
|
287
|
+
if (this.options.styleManager) {
|
|
288
|
+
this.options.styleManager.applyUserInputStyles(container, input);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Click on container focuses input
|
|
292
|
+
container.addEventListener('click', (e) => {
|
|
293
|
+
e.stopPropagation();
|
|
294
|
+
input.focus();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Handle submit on Enter key
|
|
298
|
+
const handleSubmit = () => {
|
|
299
|
+
const value = input.value.trim();
|
|
300
|
+
if (value) {
|
|
301
|
+
const customOption = {
|
|
302
|
+
label: value,
|
|
303
|
+
'on-select': {
|
|
304
|
+
display: value
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
this.handleDropdownSelect(customOption, matchData);
|
|
308
|
+
this.hideDropdown();
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
input.addEventListener('keydown', (e) => {
|
|
313
|
+
if (e.key === 'Enter') {
|
|
314
|
+
e.preventDefault();
|
|
315
|
+
e.stopPropagation();
|
|
316
|
+
handleSubmit();
|
|
317
|
+
} else if (e.key === 'Escape') {
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
this.hideDropdown();
|
|
321
|
+
} else if (e.key === 'ArrowDown') {
|
|
322
|
+
// Navigate to next item
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
e.stopPropagation();
|
|
325
|
+
this.moveDropdownSelection(1);
|
|
326
|
+
this.options.textarea.focus();
|
|
327
|
+
} else if (e.key === 'ArrowUp') {
|
|
328
|
+
// Navigate to previous item
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
e.stopPropagation();
|
|
331
|
+
this.moveDropdownSelection(-1);
|
|
332
|
+
this.options.textarea.focus();
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
dropdown.appendChild(container);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Hide current dropdown
|
|
341
|
+
*/
|
|
342
|
+
hideDropdown() {
|
|
343
|
+
if (this.activeDropdown) {
|
|
344
|
+
this.activeDropdown.remove();
|
|
345
|
+
this.activeDropdown = null;
|
|
346
|
+
this.activeDropdownMatch = null;
|
|
347
|
+
this.dropdownOptions = null;
|
|
348
|
+
this.dropdownMatchData = null;
|
|
349
|
+
this.selectedDropdownIndex = 0;
|
|
350
|
+
document.removeEventListener('click', this.closeDropdownHandler);
|
|
351
|
+
document.removeEventListener('keydown', this.keyboardHandler);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Close dropdown handler (bound to document)
|
|
357
|
+
*/
|
|
358
|
+
closeDropdownHandler = (e) => {
|
|
359
|
+
// Only close if clicking outside the dropdown
|
|
360
|
+
if (this.activeDropdown && !this.activeDropdown.contains(e.target)) {
|
|
361
|
+
this.hideDropdown();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Position dropdown relative to match element
|
|
367
|
+
* @param {HTMLElement} dropdown - Dropdown element
|
|
368
|
+
* @param {HTMLElement} matchEl - Match element
|
|
369
|
+
*/
|
|
370
|
+
positionDropdown(dropdown, matchEl) {
|
|
371
|
+
const rect = matchEl.getBoundingClientRect();
|
|
372
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
373
|
+
const offset = this.options.dropdownOffset;
|
|
374
|
+
|
|
375
|
+
// Position above match by default (since input is at bottom)
|
|
376
|
+
let top = rect.top + window.scrollY - dropdownRect.height - offset;
|
|
377
|
+
let left = rect.left + window.scrollX;
|
|
378
|
+
|
|
379
|
+
// If dropdown goes off top edge, position below instead
|
|
380
|
+
if (top < window.scrollY) {
|
|
381
|
+
top = rect.bottom + window.scrollY + offset;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check if dropdown goes off right edge
|
|
385
|
+
if (left + dropdownRect.width > window.innerWidth) {
|
|
386
|
+
left = window.innerWidth - dropdownRect.width - 10;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
dropdown.style.top = `${top}px`;
|
|
390
|
+
dropdown.style.left = `${left}px`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Setup keyboard handlers for dropdown navigation
|
|
395
|
+
*/
|
|
396
|
+
setupKeyboardHandlers() {
|
|
397
|
+
// Remove old handler if exists
|
|
398
|
+
if (this.keyboardHandler) {
|
|
399
|
+
document.removeEventListener('keydown', this.keyboardHandler);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Create new handler
|
|
403
|
+
this.keyboardHandler = (e) => {
|
|
404
|
+
// Only handle if dropdown is active
|
|
405
|
+
if (!this.activeDropdown) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
switch (e.key) {
|
|
410
|
+
case 'ArrowDown':
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
this.moveDropdownSelection(1);
|
|
413
|
+
break;
|
|
414
|
+
case 'ArrowUp':
|
|
415
|
+
e.preventDefault();
|
|
416
|
+
this.moveDropdownSelection(-1);
|
|
417
|
+
break;
|
|
418
|
+
case 'Enter':
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
this.selectCurrentDropdownItem();
|
|
421
|
+
break;
|
|
422
|
+
case 'Escape':
|
|
423
|
+
e.preventDefault();
|
|
424
|
+
this.hideDropdown();
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Attach handler
|
|
430
|
+
document.addEventListener('keydown', this.keyboardHandler);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Move dropdown selection up or down
|
|
435
|
+
* @param {number} direction - 1 for down, -1 for up
|
|
436
|
+
*/
|
|
437
|
+
moveDropdownSelection(direction) {
|
|
438
|
+
if (!this.activeDropdown || !this.dropdownOptions) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const items = this.activeDropdown.querySelectorAll('.tq-dropdown-item');
|
|
443
|
+
const visibleItems = Array.from(items).filter(item => item.style.display !== 'none');
|
|
444
|
+
|
|
445
|
+
if (visibleItems.length === 0) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Find current selected visible index
|
|
450
|
+
let currentVisibleIndex = visibleItems.findIndex((item, index) => {
|
|
451
|
+
return Array.from(items).indexOf(item) === this.selectedDropdownIndex;
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Move to next/prev visible item
|
|
455
|
+
currentVisibleIndex += direction;
|
|
456
|
+
|
|
457
|
+
// Wrap around visible items only
|
|
458
|
+
if (currentVisibleIndex < 0) {
|
|
459
|
+
currentVisibleIndex = visibleItems.length - 1;
|
|
460
|
+
} else if (currentVisibleIndex >= visibleItems.length) {
|
|
461
|
+
currentVisibleIndex = 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Update selected index to the actual index
|
|
465
|
+
this.selectedDropdownIndex = Array.from(items).indexOf(visibleItems[currentVisibleIndex]);
|
|
466
|
+
|
|
467
|
+
// Update visual selection
|
|
468
|
+
this.updateDropdownSelection();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Update visual selection in dropdown
|
|
473
|
+
*/
|
|
474
|
+
updateDropdownSelection() {
|
|
475
|
+
if (!this.activeDropdown) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const items = this.activeDropdown.querySelectorAll('.tq-dropdown-item');
|
|
480
|
+
items.forEach((item, index) => {
|
|
481
|
+
if (index === this.selectedDropdownIndex) {
|
|
482
|
+
item.classList.add('tq-dropdown-item-selected');
|
|
483
|
+
// Scroll into view if needed
|
|
484
|
+
item.scrollIntoView({ block: 'nearest' });
|
|
485
|
+
|
|
486
|
+
// If this is a user input item, focus the input
|
|
487
|
+
if (item.getAttribute('data-user-input') === 'true') {
|
|
488
|
+
const input = item.querySelector('.tq-dropdown-user-input');
|
|
489
|
+
if (input) {
|
|
490
|
+
setTimeout(() => input.focus(), 0);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
item.classList.remove('tq-dropdown-item-selected');
|
|
495
|
+
|
|
496
|
+
// If this is a user input item, blur the input when navigating away
|
|
497
|
+
if (item.getAttribute('data-user-input') === 'true') {
|
|
498
|
+
const input = item.querySelector('.tq-dropdown-user-input');
|
|
499
|
+
if (input) {
|
|
500
|
+
input.blur();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Select the currently highlighted dropdown item
|
|
509
|
+
*/
|
|
510
|
+
selectCurrentDropdownItem() {
|
|
511
|
+
if (!this.dropdownOptions || !this.dropdownMatchData) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const items = this.activeDropdown.querySelectorAll('.tq-dropdown-item');
|
|
516
|
+
const selectedItem = items[this.selectedDropdownIndex];
|
|
517
|
+
|
|
518
|
+
// Check if this is a user input item
|
|
519
|
+
if (selectedItem && selectedItem.getAttribute('data-user-input') === 'true') {
|
|
520
|
+
// Focus the input field instead of selecting
|
|
521
|
+
const input = selectedItem.querySelector('.tq-dropdown-user-input');
|
|
522
|
+
if (input) {
|
|
523
|
+
input.focus();
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const selectedOption = this.dropdownOptions[this.selectedDropdownIndex];
|
|
529
|
+
this.handleDropdownSelect(selectedOption, this.dropdownMatchData);
|
|
530
|
+
this.hideDropdown();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Handle dropdown option selection
|
|
535
|
+
* @param {*} option - Selected option
|
|
536
|
+
* @param {Object} matchData - Match data
|
|
537
|
+
*/
|
|
538
|
+
handleDropdownSelect(option, matchData) {
|
|
539
|
+
console.log('[DropdownManager] Dropdown option selected:', option, 'for:', matchData.text);
|
|
540
|
+
|
|
541
|
+
// Check if option has on-select.display
|
|
542
|
+
if (option['on-select'] && option['on-select'].display && this.options.textarea) {
|
|
543
|
+
const displayText = option['on-select'].display;
|
|
544
|
+
const textarea = this.options.textarea;
|
|
545
|
+
const text = textarea.value;
|
|
546
|
+
|
|
547
|
+
// Find the trigger text position
|
|
548
|
+
const lines = text.split('\n');
|
|
549
|
+
const line = lines[matchData.line];
|
|
550
|
+
|
|
551
|
+
if (line) {
|
|
552
|
+
// Append to trigger text with "/" separator
|
|
553
|
+
const before = line.substring(0, matchData.col);
|
|
554
|
+
const after = line.substring(matchData.col + matchData.text.length);
|
|
555
|
+
const newText = matchData.text + '/' + displayText;
|
|
556
|
+
lines[matchData.line] = before + newText + after;
|
|
557
|
+
|
|
558
|
+
// Update textarea
|
|
559
|
+
textarea.value = lines.join('\n');
|
|
560
|
+
|
|
561
|
+
// Trigger input event to re-render
|
|
562
|
+
const inputEvent = new Event('input', { bubbles: true });
|
|
563
|
+
textarea.dispatchEvent(inputEvent);
|
|
564
|
+
|
|
565
|
+
console.log('[DropdownManager] Appended to', matchData.text, '→', newText);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Trigger callback
|
|
570
|
+
if (this.options.onWordClick) {
|
|
571
|
+
this.options.onWordClick({
|
|
572
|
+
...matchData,
|
|
573
|
+
selectedOption: option
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Cleanup
|
|
580
|
+
*/
|
|
581
|
+
cleanup() {
|
|
582
|
+
this.hideDropdown();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Destroy
|
|
587
|
+
*/
|
|
588
|
+
destroy() {
|
|
589
|
+
this.cleanup();
|
|
590
|
+
console.log('[DropdownManager] Destroyed');
|
|
591
|
+
}
|
|
592
|
+
}
|