@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,402 @@
|
|
|
1
|
+
// TrustQuery - Lightweight library to make textareas interactive
|
|
2
|
+
// Turns matching words into interactive elements with hover bubbles and click actions
|
|
3
|
+
|
|
4
|
+
import OverlayRenderer from './OverlayRenderer.js';
|
|
5
|
+
import CommandScanner from './CommandScanner.js';
|
|
6
|
+
import InteractionHandler from './InteractionHandler.js';
|
|
7
|
+
import StyleManager from './StyleManager.js';
|
|
8
|
+
import CommandHandlerRegistry from './CommandHandlers.js';
|
|
9
|
+
import AutoGrow from './AutoGrow.js';
|
|
10
|
+
|
|
11
|
+
export default class TrustQuery {
|
|
12
|
+
// Store all instances
|
|
13
|
+
static instances = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize TrustQuery on a textarea
|
|
17
|
+
* @param {string|HTMLElement} textareaId - ID or element of textarea
|
|
18
|
+
* @param {Object} options - Configuration options
|
|
19
|
+
* @returns {TrustQuery} Instance
|
|
20
|
+
*/
|
|
21
|
+
static init(textareaId, options = {}) {
|
|
22
|
+
const textarea = typeof textareaId === 'string'
|
|
23
|
+
? document.getElementById(textareaId)
|
|
24
|
+
: textareaId;
|
|
25
|
+
|
|
26
|
+
if (!textarea) {
|
|
27
|
+
console.error('[TrustQuery] Textarea not found:', textareaId);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if already initialized
|
|
32
|
+
const existingInstance = TrustQuery.instances.get(textarea);
|
|
33
|
+
if (existingInstance) {
|
|
34
|
+
console.warn('[TrustQuery] Already initialized on this textarea, returning existing instance');
|
|
35
|
+
return existingInstance;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Create new instance
|
|
39
|
+
const instance = new TrustQuery(textarea, options);
|
|
40
|
+
TrustQuery.instances.set(textarea, instance);
|
|
41
|
+
|
|
42
|
+
console.log('[TrustQuery] Initialized successfully on:', textarea.id || textarea);
|
|
43
|
+
return instance;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get existing instance
|
|
48
|
+
* @param {string|HTMLElement} textareaId - ID or element
|
|
49
|
+
* @returns {TrustQuery|null} Instance or null
|
|
50
|
+
*/
|
|
51
|
+
static getInstance(textareaId) {
|
|
52
|
+
const textarea = typeof textareaId === 'string'
|
|
53
|
+
? document.getElementById(textareaId)
|
|
54
|
+
: textareaId;
|
|
55
|
+
return TrustQuery.instances.get(textarea) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a TrustQuery instance
|
|
60
|
+
* @param {HTMLElement} textarea - Textarea element
|
|
61
|
+
* @param {Object} options - Configuration
|
|
62
|
+
*/
|
|
63
|
+
constructor(textarea, options = {}) {
|
|
64
|
+
this.textarea = textarea;
|
|
65
|
+
|
|
66
|
+
// Normalize options (support both old and new API)
|
|
67
|
+
this.options = this.normalizeOptions(options);
|
|
68
|
+
|
|
69
|
+
this.commandMap = null;
|
|
70
|
+
this.isReady = false;
|
|
71
|
+
this.features = {};
|
|
72
|
+
|
|
73
|
+
// Initialize components
|
|
74
|
+
this.init();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalize options - support both old and new API
|
|
79
|
+
* @param {Object} options - Raw options
|
|
80
|
+
* @returns {Object} Normalized options
|
|
81
|
+
*/
|
|
82
|
+
normalizeOptions(options) {
|
|
83
|
+
// New API structure
|
|
84
|
+
const triggerMap = options.triggerMap || {};
|
|
85
|
+
const features = options.features || {};
|
|
86
|
+
const ui = options.ui || {};
|
|
87
|
+
const events = options.events || {};
|
|
88
|
+
|
|
89
|
+
// Build normalized config (backward compatible)
|
|
90
|
+
return {
|
|
91
|
+
// Trigger map configuration
|
|
92
|
+
commandMapUrl: triggerMap.url || options.commandMapUrl || null,
|
|
93
|
+
commandMap: triggerMap.data || options.commandMap || null,
|
|
94
|
+
autoLoadCommandMap: options.autoLoadCommandMap !== false,
|
|
95
|
+
triggerMapSource: triggerMap.source || (triggerMap.url ? 'url' : triggerMap.data ? 'inline' : null),
|
|
96
|
+
triggerMapApi: triggerMap.api || null,
|
|
97
|
+
|
|
98
|
+
// Features
|
|
99
|
+
autoGrow: features.autoGrow || false,
|
|
100
|
+
autoGrowMaxHeight: features.maxHeight || 300,
|
|
101
|
+
debug: features.debug || false,
|
|
102
|
+
|
|
103
|
+
// UI settings
|
|
104
|
+
theme: ui.theme || options.theme || 'light',
|
|
105
|
+
bubbleDelay: ui.bubbleDelay !== undefined ? ui.bubbleDelay : (options.bubbleDelay || 200),
|
|
106
|
+
dropdownOffset: ui.dropdownOffset !== undefined ? ui.dropdownOffset : (options.dropdownOffset || 28),
|
|
107
|
+
|
|
108
|
+
// Events (callbacks)
|
|
109
|
+
onWordClick: events.onWordClick || options.onWordClick || null,
|
|
110
|
+
onWordHover: events.onWordHover || options.onWordHover || null,
|
|
111
|
+
|
|
112
|
+
// Theme/style options (passed to StyleManager)
|
|
113
|
+
backgroundColor: ui.backgroundColor || options.backgroundColor,
|
|
114
|
+
textColor: ui.textColor || options.textColor,
|
|
115
|
+
caretColor: ui.caretColor || options.caretColor,
|
|
116
|
+
borderColor: ui.borderColor || options.borderColor,
|
|
117
|
+
borderColorFocus: ui.borderColorFocus || options.borderColorFocus,
|
|
118
|
+
matchBackgroundColor: ui.matchBackgroundColor || options.matchBackgroundColor,
|
|
119
|
+
matchTextColor: ui.matchTextColor || options.matchTextColor,
|
|
120
|
+
matchHoverBackgroundColor: ui.matchHoverBackgroundColor || options.matchHoverBackgroundColor,
|
|
121
|
+
fontFamily: ui.fontFamily || options.fontFamily,
|
|
122
|
+
fontSize: ui.fontSize || options.fontSize,
|
|
123
|
+
lineHeight: ui.lineHeight || options.lineHeight,
|
|
124
|
+
|
|
125
|
+
...options
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Initialize all components
|
|
131
|
+
*/
|
|
132
|
+
async init() {
|
|
133
|
+
console.log('[TrustQuery] Starting initialization...');
|
|
134
|
+
|
|
135
|
+
// Initialize command handler registry
|
|
136
|
+
this.commandHandlers = new CommandHandlerRegistry();
|
|
137
|
+
|
|
138
|
+
// Initialize style manager (handles all inline styling)
|
|
139
|
+
this.styleManager = new StyleManager(this.options);
|
|
140
|
+
|
|
141
|
+
// Create wrapper and overlay structure
|
|
142
|
+
this.createOverlayStructure();
|
|
143
|
+
|
|
144
|
+
// Initialize renderer
|
|
145
|
+
this.renderer = new OverlayRenderer(this.overlay, {
|
|
146
|
+
theme: this.options.theme,
|
|
147
|
+
commandHandlers: this.commandHandlers // Pass handlers for styling
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Initialize scanner (will be configured when command map loads)
|
|
151
|
+
this.scanner = new CommandScanner();
|
|
152
|
+
|
|
153
|
+
// Initialize interaction handler
|
|
154
|
+
this.interactionHandler = new InteractionHandler(this.overlay, {
|
|
155
|
+
bubbleDelay: this.options.bubbleDelay,
|
|
156
|
+
dropdownOffset: this.options.dropdownOffset,
|
|
157
|
+
onWordClick: this.options.onWordClick,
|
|
158
|
+
onWordHover: this.options.onWordHover,
|
|
159
|
+
styleManager: this.styleManager, // Pass style manager for bubbles/dropdowns
|
|
160
|
+
commandHandlers: this.commandHandlers, // Pass handlers for bubble content
|
|
161
|
+
textarea: this.textarea // Pass textarea for on-select display updates
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Initialize features
|
|
165
|
+
this.initializeFeatures();
|
|
166
|
+
|
|
167
|
+
// Setup textarea event listeners
|
|
168
|
+
this.setupTextareaListeners();
|
|
169
|
+
|
|
170
|
+
// Load command map
|
|
171
|
+
if (this.options.autoLoadCommandMap) {
|
|
172
|
+
await this.loadCommandMap();
|
|
173
|
+
} else if (this.options.commandMap) {
|
|
174
|
+
this.updateCommandMap(this.options.commandMap);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Initial render
|
|
178
|
+
this.render();
|
|
179
|
+
|
|
180
|
+
this.isReady = true;
|
|
181
|
+
|
|
182
|
+
// Auto-focus textarea to show cursor
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
this.textarea.focus();
|
|
185
|
+
}, 100);
|
|
186
|
+
|
|
187
|
+
console.log('[TrustQuery] Initialization complete');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Initialize optional features based on configuration
|
|
192
|
+
*/
|
|
193
|
+
initializeFeatures() {
|
|
194
|
+
// Auto-grow textarea feature
|
|
195
|
+
if (this.options.autoGrow) {
|
|
196
|
+
this.features.autoGrow = new AutoGrow(this.textarea, {
|
|
197
|
+
maxHeight: this.options.autoGrowMaxHeight,
|
|
198
|
+
minHeight: 44
|
|
199
|
+
});
|
|
200
|
+
console.log('[TrustQuery] AutoGrow feature enabled');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Debug logging feature
|
|
204
|
+
if (this.options.debug) {
|
|
205
|
+
this.enableDebugLogging();
|
|
206
|
+
console.log('[TrustQuery] Debug logging enabled');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Enable debug logging for events
|
|
212
|
+
*/
|
|
213
|
+
enableDebugLogging() {
|
|
214
|
+
const originalOnWordHover = this.options.onWordHover;
|
|
215
|
+
const originalOnWordClick = this.options.onWordClick;
|
|
216
|
+
|
|
217
|
+
this.options.onWordHover = (matchData) => {
|
|
218
|
+
console.log('[TrustQuery Debug] Word Hover:', matchData);
|
|
219
|
+
if (originalOnWordHover) originalOnWordHover(matchData);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
this.options.onWordClick = (matchData) => {
|
|
223
|
+
console.log('[TrustQuery Debug] Word Click:', matchData);
|
|
224
|
+
if (originalOnWordClick) originalOnWordClick(matchData);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Create the overlay structure
|
|
230
|
+
*/
|
|
231
|
+
createOverlayStructure() {
|
|
232
|
+
// Create wrapper to contain both textarea and overlay
|
|
233
|
+
const wrapper = document.createElement('div');
|
|
234
|
+
wrapper.className = 'tq-wrapper';
|
|
235
|
+
|
|
236
|
+
// Wrap textarea
|
|
237
|
+
this.textarea.parentNode.insertBefore(wrapper, this.textarea);
|
|
238
|
+
wrapper.appendChild(this.textarea);
|
|
239
|
+
|
|
240
|
+
// Add TrustQuery class to textarea
|
|
241
|
+
this.textarea.classList.add('tq-textarea');
|
|
242
|
+
|
|
243
|
+
// Create overlay
|
|
244
|
+
this.overlay = document.createElement('div');
|
|
245
|
+
this.overlay.className = 'tq-overlay';
|
|
246
|
+
wrapper.appendChild(this.overlay);
|
|
247
|
+
|
|
248
|
+
// Store wrapper reference
|
|
249
|
+
this.wrapper = wrapper;
|
|
250
|
+
|
|
251
|
+
// Apply all inline styles via StyleManager
|
|
252
|
+
this.styleManager.applyAllStyles(wrapper, this.textarea, this.overlay);
|
|
253
|
+
|
|
254
|
+
// Show textarea now that structure is ready (prevents FOUC)
|
|
255
|
+
this.textarea.style.opacity = '1';
|
|
256
|
+
|
|
257
|
+
console.log('[TrustQuery] Overlay structure created with inline styles');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Setup textarea event listeners
|
|
262
|
+
*/
|
|
263
|
+
setupTextareaListeners() {
|
|
264
|
+
// Input event - re-render on content change
|
|
265
|
+
this.textarea.addEventListener('input', () => {
|
|
266
|
+
this.render();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Scroll event - sync overlay scroll with textarea
|
|
270
|
+
this.textarea.addEventListener('scroll', () => {
|
|
271
|
+
this.overlay.scrollTop = this.textarea.scrollTop;
|
|
272
|
+
this.overlay.scrollLeft = this.textarea.scrollLeft;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Focus/blur events - add/remove focus class
|
|
276
|
+
this.textarea.addEventListener('focus', () => {
|
|
277
|
+
this.wrapper.classList.add('tq-focused');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
this.textarea.addEventListener('blur', (e) => {
|
|
281
|
+
// Close dropdown when textarea loses focus (unless interacting with dropdown)
|
|
282
|
+
if (this.interactionHandler) {
|
|
283
|
+
// Use setTimeout to let the new focus target be set and check if clicking on dropdown
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
const activeElement = document.activeElement;
|
|
286
|
+
const isDropdownRelated = activeElement && (
|
|
287
|
+
activeElement.classList.contains('tq-dropdown-filter') ||
|
|
288
|
+
activeElement.closest('.tq-dropdown') // Check if clicking anywhere in dropdown
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Only close if not interacting with dropdown
|
|
292
|
+
if (!isDropdownRelated) {
|
|
293
|
+
this.interactionHandler.hideDropdown();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Remove focus class only if we're truly leaving the component
|
|
297
|
+
if (!isDropdownRelated) {
|
|
298
|
+
this.wrapper.classList.remove('tq-focused');
|
|
299
|
+
}
|
|
300
|
+
}, 0);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
console.log('[TrustQuery] Textarea listeners attached');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Load command map (static tql-triggers.json or from URL)
|
|
309
|
+
*/
|
|
310
|
+
async loadCommandMap() {
|
|
311
|
+
try {
|
|
312
|
+
// Default to static file if no URL provided
|
|
313
|
+
const url = this.options.commandMapUrl || '/trustquery/tql-triggers.json';
|
|
314
|
+
|
|
315
|
+
console.log('[TrustQuery] Loading trigger map from:', url);
|
|
316
|
+
const response = await fetch(url);
|
|
317
|
+
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const data = await response.json();
|
|
323
|
+
this.updateCommandMap(data);
|
|
324
|
+
|
|
325
|
+
console.log('[TrustQuery] Trigger map loaded successfully');
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('[TrustQuery] Failed to load trigger map:', error);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Update command map
|
|
333
|
+
* @param {Object} commandMap - New command map
|
|
334
|
+
*/
|
|
335
|
+
updateCommandMap(commandMap) {
|
|
336
|
+
this.commandMap = commandMap;
|
|
337
|
+
this.scanner.setCommandMap(commandMap);
|
|
338
|
+
console.log('[TrustQuery] Command map updated');
|
|
339
|
+
|
|
340
|
+
// Re-render with new command map
|
|
341
|
+
if (this.isReady) {
|
|
342
|
+
this.render();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Render the overlay with styled text
|
|
348
|
+
*/
|
|
349
|
+
render() {
|
|
350
|
+
const text = this.textarea.value;
|
|
351
|
+
|
|
352
|
+
// Scan text for matches
|
|
353
|
+
const matches = this.scanner.scan(text);
|
|
354
|
+
|
|
355
|
+
// Render overlay with matches
|
|
356
|
+
this.renderer.render(text, matches);
|
|
357
|
+
|
|
358
|
+
// Update interaction handler with new elements
|
|
359
|
+
this.interactionHandler.update();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Destroy instance and cleanup
|
|
364
|
+
*/
|
|
365
|
+
destroy() {
|
|
366
|
+
console.log('[TrustQuery] Destroying instance');
|
|
367
|
+
|
|
368
|
+
// Remove event listeners
|
|
369
|
+
this.textarea.removeEventListener('input', this.render);
|
|
370
|
+
this.textarea.removeEventListener('scroll', this.syncScroll);
|
|
371
|
+
|
|
372
|
+
// Cleanup interaction handler
|
|
373
|
+
if (this.interactionHandler) {
|
|
374
|
+
this.interactionHandler.destroy();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Unwrap textarea
|
|
378
|
+
const parent = this.wrapper.parentNode;
|
|
379
|
+
parent.insertBefore(this.textarea, this.wrapper);
|
|
380
|
+
parent.removeChild(this.wrapper);
|
|
381
|
+
|
|
382
|
+
// Remove from instances
|
|
383
|
+
TrustQuery.instances.delete(this.textarea);
|
|
384
|
+
|
|
385
|
+
console.log('[TrustQuery] Destroyed');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get current value
|
|
390
|
+
*/
|
|
391
|
+
getValue() {
|
|
392
|
+
return this.textarea.value;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Set value
|
|
397
|
+
*/
|
|
398
|
+
setValue(value) {
|
|
399
|
+
this.textarea.value = value;
|
|
400
|
+
this.render();
|
|
401
|
+
}
|
|
402
|
+
}
|