@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,225 @@
|
|
|
1
|
+
// InteractionHandler - Orchestrates hover bubbles and click interactions on matched words
|
|
2
|
+
// Delegates to BubbleManager and DropdownManager for specific functionality
|
|
3
|
+
|
|
4
|
+
import BubbleManager from './BubbleManager.js';
|
|
5
|
+
import DropdownManager from './DropdownManager.js';
|
|
6
|
+
|
|
7
|
+
export default class InteractionHandler {
|
|
8
|
+
/**
|
|
9
|
+
* Create interaction handler
|
|
10
|
+
* @param {HTMLElement} overlay - Overlay element containing matches
|
|
11
|
+
* @param {Object} options - Configuration
|
|
12
|
+
*/
|
|
13
|
+
constructor(overlay, options = {}) {
|
|
14
|
+
this.overlay = overlay;
|
|
15
|
+
this.options = {
|
|
16
|
+
bubbleDelay: options.bubbleDelay || 200,
|
|
17
|
+
onWordClick: options.onWordClick || null,
|
|
18
|
+
onWordHover: options.onWordHover || null,
|
|
19
|
+
styleManager: options.styleManager || null,
|
|
20
|
+
commandHandlers: options.commandHandlers || null,
|
|
21
|
+
textarea: options.textarea || null,
|
|
22
|
+
...options
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
this.hoverTimeout = null;
|
|
26
|
+
|
|
27
|
+
// Create manager instances
|
|
28
|
+
this.bubbleManager = new BubbleManager({
|
|
29
|
+
bubbleDelay: this.options.bubbleDelay,
|
|
30
|
+
styleManager: this.options.styleManager,
|
|
31
|
+
commandHandlers: this.options.commandHandlers
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.dropdownManager = new DropdownManager({
|
|
35
|
+
styleManager: this.options.styleManager,
|
|
36
|
+
textarea: this.options.textarea,
|
|
37
|
+
onWordClick: this.options.onWordClick,
|
|
38
|
+
dropdownOffset: this.options.dropdownOffset
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log('[InteractionHandler] Initialized');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Update handlers after overlay re-render
|
|
46
|
+
* Attach event listeners to all .tq-match elements
|
|
47
|
+
*/
|
|
48
|
+
update() {
|
|
49
|
+
// Remove old handlers (if any)
|
|
50
|
+
this.cleanup();
|
|
51
|
+
|
|
52
|
+
// Find all match elements
|
|
53
|
+
const matches = this.overlay.querySelectorAll('.tq-match');
|
|
54
|
+
|
|
55
|
+
matches.forEach(matchEl => {
|
|
56
|
+
const behavior = matchEl.getAttribute('data-behavior');
|
|
57
|
+
|
|
58
|
+
// Hover events for bubbles
|
|
59
|
+
matchEl.addEventListener('mouseenter', (e) => this.handleMouseEnter(e, matchEl));
|
|
60
|
+
matchEl.addEventListener('mouseleave', (e) => this.handleMouseLeave(e, matchEl));
|
|
61
|
+
|
|
62
|
+
// Click events for dropdowns/actions
|
|
63
|
+
matchEl.addEventListener('click', (e) => this.handleClick(e, matchEl));
|
|
64
|
+
|
|
65
|
+
// Add hover class for CSS styling
|
|
66
|
+
matchEl.classList.add('tq-hoverable');
|
|
67
|
+
|
|
68
|
+
// Auto-show dropdown for dropdown-behavior matches
|
|
69
|
+
if (behavior === 'dropdown') {
|
|
70
|
+
const matchData = this.getMatchData(matchEl);
|
|
71
|
+
// Only show if this isn't the currently active dropdown match
|
|
72
|
+
if (!this.dropdownManager.activeDropdownMatch ||
|
|
73
|
+
this.dropdownManager.activeDropdownMatch.textContent !== matchEl.textContent) {
|
|
74
|
+
this.dropdownManager.showDropdown(matchEl, matchData);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log('[InteractionHandler] Updated with', matches.length, 'interactive elements');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle mouse enter on a match
|
|
84
|
+
* @param {Event} e - Mouse event
|
|
85
|
+
* @param {HTMLElement} matchEl - Match element
|
|
86
|
+
*/
|
|
87
|
+
handleMouseEnter(e, matchEl) {
|
|
88
|
+
const behavior = matchEl.getAttribute('data-behavior');
|
|
89
|
+
|
|
90
|
+
// Only show bubble if behavior is 'bubble' or 'hover'
|
|
91
|
+
if (behavior === 'bubble' || behavior === 'hover') {
|
|
92
|
+
// Clear any existing timeout
|
|
93
|
+
if (this.hoverTimeout) {
|
|
94
|
+
clearTimeout(this.hoverTimeout);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Delay bubble appearance
|
|
98
|
+
this.hoverTimeout = setTimeout(() => {
|
|
99
|
+
const matchData = this.getMatchData(matchEl);
|
|
100
|
+
this.bubbleManager.showBubble(matchEl, matchData);
|
|
101
|
+
}, this.options.bubbleDelay);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Callback
|
|
105
|
+
if (this.options.onWordHover) {
|
|
106
|
+
const matchData = this.getMatchData(matchEl);
|
|
107
|
+
this.options.onWordHover(matchData);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Handle mouse leave from a match
|
|
113
|
+
* @param {Event} e - Mouse event
|
|
114
|
+
* @param {HTMLElement} matchEl - Match element
|
|
115
|
+
*/
|
|
116
|
+
handleMouseLeave(e, matchEl) {
|
|
117
|
+
// Clear hover timeout
|
|
118
|
+
if (this.hoverTimeout) {
|
|
119
|
+
clearTimeout(this.hoverTimeout);
|
|
120
|
+
this.hoverTimeout = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Don't immediately hide bubble - let user hover over it
|
|
124
|
+
// Bubble will auto-hide when mouse leaves bubble area
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Handle click on a match
|
|
129
|
+
* @param {Event} e - Click event
|
|
130
|
+
* @param {HTMLElement} matchEl - Match element
|
|
131
|
+
*/
|
|
132
|
+
handleClick(e, matchEl) {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
e.stopPropagation();
|
|
135
|
+
|
|
136
|
+
const behavior = matchEl.getAttribute('data-behavior');
|
|
137
|
+
const matchData = this.getMatchData(matchEl);
|
|
138
|
+
|
|
139
|
+
console.log('[InteractionHandler] Match clicked:', matchData);
|
|
140
|
+
|
|
141
|
+
// Handle different behaviors
|
|
142
|
+
if (behavior === 'dropdown') {
|
|
143
|
+
// Toggle dropdown - close if already open for this match, otherwise show
|
|
144
|
+
if (this.dropdownManager.activeDropdownMatch === matchEl) {
|
|
145
|
+
this.dropdownManager.hideDropdown();
|
|
146
|
+
} else {
|
|
147
|
+
this.dropdownManager.showDropdown(matchEl, matchData);
|
|
148
|
+
}
|
|
149
|
+
} else if (behavior === 'action') {
|
|
150
|
+
// Custom action callback
|
|
151
|
+
if (this.options.onWordClick) {
|
|
152
|
+
this.options.onWordClick(matchData);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Always trigger callback (unless it's a dropdown toggle to close)
|
|
157
|
+
if (this.options.onWordClick && !(behavior === 'dropdown' && this.dropdownManager.activeDropdownMatch === matchEl)) {
|
|
158
|
+
this.options.onWordClick(matchData);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Hide dropdown (exposed for external use, e.g., when textarea loses focus)
|
|
164
|
+
*/
|
|
165
|
+
hideDropdown() {
|
|
166
|
+
this.dropdownManager.hideDropdown();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract match data from element
|
|
171
|
+
* @param {HTMLElement} matchEl - Match element
|
|
172
|
+
* @returns {Object} Match data
|
|
173
|
+
*/
|
|
174
|
+
getMatchData(matchEl) {
|
|
175
|
+
// Parse intent JSON if available
|
|
176
|
+
let intent = null;
|
|
177
|
+
const intentStr = matchEl.getAttribute('data-intent');
|
|
178
|
+
if (intentStr) {
|
|
179
|
+
try {
|
|
180
|
+
intent = JSON.parse(intentStr);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.warn('[InteractionHandler] Failed to parse intent JSON:', e);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
text: matchEl.getAttribute('data-match-text'),
|
|
188
|
+
line: parseInt(matchEl.getAttribute('data-line')),
|
|
189
|
+
col: parseInt(matchEl.getAttribute('data-col')),
|
|
190
|
+
commandType: matchEl.getAttribute('data-command-type'),
|
|
191
|
+
commandPath: matchEl.getAttribute('data-command-path'),
|
|
192
|
+
intentPath: matchEl.getAttribute('data-intent-path'),
|
|
193
|
+
intent: intent,
|
|
194
|
+
command: {
|
|
195
|
+
id: matchEl.getAttribute('data-command-id'),
|
|
196
|
+
type: matchEl.getAttribute('data-command-type'),
|
|
197
|
+
behavior: matchEl.getAttribute('data-behavior')
|
|
198
|
+
},
|
|
199
|
+
element: matchEl
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Cleanup event listeners
|
|
205
|
+
*/
|
|
206
|
+
cleanup() {
|
|
207
|
+
this.bubbleManager.cleanup();
|
|
208
|
+
this.dropdownManager.cleanup();
|
|
209
|
+
|
|
210
|
+
if (this.hoverTimeout) {
|
|
211
|
+
clearTimeout(this.hoverTimeout);
|
|
212
|
+
this.hoverTimeout = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Destroy handler
|
|
218
|
+
*/
|
|
219
|
+
destroy() {
|
|
220
|
+
this.cleanup();
|
|
221
|
+
this.bubbleManager.destroy();
|
|
222
|
+
this.dropdownManager.destroy();
|
|
223
|
+
console.log('[InteractionHandler] Destroyed');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// OverlayRenderer - Renders text with styled matches in a line-based overlay
|
|
2
|
+
// Much simpler than grid approach - each line is a div, matches are spans
|
|
3
|
+
|
|
4
|
+
export default class OverlayRenderer {
|
|
5
|
+
/**
|
|
6
|
+
* Create a renderer
|
|
7
|
+
* @param {HTMLElement} overlay - Overlay container element
|
|
8
|
+
* @param {Object} options - Rendering options
|
|
9
|
+
*/
|
|
10
|
+
constructor(overlay, options = {}) {
|
|
11
|
+
this.overlay = overlay;
|
|
12
|
+
this.options = {
|
|
13
|
+
theme: options.theme || 'light',
|
|
14
|
+
commandHandlers: options.commandHandlers || null,
|
|
15
|
+
...options
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
console.log('[OverlayRenderer] Initialized');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render text with matches
|
|
23
|
+
* @param {string} text - Text content
|
|
24
|
+
* @param {Array} matches - Array of match objects from CommandScanner
|
|
25
|
+
*/
|
|
26
|
+
render(text, matches = []) {
|
|
27
|
+
// Split text into lines
|
|
28
|
+
const lines = text.split('\n');
|
|
29
|
+
|
|
30
|
+
// Build HTML for each line
|
|
31
|
+
const linesHTML = lines.map((line, lineIndex) => {
|
|
32
|
+
return this.renderLine(line, lineIndex, matches);
|
|
33
|
+
}).join('');
|
|
34
|
+
|
|
35
|
+
// Update overlay
|
|
36
|
+
this.overlay.innerHTML = linesHTML;
|
|
37
|
+
|
|
38
|
+
console.log('[OverlayRenderer] Rendered', lines.length, 'lines with', matches.length, 'matches');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Render a single line with matches
|
|
43
|
+
* @param {string} line - Line text
|
|
44
|
+
* @param {number} lineIndex - Line number (0-indexed)
|
|
45
|
+
* @param {Array} matches - All matches from scanner
|
|
46
|
+
* @returns {string} HTML for line
|
|
47
|
+
*/
|
|
48
|
+
renderLine(line, lineIndex, matches) {
|
|
49
|
+
// Find matches on this line
|
|
50
|
+
const lineMatches = this.getMatchesForLine(line, lineIndex, matches);
|
|
51
|
+
|
|
52
|
+
if (lineMatches.length === 0) {
|
|
53
|
+
// No matches - render plain line
|
|
54
|
+
return `<div class="tq-line">${this.escapeHtml(line) || ' '}</div>`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Sort matches by start position
|
|
58
|
+
lineMatches.sort((a, b) => a.start - b.start);
|
|
59
|
+
|
|
60
|
+
// Build HTML with matches as spans
|
|
61
|
+
let html = '<div class="tq-line">';
|
|
62
|
+
let lastIndex = 0;
|
|
63
|
+
|
|
64
|
+
for (const match of lineMatches) {
|
|
65
|
+
// Add text before match
|
|
66
|
+
if (match.start > lastIndex) {
|
|
67
|
+
html += this.escapeHtml(line.substring(lastIndex, match.start));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Add match as styled span
|
|
71
|
+
const matchText = line.substring(match.start, match.end);
|
|
72
|
+
const classes = this.getMatchClasses(match);
|
|
73
|
+
const dataAttrs = this.getMatchDataAttributes(match);
|
|
74
|
+
const inlineStyles = this.getMatchInlineStyles(match);
|
|
75
|
+
|
|
76
|
+
html += `<span class="${classes}" ${dataAttrs} style="${inlineStyles}">${this.escapeHtml(matchText)}</span>`;
|
|
77
|
+
|
|
78
|
+
lastIndex = match.end;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add remaining text after last match
|
|
82
|
+
if (lastIndex < line.length) {
|
|
83
|
+
html += this.escapeHtml(line.substring(lastIndex));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle empty lines
|
|
87
|
+
if (line.length === 0) {
|
|
88
|
+
html += ' ';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
html += '</div>';
|
|
92
|
+
|
|
93
|
+
return html;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get matches that apply to a specific line
|
|
98
|
+
* @param {string} line - Line text
|
|
99
|
+
* @param {number} lineIndex - Line number
|
|
100
|
+
* @param {Array} matches - All matches
|
|
101
|
+
* @returns {Array} Matches for this line with adjusted positions
|
|
102
|
+
*/
|
|
103
|
+
getMatchesForLine(line, lineIndex, matches) {
|
|
104
|
+
const lineMatches = [];
|
|
105
|
+
|
|
106
|
+
// Calculate absolute position of this line in the full text
|
|
107
|
+
// (we'll need to know line starts to filter matches)
|
|
108
|
+
// For now, matches already have line info from scanner
|
|
109
|
+
|
|
110
|
+
for (const match of matches) {
|
|
111
|
+
if (match.line === lineIndex) {
|
|
112
|
+
lineMatches.push({
|
|
113
|
+
...match,
|
|
114
|
+
start: match.col, // Column position on this line
|
|
115
|
+
end: match.col + match.length
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lineMatches;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get CSS classes for a match
|
|
125
|
+
* @param {Object} match - Match object
|
|
126
|
+
* @returns {string} Space-separated class names
|
|
127
|
+
*/
|
|
128
|
+
getMatchClasses(match) {
|
|
129
|
+
const classes = ['tq-match'];
|
|
130
|
+
|
|
131
|
+
// Add command type as class
|
|
132
|
+
if (match.command && match.command.type) {
|
|
133
|
+
classes.push(`tq-match-${match.command.type}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add behavior as class (bubble, dropdown, etc.)
|
|
137
|
+
if (match.command && match.command.behavior) {
|
|
138
|
+
classes.push(`tq-behavior-${match.command.behavior}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return classes.join(' ');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get data attributes for a match
|
|
146
|
+
* @param {Object} match - Match object
|
|
147
|
+
* @returns {string} Data attributes string
|
|
148
|
+
*/
|
|
149
|
+
getMatchDataAttributes(match) {
|
|
150
|
+
const attrs = [];
|
|
151
|
+
|
|
152
|
+
// Store match text
|
|
153
|
+
attrs.push(`data-match-text="${this.escapeAttr(match.text)}"`);
|
|
154
|
+
|
|
155
|
+
// Store command info
|
|
156
|
+
if (match.command) {
|
|
157
|
+
attrs.push(`data-command-id="${this.escapeAttr(match.command.id || '')}"`);
|
|
158
|
+
attrs.push(`data-command-type="${this.escapeAttr(match.command.commandType || '')}"`);
|
|
159
|
+
attrs.push(`data-command-path="${this.escapeAttr(match.command.commandPath || '')}"`);
|
|
160
|
+
attrs.push(`data-intent-path="${this.escapeAttr(match.command.intentPath || '')}"`);
|
|
161
|
+
|
|
162
|
+
// Store intent info as JSON for InteractionHandler
|
|
163
|
+
if (match.command.intent) {
|
|
164
|
+
attrs.push(`data-intent='${this.escapeAttr(JSON.stringify(match.command.intent))}'`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Determine behavior based on handler properties
|
|
168
|
+
if (match.command.intent && match.command.intent.handler) {
|
|
169
|
+
const handler = match.command.intent.handler;
|
|
170
|
+
if (handler.options && Array.isArray(handler.options) && handler.options.length > 0) {
|
|
171
|
+
attrs.push(`data-behavior="dropdown"`);
|
|
172
|
+
} else if (handler.message || handler['message-content'] || match.command.intent.description) {
|
|
173
|
+
attrs.push(`data-behavior="bubble"`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Store position info
|
|
179
|
+
attrs.push(`data-line="${match.line}"`);
|
|
180
|
+
attrs.push(`data-col="${match.col}"`);
|
|
181
|
+
|
|
182
|
+
return attrs.join(' ');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get inline styles for a match based on command type or message-state
|
|
187
|
+
* @param {Object} match - Match object
|
|
188
|
+
* @returns {string} Inline style string
|
|
189
|
+
*/
|
|
190
|
+
getMatchInlineStyles(match) {
|
|
191
|
+
if (!this.options.commandHandlers || !match.command) {
|
|
192
|
+
return 'pointer-events: auto; cursor: pointer;';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const commandType = match.command.commandType;
|
|
196
|
+
|
|
197
|
+
// Create matchData with intent info for message-state lookup
|
|
198
|
+
const matchData = {
|
|
199
|
+
intent: match.command.intent,
|
|
200
|
+
commandType: commandType
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const styles = this.options.commandHandlers.getStyles(commandType, matchData);
|
|
204
|
+
|
|
205
|
+
// Convert style object to CSS string
|
|
206
|
+
const styleStr = Object.entries(styles)
|
|
207
|
+
.map(([key, value]) => {
|
|
208
|
+
// Convert camelCase to kebab-case
|
|
209
|
+
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
210
|
+
return `${cssKey}: ${value}`;
|
|
211
|
+
})
|
|
212
|
+
.join('; ');
|
|
213
|
+
|
|
214
|
+
return styleStr + '; pointer-events: auto;';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Escape HTML to prevent XSS
|
|
219
|
+
* @param {string} text - Text to escape
|
|
220
|
+
* @returns {string} Escaped text
|
|
221
|
+
*/
|
|
222
|
+
escapeHtml(text) {
|
|
223
|
+
const div = document.createElement('div');
|
|
224
|
+
div.textContent = text;
|
|
225
|
+
return div.innerHTML;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Escape attribute value
|
|
230
|
+
* @param {string} text - Text to escape
|
|
231
|
+
* @returns {string} Escaped text
|
|
232
|
+
*/
|
|
233
|
+
escapeAttr(text) {
|
|
234
|
+
return String(text)
|
|
235
|
+
.replace(/&/g, '&')
|
|
236
|
+
.replace(/"/g, '"')
|
|
237
|
+
.replace(/'/g, ''')
|
|
238
|
+
.replace(/</g, '<')
|
|
239
|
+
.replace(/>/g, '>');
|
|
240
|
+
}
|
|
241
|
+
}
|