@trustquery/browser 0.3.0 → 0.3.2
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 +1371 -1
- package/dist/trustquery.js.map +1 -1
- package/package.json +1 -1
- package/src/AttachmentManager.js +516 -0
- package/src/AttachmentStyleManager.js +174 -0
- package/src/CSVModalManager.js +417 -0
- package/src/CSVModalStyleManager.js +259 -0
- package/src/TrustQuery.js +14 -0
package/package.json
CHANGED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
// AttachmentManager - Handles CSV file attachments with drag & drop
|
|
2
|
+
// Single responsibility: manage attachment state and file operations
|
|
3
|
+
|
|
4
|
+
export default class AttachmentManager {
|
|
5
|
+
/**
|
|
6
|
+
* Create attachment manager
|
|
7
|
+
* @param {Object} options - Configuration options
|
|
8
|
+
*/
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.options = {
|
|
11
|
+
container: options.container || null,
|
|
12
|
+
dropZone: options.dropZone || null,
|
|
13
|
+
styleManager: options.styleManager || null,
|
|
14
|
+
commandScanner: options.commandScanner || null, // For scanning CSV columns
|
|
15
|
+
dropdownManager: options.dropdownManager || null, // For showing dropdown on warning click
|
|
16
|
+
csvModalManager: options.csvModalManager || null, // For displaying CSV content in modal
|
|
17
|
+
onAttachmentAdd: options.onAttachmentAdd || null,
|
|
18
|
+
onAttachmentRemove: options.onAttachmentRemove || null,
|
|
19
|
+
debug: options.debug || false,
|
|
20
|
+
...options
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
this.attachedFiles = new Map(); // Store attached files
|
|
24
|
+
|
|
25
|
+
if (this.options.debug) {
|
|
26
|
+
console.log('[AttachmentManager] Initialized');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initialize drag & drop handlers
|
|
32
|
+
*/
|
|
33
|
+
init() {
|
|
34
|
+
if (!this.options.container || !this.options.dropZone) {
|
|
35
|
+
console.warn('[AttachmentManager] Missing container or dropZone');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.setupDragAndDrop();
|
|
40
|
+
|
|
41
|
+
if (this.options.debug) {
|
|
42
|
+
console.log('[AttachmentManager] Drag & drop initialized');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Setup drag & drop event handlers
|
|
48
|
+
*/
|
|
49
|
+
setupDragAndDrop() {
|
|
50
|
+
const dropZone = this.options.dropZone;
|
|
51
|
+
|
|
52
|
+
dropZone.addEventListener('dragover', this.handleDragOver);
|
|
53
|
+
dropZone.addEventListener('dragleave', this.handleDragLeave);
|
|
54
|
+
dropZone.addEventListener('drop', this.handleDrop);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle drag over event
|
|
59
|
+
*/
|
|
60
|
+
handleDragOver = (e) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
this.options.dropZone.style.background = '#f0f9ff';
|
|
64
|
+
this.options.dropZone.style.borderColor = '#3b82f6';
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Handle drag leave event
|
|
69
|
+
*/
|
|
70
|
+
handleDragLeave = (e) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
this.options.dropZone.style.background = '';
|
|
74
|
+
this.options.dropZone.style.borderColor = '';
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handle drop event
|
|
79
|
+
*/
|
|
80
|
+
handleDrop = async (e) => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
this.options.dropZone.style.background = '';
|
|
84
|
+
this.options.dropZone.style.borderColor = '';
|
|
85
|
+
|
|
86
|
+
const files = Array.from(e.dataTransfer.files);
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
await this.addAttachment(file);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse CSV and extract metadata
|
|
94
|
+
* @param {string} csvText - CSV file content
|
|
95
|
+
* @returns {Object} - { rows, columns, headers }
|
|
96
|
+
*/
|
|
97
|
+
parseCSVMetadata(csvText) {
|
|
98
|
+
const lines = csvText.trim().split('\n');
|
|
99
|
+
const rows = lines.length;
|
|
100
|
+
|
|
101
|
+
// Parse first row as headers
|
|
102
|
+
const firstLine = lines[0] || '';
|
|
103
|
+
const headers = firstLine.split(',').map(h => h.trim());
|
|
104
|
+
const columns = headers.length;
|
|
105
|
+
|
|
106
|
+
return { rows, columns, headers };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Scan CSV headers for trigger matches
|
|
111
|
+
* @param {Array} headers - CSV column headers
|
|
112
|
+
* @returns {Array} - Array of matches
|
|
113
|
+
*/
|
|
114
|
+
scanCSVHeaders(headers) {
|
|
115
|
+
if (!this.options.commandScanner) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const matches = [];
|
|
120
|
+
|
|
121
|
+
// Get CSV-specific triggers from command map
|
|
122
|
+
const commandMap = this.options.commandScanner.commandMap;
|
|
123
|
+
if (!commandMap || !commandMap['tql-triggers']) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check warning triggers for csv-match-column type
|
|
128
|
+
const warnings = commandMap['tql-triggers'].warning || [];
|
|
129
|
+
const csvTriggers = warnings.filter(t => t.type === 'csv-match-column');
|
|
130
|
+
|
|
131
|
+
// Check each header against CSV triggers
|
|
132
|
+
headers.forEach((header, index) => {
|
|
133
|
+
csvTriggers.forEach(trigger => {
|
|
134
|
+
if (trigger.match && trigger.match.includes(header)) {
|
|
135
|
+
matches.push({
|
|
136
|
+
header,
|
|
137
|
+
columnIndex: index,
|
|
138
|
+
trigger,
|
|
139
|
+
intent: {
|
|
140
|
+
category: trigger.category,
|
|
141
|
+
handler: trigger.handler
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return matches;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format file size in KB
|
|
153
|
+
* @param {number} bytes - File size in bytes
|
|
154
|
+
* @returns {string} - Formatted size
|
|
155
|
+
*/
|
|
156
|
+
formatFileSize(bytes) {
|
|
157
|
+
return (bytes / 1024).toFixed(1) + ' KB';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create attachment wrapper with icon placeholder and card
|
|
162
|
+
* @param {File} file - File object
|
|
163
|
+
* @param {string} csvText - CSV file content
|
|
164
|
+
* @param {Object} metadata - { rows, columns, headers }
|
|
165
|
+
* @param {Array} matches - CSV header matches
|
|
166
|
+
* @returns {HTMLElement} - Wrapper element
|
|
167
|
+
*/
|
|
168
|
+
createAttachmentCard(file, csvText, metadata, matches = []) {
|
|
169
|
+
// Create wrapper container
|
|
170
|
+
const wrapper = document.createElement('div');
|
|
171
|
+
wrapper.className = 'tq-attachment-wrapper';
|
|
172
|
+
|
|
173
|
+
// Create icon placeholder
|
|
174
|
+
const iconPlaceholder = document.createElement('div');
|
|
175
|
+
iconPlaceholder.className = 'tq-attachment-icon-placeholder';
|
|
176
|
+
|
|
177
|
+
// Add warning/error/info icon if matches found
|
|
178
|
+
let icon = null;
|
|
179
|
+
if (matches.length > 0) {
|
|
180
|
+
const match = matches[0];
|
|
181
|
+
const messageState = match.intent?.handler?.['message-state'] || 'warning';
|
|
182
|
+
|
|
183
|
+
// Map message state to icon filename
|
|
184
|
+
const iconMap = {
|
|
185
|
+
'error': 'trustquery-error.svg',
|
|
186
|
+
'warning': 'trustquery-warning.svg',
|
|
187
|
+
'info': 'trustquery-info.svg'
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
icon = document.createElement('img');
|
|
191
|
+
icon.className = 'tq-attachment-icon';
|
|
192
|
+
icon.src = `./assets/${iconMap[messageState] || iconMap.warning}`;
|
|
193
|
+
icon.title = `CSV column ${messageState} - click to review`;
|
|
194
|
+
|
|
195
|
+
// Handle icon click - show dropdown (using mousedown to prevent double-firing)
|
|
196
|
+
icon.addEventListener('mousedown', (e) => {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
this.handleWarningClick(icon, matches, wrapper, file.name);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
iconPlaceholder.appendChild(icon);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Create card
|
|
206
|
+
const card = document.createElement('div');
|
|
207
|
+
card.className = 'tq-attachment-card';
|
|
208
|
+
|
|
209
|
+
// Remove button
|
|
210
|
+
const removeBtn = document.createElement('button');
|
|
211
|
+
removeBtn.className = 'tq-attachment-remove';
|
|
212
|
+
removeBtn.innerHTML = '×';
|
|
213
|
+
removeBtn.onclick = () => this.removeAttachment(file.name, wrapper);
|
|
214
|
+
|
|
215
|
+
// File name header
|
|
216
|
+
const fileNameHeader = document.createElement('div');
|
|
217
|
+
fileNameHeader.className = 'tq-attachment-header';
|
|
218
|
+
fileNameHeader.textContent = file.name;
|
|
219
|
+
|
|
220
|
+
// Metadata row
|
|
221
|
+
const metaRow = document.createElement('div');
|
|
222
|
+
metaRow.className = 'tq-attachment-meta';
|
|
223
|
+
|
|
224
|
+
const sizeSpan = document.createElement('span');
|
|
225
|
+
sizeSpan.textContent = this.formatFileSize(file.size);
|
|
226
|
+
|
|
227
|
+
const rowsSpan = document.createElement('span');
|
|
228
|
+
rowsSpan.textContent = `${metadata.rows} rows`;
|
|
229
|
+
|
|
230
|
+
const colsSpan = document.createElement('span');
|
|
231
|
+
colsSpan.textContent = `${metadata.columns} cols`;
|
|
232
|
+
|
|
233
|
+
metaRow.appendChild(sizeSpan);
|
|
234
|
+
metaRow.appendChild(rowsSpan);
|
|
235
|
+
metaRow.appendChild(colsSpan);
|
|
236
|
+
|
|
237
|
+
card.appendChild(removeBtn);
|
|
238
|
+
card.appendChild(fileNameHeader);
|
|
239
|
+
card.appendChild(metaRow);
|
|
240
|
+
|
|
241
|
+
// Apply styles via StyleManager
|
|
242
|
+
if (this.options.styleManager) {
|
|
243
|
+
this.options.styleManager.applyWrapperStyles(wrapper);
|
|
244
|
+
this.options.styleManager.applyIconPlaceholderStyles(iconPlaceholder, matches.length > 0);
|
|
245
|
+
this.options.styleManager.applyIconStyles(icon);
|
|
246
|
+
this.options.styleManager.applyCardStyles(card);
|
|
247
|
+
this.options.styleManager.applyRemoveButtonStyles(removeBtn);
|
|
248
|
+
this.options.styleManager.applyHeaderStyles(fileNameHeader);
|
|
249
|
+
this.options.styleManager.applyMetaStyles(metaRow);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Add click handler to card to open CSV modal
|
|
253
|
+
card.style.cursor = 'pointer';
|
|
254
|
+
card.addEventListener('click', (e) => {
|
|
255
|
+
// Don't open modal if clicking remove button
|
|
256
|
+
if (e.target === removeBtn || removeBtn.contains(e.target)) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (this.options.csvModalManager) {
|
|
261
|
+
// Get the latest CSV text from attachedFiles (in case it was updated)
|
|
262
|
+
const attachment = this.attachedFiles.get(file.name);
|
|
263
|
+
const currentText = attachment ? attachment.text : csvText;
|
|
264
|
+
const currentMetadata = attachment ? attachment.metadata : metadata;
|
|
265
|
+
|
|
266
|
+
this.options.csvModalManager.show({
|
|
267
|
+
file,
|
|
268
|
+
text: currentText,
|
|
269
|
+
metadata: currentMetadata
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Assemble structure
|
|
275
|
+
wrapper.appendChild(iconPlaceholder);
|
|
276
|
+
wrapper.appendChild(card);
|
|
277
|
+
|
|
278
|
+
return wrapper;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Update CSV column header in stored text
|
|
283
|
+
* @param {string} fileName - File name
|
|
284
|
+
* @param {number} colIndex - Column index
|
|
285
|
+
* @param {string} appendText - Text to append (e.g., "/PST")
|
|
286
|
+
*/
|
|
287
|
+
updateCSVColumnHeader(fileName, colIndex, appendText) {
|
|
288
|
+
const attachment = this.attachedFiles.get(fileName);
|
|
289
|
+
if (!attachment) {
|
|
290
|
+
console.warn('[AttachmentManager] File not found:', fileName);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Parse CSV and update header
|
|
295
|
+
const lines = attachment.text.split('\n');
|
|
296
|
+
if (lines.length === 0) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const headers = lines[0].split(',').map(h => h.trim());
|
|
301
|
+
if (colIndex >= headers.length) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Remove any existing suffix (text after /)
|
|
306
|
+
const baseHeader = headers[colIndex].split('/')[0];
|
|
307
|
+
headers[colIndex] = baseHeader + appendText;
|
|
308
|
+
|
|
309
|
+
// Update first line
|
|
310
|
+
lines[0] = headers.join(', ');
|
|
311
|
+
|
|
312
|
+
// Update stored text
|
|
313
|
+
attachment.text = lines.join('\n');
|
|
314
|
+
|
|
315
|
+
// Update metadata
|
|
316
|
+
attachment.metadata.headers = headers;
|
|
317
|
+
|
|
318
|
+
// Re-scan headers for matches
|
|
319
|
+
const newMatches = this.scanCSVHeaders(headers);
|
|
320
|
+
attachment.matches = newMatches;
|
|
321
|
+
|
|
322
|
+
// Update icon visibility based on matches
|
|
323
|
+
if (attachment.wrapper) {
|
|
324
|
+
const iconPlaceholder = attachment.wrapper.querySelector('.tq-attachment-icon-placeholder');
|
|
325
|
+
const existingIcon = iconPlaceholder?.querySelector('.tq-attachment-icon');
|
|
326
|
+
|
|
327
|
+
if (newMatches.length === 0 && existingIcon) {
|
|
328
|
+
// No more matches - remove the icon
|
|
329
|
+
existingIcon.remove();
|
|
330
|
+
if (this.options.debug) {
|
|
331
|
+
console.log('[AttachmentManager] Removed warning icon - all warnings resolved');
|
|
332
|
+
}
|
|
333
|
+
} else if (newMatches.length > 0 && !existingIcon) {
|
|
334
|
+
// New matches appeared - add icon (shouldn't happen in normal flow, but handle it)
|
|
335
|
+
const match = newMatches[0];
|
|
336
|
+
const messageState = match.intent?.handler?.['message-state'] || 'warning';
|
|
337
|
+
const iconMap = {
|
|
338
|
+
'error': 'trustquery-error.svg',
|
|
339
|
+
'warning': 'trustquery-warning.svg',
|
|
340
|
+
'info': 'trustquery-info.svg'
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const icon = document.createElement('img');
|
|
344
|
+
icon.className = 'tq-attachment-icon';
|
|
345
|
+
icon.src = `./assets/${iconMap[messageState] || iconMap.warning}`;
|
|
346
|
+
icon.title = `CSV column ${messageState} - click to review`;
|
|
347
|
+
|
|
348
|
+
// Handle icon click
|
|
349
|
+
icon.addEventListener('mousedown', (e) => {
|
|
350
|
+
e.preventDefault();
|
|
351
|
+
e.stopPropagation();
|
|
352
|
+
this.handleWarningClick(icon, newMatches, attachment.wrapper, fileName);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Apply styles if styleManager exists
|
|
356
|
+
if (this.options.styleManager) {
|
|
357
|
+
this.options.styleManager.applyIconStyles(icon);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
iconPlaceholder?.appendChild(icon);
|
|
361
|
+
|
|
362
|
+
if (this.options.debug) {
|
|
363
|
+
console.log('[AttachmentManager] Added warning icon - new warnings detected');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (this.options.debug) {
|
|
369
|
+
console.log('[AttachmentManager] Updated CSV column', colIndex, 'to', headers[colIndex]);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Handle warning icon click - show dropdown
|
|
375
|
+
* @param {HTMLElement} iconEl - Warning icon element
|
|
376
|
+
* @param {Array} matches - CSV header matches
|
|
377
|
+
* @param {HTMLElement} wrapper - Wrapper element
|
|
378
|
+
* @param {string} fileName - File name for tracking
|
|
379
|
+
*/
|
|
380
|
+
handleWarningClick(iconEl, matches, wrapper, fileName) {
|
|
381
|
+
if (!this.options.dropdownManager || matches.length === 0) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Use the first match (could be enhanced to handle multiple)
|
|
386
|
+
const match = matches[0];
|
|
387
|
+
|
|
388
|
+
// Create match data similar to text matches
|
|
389
|
+
const matchData = {
|
|
390
|
+
text: match.header,
|
|
391
|
+
command: {
|
|
392
|
+
id: `csv-column-${match.columnIndex}`,
|
|
393
|
+
match: match.header,
|
|
394
|
+
matchType: 'csv-column',
|
|
395
|
+
messageState: match.intent.handler['message-state'] || 'warning',
|
|
396
|
+
category: match.intent.category,
|
|
397
|
+
intent: match.intent,
|
|
398
|
+
handler: match.intent.handler
|
|
399
|
+
},
|
|
400
|
+
intent: match.intent,
|
|
401
|
+
// Add context for attachment icon click
|
|
402
|
+
isAttachmentIcon: true,
|
|
403
|
+
fileName: fileName,
|
|
404
|
+
csvColumnIndex: match.columnIndex
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// Show dropdown using DropdownManager
|
|
408
|
+
this.options.dropdownManager.showDropdown(iconEl, matchData);
|
|
409
|
+
|
|
410
|
+
if (this.options.debug) {
|
|
411
|
+
console.log('[AttachmentManager] Showing dropdown for CSV column:', match.header);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Add attachment to container
|
|
417
|
+
* @param {File} file - File to attach
|
|
418
|
+
*/
|
|
419
|
+
async addAttachment(file) {
|
|
420
|
+
if (!file.name.endsWith('.csv')) {
|
|
421
|
+
alert('Only CSV files are supported');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Check if already attached
|
|
426
|
+
if (this.attachedFiles.has(file.name)) {
|
|
427
|
+
console.warn('[AttachmentManager] File already attached:', file.name);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Read file to get metadata
|
|
432
|
+
const text = await file.text();
|
|
433
|
+
const metadata = this.parseCSVMetadata(text);
|
|
434
|
+
|
|
435
|
+
// Scan CSV headers for trigger matches
|
|
436
|
+
const matches = this.scanCSVHeaders(metadata.headers || []);
|
|
437
|
+
|
|
438
|
+
// Create wrapper with card and icon
|
|
439
|
+
const wrapper = this.createAttachmentCard(file, text, metadata, matches);
|
|
440
|
+
|
|
441
|
+
// Store file reference with matches and text
|
|
442
|
+
this.attachedFiles.set(file.name, { file, text, wrapper, metadata, matches });
|
|
443
|
+
|
|
444
|
+
// Add to container
|
|
445
|
+
this.options.container.appendChild(wrapper);
|
|
446
|
+
this.options.container.classList.add('has-attachments');
|
|
447
|
+
|
|
448
|
+
// Trigger callback
|
|
449
|
+
if (this.options.onAttachmentAdd) {
|
|
450
|
+
this.options.onAttachmentAdd({ file, metadata, matches });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (this.options.debug) {
|
|
454
|
+
console.log('[AttachmentManager] Added:', file.name, metadata, 'matches:', matches);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Remove attachment
|
|
460
|
+
* @param {string} fileName - File name to remove
|
|
461
|
+
* @param {HTMLElement} wrapper - Wrapper element
|
|
462
|
+
*/
|
|
463
|
+
removeAttachment(fileName, wrapper) {
|
|
464
|
+
wrapper.remove();
|
|
465
|
+
const attachment = this.attachedFiles.get(fileName);
|
|
466
|
+
this.attachedFiles.delete(fileName);
|
|
467
|
+
|
|
468
|
+
// Remove has-attachments class if no attachments left
|
|
469
|
+
if (this.attachedFiles.size === 0) {
|
|
470
|
+
this.options.container.classList.remove('has-attachments');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Trigger callback
|
|
474
|
+
if (this.options.onAttachmentRemove && attachment) {
|
|
475
|
+
this.options.onAttachmentRemove({ file: attachment.file, metadata: attachment.metadata });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (this.options.debug) {
|
|
479
|
+
console.log('[AttachmentManager] Removed:', fileName);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Get all attached files
|
|
485
|
+
* @returns {Array} - Array of { file, metadata }
|
|
486
|
+
*/
|
|
487
|
+
getAttachments() {
|
|
488
|
+
return Array.from(this.attachedFiles.values());
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Clear all attachments
|
|
493
|
+
*/
|
|
494
|
+
clearAll() {
|
|
495
|
+
this.attachedFiles.forEach((attachment, fileName) => {
|
|
496
|
+
this.removeAttachment(fileName, attachment.wrapper);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Cleanup
|
|
502
|
+
*/
|
|
503
|
+
destroy() {
|
|
504
|
+
if (this.options.dropZone) {
|
|
505
|
+
this.options.dropZone.removeEventListener('dragover', this.handleDragOver);
|
|
506
|
+
this.options.dropZone.removeEventListener('dragleave', this.handleDragLeave);
|
|
507
|
+
this.options.dropZone.removeEventListener('drop', this.handleDrop);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
this.clearAll();
|
|
511
|
+
|
|
512
|
+
if (this.options.debug) {
|
|
513
|
+
console.log('[AttachmentManager] Destroyed');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// AttachmentStyleManager - Handles all styling for attachment cards
|
|
2
|
+
// Single responsibility: apply consistent inline styles to attachment UI elements
|
|
3
|
+
|
|
4
|
+
export default class AttachmentStyleManager {
|
|
5
|
+
/**
|
|
6
|
+
* Create attachment style manager
|
|
7
|
+
* @param {Object} options - Style options
|
|
8
|
+
*/
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.options = {
|
|
11
|
+
// Card colors
|
|
12
|
+
cardBackground: options.cardBackground || '#f0f9ff', // Light blue background
|
|
13
|
+
cardBorder: options.cardBorder || '#e2e8f0',
|
|
14
|
+
|
|
15
|
+
// Text colors
|
|
16
|
+
headerColor: options.headerColor || '#1e293b',
|
|
17
|
+
metaColor: options.metaColor || '#64748b',
|
|
18
|
+
|
|
19
|
+
// Remove button colors
|
|
20
|
+
removeBackground: options.removeBackground || '#ef4444',
|
|
21
|
+
removeBackgroundHover: options.removeBackgroundHover || '#dc2626',
|
|
22
|
+
|
|
23
|
+
...options
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Apply wrapper styles (container for icon + card)
|
|
29
|
+
* @param {HTMLElement} wrapper - Wrapper element
|
|
30
|
+
*/
|
|
31
|
+
applyWrapperStyles(wrapper) {
|
|
32
|
+
Object.assign(wrapper.style, {
|
|
33
|
+
display: 'flex',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
gap: '8px',
|
|
36
|
+
marginBottom: '6px'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply icon placeholder styles
|
|
42
|
+
* @param {HTMLElement} placeholder - Icon placeholder element
|
|
43
|
+
* @param {boolean} hasIcon - Whether icon is present
|
|
44
|
+
*/
|
|
45
|
+
applyIconPlaceholderStyles(placeholder, hasIcon) {
|
|
46
|
+
Object.assign(placeholder.style, {
|
|
47
|
+
width: '24px',
|
|
48
|
+
minWidth: '24px',
|
|
49
|
+
height: '48px',
|
|
50
|
+
display: 'flex',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
flexShrink: '0'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply icon styles
|
|
59
|
+
* @param {HTMLElement} icon - Icon element
|
|
60
|
+
*/
|
|
61
|
+
applyIconStyles(icon) {
|
|
62
|
+
if (!icon) return;
|
|
63
|
+
|
|
64
|
+
Object.assign(icon.style, {
|
|
65
|
+
width: '20px',
|
|
66
|
+
height: '20px',
|
|
67
|
+
cursor: 'pointer',
|
|
68
|
+
transition: 'transform 0.2s'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Hover effect - slight scale
|
|
72
|
+
icon.addEventListener('mouseenter', () => {
|
|
73
|
+
icon.style.transform = 'scale(1.1)';
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
icon.addEventListener('mouseleave', () => {
|
|
77
|
+
icon.style.transform = 'scale(1)';
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Apply card styles (reduced padding to prevent cutoff)
|
|
83
|
+
* @param {HTMLElement} card - Card element
|
|
84
|
+
*/
|
|
85
|
+
applyCardStyles(card) {
|
|
86
|
+
Object.assign(card.style, {
|
|
87
|
+
position: 'relative',
|
|
88
|
+
background: this.options.cardBackground,
|
|
89
|
+
border: `1px solid ${this.options.cardBorder}`,
|
|
90
|
+
borderRadius: '6px',
|
|
91
|
+
padding: '6px 10px',
|
|
92
|
+
display: 'flex',
|
|
93
|
+
flexDirection: 'column',
|
|
94
|
+
gap: '2px',
|
|
95
|
+
boxSizing: 'border-box',
|
|
96
|
+
maxHeight: '48px',
|
|
97
|
+
overflow: 'hidden',
|
|
98
|
+
flex: '1' // Take remaining space in wrapper
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Apply remove button styles (dark icon, no background)
|
|
104
|
+
* @param {HTMLElement} button - Remove button element
|
|
105
|
+
*/
|
|
106
|
+
applyRemoveButtonStyles(button) {
|
|
107
|
+
Object.assign(button.style, {
|
|
108
|
+
position: 'absolute',
|
|
109
|
+
top: '4px', // Adjusted for reduced padding
|
|
110
|
+
right: '6px',
|
|
111
|
+
background: 'transparent',
|
|
112
|
+
color: '#64748b',
|
|
113
|
+
border: 'none',
|
|
114
|
+
borderRadius: '3px',
|
|
115
|
+
width: '18px',
|
|
116
|
+
height: '18px',
|
|
117
|
+
fontSize: '18px',
|
|
118
|
+
lineHeight: '1',
|
|
119
|
+
cursor: 'pointer',
|
|
120
|
+
display: 'flex',
|
|
121
|
+
alignItems: 'center',
|
|
122
|
+
justifyContent: 'center',
|
|
123
|
+
transition: 'color 0.2s',
|
|
124
|
+
padding: '0',
|
|
125
|
+
fontWeight: '400'
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Hover effect - darker color
|
|
129
|
+
button.addEventListener('mouseenter', () => {
|
|
130
|
+
button.style.color = '#1e293b'; // Darker on hover
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
button.addEventListener('mouseleave', () => {
|
|
134
|
+
button.style.color = '#64748b'; // Back to gray
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Apply header (file name) styles - smaller size
|
|
140
|
+
* @param {HTMLElement} header - Header element
|
|
141
|
+
*/
|
|
142
|
+
applyHeaderStyles(header) {
|
|
143
|
+
Object.assign(header.style, {
|
|
144
|
+
fontWeight: '600',
|
|
145
|
+
fontSize: '11px', // Further reduced from 12px to 11px
|
|
146
|
+
color: this.options.headerColor,
|
|
147
|
+
paddingRight: '22px',
|
|
148
|
+
wordBreak: 'break-word',
|
|
149
|
+
lineHeight: '1.2' // Tighter line height
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Apply metadata row styles
|
|
155
|
+
* @param {HTMLElement} metaRow - Metadata row element
|
|
156
|
+
*/
|
|
157
|
+
applyMetaStyles(metaRow) {
|
|
158
|
+
Object.assign(metaRow.style, {
|
|
159
|
+
display: 'flex',
|
|
160
|
+
gap: '10px', // Slightly reduced from 12px
|
|
161
|
+
fontSize: '10px', // Reduced from 11px
|
|
162
|
+
color: this.options.metaColor,
|
|
163
|
+
lineHeight: '1.2'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Update theme colors dynamically
|
|
169
|
+
* @param {Object} newColors - New color options
|
|
170
|
+
*/
|
|
171
|
+
updateTheme(newColors) {
|
|
172
|
+
Object.assign(this.options, newColors);
|
|
173
|
+
}
|
|
174
|
+
}
|