@trustquery/browser 0.3.0 → 0.3.1
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 +1321 -1
- package/dist/trustquery.js.map +1 -1
- package/package.json +1 -1
- package/src/AttachmentManager.js +466 -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/dist/trustquery.js
CHANGED
|
@@ -3249,6 +3249,1326 @@ class MobileKeyboardHandler {
|
|
|
3249
3249
|
}
|
|
3250
3250
|
}
|
|
3251
3251
|
|
|
3252
|
+
// AttachmentManager - Handles CSV file attachments with drag & drop
|
|
3253
|
+
// Single responsibility: manage attachment state and file operations
|
|
3254
|
+
|
|
3255
|
+
class AttachmentManager {
|
|
3256
|
+
/**
|
|
3257
|
+
* Create attachment manager
|
|
3258
|
+
* @param {Object} options - Configuration options
|
|
3259
|
+
*/
|
|
3260
|
+
constructor(options = {}) {
|
|
3261
|
+
this.options = {
|
|
3262
|
+
container: options.container || null,
|
|
3263
|
+
dropZone: options.dropZone || null,
|
|
3264
|
+
styleManager: options.styleManager || null,
|
|
3265
|
+
commandScanner: options.commandScanner || null, // For scanning CSV columns
|
|
3266
|
+
dropdownManager: options.dropdownManager || null, // For showing dropdown on warning click
|
|
3267
|
+
csvModalManager: options.csvModalManager || null, // For displaying CSV content in modal
|
|
3268
|
+
onAttachmentAdd: options.onAttachmentAdd || null,
|
|
3269
|
+
onAttachmentRemove: options.onAttachmentRemove || null,
|
|
3270
|
+
debug: options.debug || false,
|
|
3271
|
+
...options
|
|
3272
|
+
};
|
|
3273
|
+
|
|
3274
|
+
this.attachedFiles = new Map(); // Store attached files
|
|
3275
|
+
|
|
3276
|
+
if (this.options.debug) {
|
|
3277
|
+
console.log('[AttachmentManager] Initialized');
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
/**
|
|
3282
|
+
* Initialize drag & drop handlers
|
|
3283
|
+
*/
|
|
3284
|
+
init() {
|
|
3285
|
+
if (!this.options.container || !this.options.dropZone) {
|
|
3286
|
+
console.warn('[AttachmentManager] Missing container or dropZone');
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
this.setupDragAndDrop();
|
|
3291
|
+
|
|
3292
|
+
if (this.options.debug) {
|
|
3293
|
+
console.log('[AttachmentManager] Drag & drop initialized');
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
/**
|
|
3298
|
+
* Setup drag & drop event handlers
|
|
3299
|
+
*/
|
|
3300
|
+
setupDragAndDrop() {
|
|
3301
|
+
const dropZone = this.options.dropZone;
|
|
3302
|
+
|
|
3303
|
+
dropZone.addEventListener('dragover', this.handleDragOver);
|
|
3304
|
+
dropZone.addEventListener('dragleave', this.handleDragLeave);
|
|
3305
|
+
dropZone.addEventListener('drop', this.handleDrop);
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
/**
|
|
3309
|
+
* Handle drag over event
|
|
3310
|
+
*/
|
|
3311
|
+
handleDragOver = (e) => {
|
|
3312
|
+
e.preventDefault();
|
|
3313
|
+
e.stopPropagation();
|
|
3314
|
+
this.options.dropZone.style.background = '#f0f9ff';
|
|
3315
|
+
this.options.dropZone.style.borderColor = '#3b82f6';
|
|
3316
|
+
};
|
|
3317
|
+
|
|
3318
|
+
/**
|
|
3319
|
+
* Handle drag leave event
|
|
3320
|
+
*/
|
|
3321
|
+
handleDragLeave = (e) => {
|
|
3322
|
+
e.preventDefault();
|
|
3323
|
+
e.stopPropagation();
|
|
3324
|
+
this.options.dropZone.style.background = '';
|
|
3325
|
+
this.options.dropZone.style.borderColor = '';
|
|
3326
|
+
};
|
|
3327
|
+
|
|
3328
|
+
/**
|
|
3329
|
+
* Handle drop event
|
|
3330
|
+
*/
|
|
3331
|
+
handleDrop = async (e) => {
|
|
3332
|
+
e.preventDefault();
|
|
3333
|
+
e.stopPropagation();
|
|
3334
|
+
this.options.dropZone.style.background = '';
|
|
3335
|
+
this.options.dropZone.style.borderColor = '';
|
|
3336
|
+
|
|
3337
|
+
const files = Array.from(e.dataTransfer.files);
|
|
3338
|
+
for (const file of files) {
|
|
3339
|
+
await this.addAttachment(file);
|
|
3340
|
+
}
|
|
3341
|
+
};
|
|
3342
|
+
|
|
3343
|
+
/**
|
|
3344
|
+
* Parse CSV and extract metadata
|
|
3345
|
+
* @param {string} csvText - CSV file content
|
|
3346
|
+
* @returns {Object} - { rows, columns, headers }
|
|
3347
|
+
*/
|
|
3348
|
+
parseCSVMetadata(csvText) {
|
|
3349
|
+
const lines = csvText.trim().split('\n');
|
|
3350
|
+
const rows = lines.length;
|
|
3351
|
+
|
|
3352
|
+
// Parse first row as headers
|
|
3353
|
+
const firstLine = lines[0] || '';
|
|
3354
|
+
const headers = firstLine.split(',').map(h => h.trim());
|
|
3355
|
+
const columns = headers.length;
|
|
3356
|
+
|
|
3357
|
+
return { rows, columns, headers };
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
/**
|
|
3361
|
+
* Scan CSV headers for trigger matches
|
|
3362
|
+
* @param {Array} headers - CSV column headers
|
|
3363
|
+
* @returns {Array} - Array of matches
|
|
3364
|
+
*/
|
|
3365
|
+
scanCSVHeaders(headers) {
|
|
3366
|
+
if (!this.options.commandScanner) {
|
|
3367
|
+
return [];
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
const matches = [];
|
|
3371
|
+
|
|
3372
|
+
// Get CSV-specific triggers from command map
|
|
3373
|
+
const commandMap = this.options.commandScanner.commandMap;
|
|
3374
|
+
if (!commandMap || !commandMap['tql-triggers']) {
|
|
3375
|
+
return [];
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
// Check warning triggers for csv-match-column type
|
|
3379
|
+
const warnings = commandMap['tql-triggers'].warning || [];
|
|
3380
|
+
const csvTriggers = warnings.filter(t => t.type === 'csv-match-column');
|
|
3381
|
+
|
|
3382
|
+
// Check each header against CSV triggers
|
|
3383
|
+
headers.forEach((header, index) => {
|
|
3384
|
+
csvTriggers.forEach(trigger => {
|
|
3385
|
+
if (trigger.match && trigger.match.includes(header)) {
|
|
3386
|
+
matches.push({
|
|
3387
|
+
header,
|
|
3388
|
+
columnIndex: index,
|
|
3389
|
+
trigger,
|
|
3390
|
+
intent: {
|
|
3391
|
+
category: trigger.category,
|
|
3392
|
+
handler: trigger.handler
|
|
3393
|
+
}
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
});
|
|
3397
|
+
});
|
|
3398
|
+
|
|
3399
|
+
return matches;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
/**
|
|
3403
|
+
* Format file size in KB
|
|
3404
|
+
* @param {number} bytes - File size in bytes
|
|
3405
|
+
* @returns {string} - Formatted size
|
|
3406
|
+
*/
|
|
3407
|
+
formatFileSize(bytes) {
|
|
3408
|
+
return (bytes / 1024).toFixed(1) + ' KB';
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
/**
|
|
3412
|
+
* Create attachment wrapper with icon placeholder and card
|
|
3413
|
+
* @param {File} file - File object
|
|
3414
|
+
* @param {string} csvText - CSV file content
|
|
3415
|
+
* @param {Object} metadata - { rows, columns, headers }
|
|
3416
|
+
* @param {Array} matches - CSV header matches
|
|
3417
|
+
* @returns {HTMLElement} - Wrapper element
|
|
3418
|
+
*/
|
|
3419
|
+
createAttachmentCard(file, csvText, metadata, matches = []) {
|
|
3420
|
+
// Create wrapper container
|
|
3421
|
+
const wrapper = document.createElement('div');
|
|
3422
|
+
wrapper.className = 'tq-attachment-wrapper';
|
|
3423
|
+
|
|
3424
|
+
// Create icon placeholder
|
|
3425
|
+
const iconPlaceholder = document.createElement('div');
|
|
3426
|
+
iconPlaceholder.className = 'tq-attachment-icon-placeholder';
|
|
3427
|
+
|
|
3428
|
+
// Add warning/error/info icon if matches found
|
|
3429
|
+
let icon = null;
|
|
3430
|
+
if (matches.length > 0) {
|
|
3431
|
+
const match = matches[0];
|
|
3432
|
+
const messageState = match.intent?.handler?.['message-state'] || 'warning';
|
|
3433
|
+
|
|
3434
|
+
// Map message state to icon filename
|
|
3435
|
+
const iconMap = {
|
|
3436
|
+
'error': 'trustquery-error.svg',
|
|
3437
|
+
'warning': 'trustquery-warning.svg',
|
|
3438
|
+
'info': 'trustquery-info.svg'
|
|
3439
|
+
};
|
|
3440
|
+
|
|
3441
|
+
icon = document.createElement('img');
|
|
3442
|
+
icon.className = 'tq-attachment-icon';
|
|
3443
|
+
icon.src = `./assets/${iconMap[messageState] || iconMap.warning}`;
|
|
3444
|
+
icon.title = `CSV column ${messageState} - click to review`;
|
|
3445
|
+
|
|
3446
|
+
// Handle icon click - show dropdown (using mousedown to prevent double-firing)
|
|
3447
|
+
icon.addEventListener('mousedown', (e) => {
|
|
3448
|
+
e.preventDefault();
|
|
3449
|
+
e.stopPropagation();
|
|
3450
|
+
this.handleWarningClick(icon, matches, wrapper, file.name);
|
|
3451
|
+
});
|
|
3452
|
+
|
|
3453
|
+
iconPlaceholder.appendChild(icon);
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// Create card
|
|
3457
|
+
const card = document.createElement('div');
|
|
3458
|
+
card.className = 'tq-attachment-card';
|
|
3459
|
+
|
|
3460
|
+
// Remove button
|
|
3461
|
+
const removeBtn = document.createElement('button');
|
|
3462
|
+
removeBtn.className = 'tq-attachment-remove';
|
|
3463
|
+
removeBtn.innerHTML = '×';
|
|
3464
|
+
removeBtn.onclick = () => this.removeAttachment(file.name, wrapper);
|
|
3465
|
+
|
|
3466
|
+
// File name header
|
|
3467
|
+
const fileNameHeader = document.createElement('div');
|
|
3468
|
+
fileNameHeader.className = 'tq-attachment-header';
|
|
3469
|
+
fileNameHeader.textContent = file.name;
|
|
3470
|
+
|
|
3471
|
+
// Metadata row
|
|
3472
|
+
const metaRow = document.createElement('div');
|
|
3473
|
+
metaRow.className = 'tq-attachment-meta';
|
|
3474
|
+
|
|
3475
|
+
const sizeSpan = document.createElement('span');
|
|
3476
|
+
sizeSpan.textContent = this.formatFileSize(file.size);
|
|
3477
|
+
|
|
3478
|
+
const rowsSpan = document.createElement('span');
|
|
3479
|
+
rowsSpan.textContent = `${metadata.rows} rows`;
|
|
3480
|
+
|
|
3481
|
+
const colsSpan = document.createElement('span');
|
|
3482
|
+
colsSpan.textContent = `${metadata.columns} cols`;
|
|
3483
|
+
|
|
3484
|
+
metaRow.appendChild(sizeSpan);
|
|
3485
|
+
metaRow.appendChild(rowsSpan);
|
|
3486
|
+
metaRow.appendChild(colsSpan);
|
|
3487
|
+
|
|
3488
|
+
card.appendChild(removeBtn);
|
|
3489
|
+
card.appendChild(fileNameHeader);
|
|
3490
|
+
card.appendChild(metaRow);
|
|
3491
|
+
|
|
3492
|
+
// Apply styles via StyleManager
|
|
3493
|
+
if (this.options.styleManager) {
|
|
3494
|
+
this.options.styleManager.applyWrapperStyles(wrapper);
|
|
3495
|
+
this.options.styleManager.applyIconPlaceholderStyles(iconPlaceholder, matches.length > 0);
|
|
3496
|
+
this.options.styleManager.applyIconStyles(icon);
|
|
3497
|
+
this.options.styleManager.applyCardStyles(card);
|
|
3498
|
+
this.options.styleManager.applyRemoveButtonStyles(removeBtn);
|
|
3499
|
+
this.options.styleManager.applyHeaderStyles(fileNameHeader);
|
|
3500
|
+
this.options.styleManager.applyMetaStyles(metaRow);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
// Add click handler to card to open CSV modal
|
|
3504
|
+
card.style.cursor = 'pointer';
|
|
3505
|
+
card.addEventListener('click', (e) => {
|
|
3506
|
+
// Don't open modal if clicking remove button
|
|
3507
|
+
if (e.target === removeBtn || removeBtn.contains(e.target)) {
|
|
3508
|
+
return;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
if (this.options.csvModalManager) {
|
|
3512
|
+
// Get the latest CSV text from attachedFiles (in case it was updated)
|
|
3513
|
+
const attachment = this.attachedFiles.get(file.name);
|
|
3514
|
+
const currentText = attachment ? attachment.text : csvText;
|
|
3515
|
+
const currentMetadata = attachment ? attachment.metadata : metadata;
|
|
3516
|
+
|
|
3517
|
+
this.options.csvModalManager.show({
|
|
3518
|
+
file,
|
|
3519
|
+
text: currentText,
|
|
3520
|
+
metadata: currentMetadata
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
});
|
|
3524
|
+
|
|
3525
|
+
// Assemble structure
|
|
3526
|
+
wrapper.appendChild(iconPlaceholder);
|
|
3527
|
+
wrapper.appendChild(card);
|
|
3528
|
+
|
|
3529
|
+
return wrapper;
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
/**
|
|
3533
|
+
* Update CSV column header in stored text
|
|
3534
|
+
* @param {string} fileName - File name
|
|
3535
|
+
* @param {number} colIndex - Column index
|
|
3536
|
+
* @param {string} appendText - Text to append (e.g., "/PST")
|
|
3537
|
+
*/
|
|
3538
|
+
updateCSVColumnHeader(fileName, colIndex, appendText) {
|
|
3539
|
+
const attachment = this.attachedFiles.get(fileName);
|
|
3540
|
+
if (!attachment) {
|
|
3541
|
+
console.warn('[AttachmentManager] File not found:', fileName);
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
// Parse CSV and update header
|
|
3546
|
+
const lines = attachment.text.split('\n');
|
|
3547
|
+
if (lines.length === 0) {
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
const headers = lines[0].split(',').map(h => h.trim());
|
|
3552
|
+
if (colIndex >= headers.length) {
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// Remove any existing suffix (text after /)
|
|
3557
|
+
const baseHeader = headers[colIndex].split('/')[0];
|
|
3558
|
+
headers[colIndex] = baseHeader + appendText;
|
|
3559
|
+
|
|
3560
|
+
// Update first line
|
|
3561
|
+
lines[0] = headers.join(', ');
|
|
3562
|
+
|
|
3563
|
+
// Update stored text
|
|
3564
|
+
attachment.text = lines.join('\n');
|
|
3565
|
+
|
|
3566
|
+
// Update metadata
|
|
3567
|
+
attachment.metadata.headers = headers;
|
|
3568
|
+
|
|
3569
|
+
if (this.options.debug) {
|
|
3570
|
+
console.log('[AttachmentManager] Updated CSV column', colIndex, 'to', headers[colIndex]);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
/**
|
|
3575
|
+
* Handle warning icon click - show dropdown
|
|
3576
|
+
* @param {HTMLElement} iconEl - Warning icon element
|
|
3577
|
+
* @param {Array} matches - CSV header matches
|
|
3578
|
+
* @param {HTMLElement} wrapper - Wrapper element
|
|
3579
|
+
* @param {string} fileName - File name for tracking
|
|
3580
|
+
*/
|
|
3581
|
+
handleWarningClick(iconEl, matches, wrapper, fileName) {
|
|
3582
|
+
if (!this.options.dropdownManager || matches.length === 0) {
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
// Use the first match (could be enhanced to handle multiple)
|
|
3587
|
+
const match = matches[0];
|
|
3588
|
+
|
|
3589
|
+
// Create match data similar to text matches
|
|
3590
|
+
const matchData = {
|
|
3591
|
+
text: match.header,
|
|
3592
|
+
command: {
|
|
3593
|
+
id: `csv-column-${match.columnIndex}`,
|
|
3594
|
+
match: match.header,
|
|
3595
|
+
matchType: 'csv-column',
|
|
3596
|
+
messageState: match.intent.handler['message-state'] || 'warning',
|
|
3597
|
+
category: match.intent.category,
|
|
3598
|
+
intent: match.intent,
|
|
3599
|
+
handler: match.intent.handler
|
|
3600
|
+
},
|
|
3601
|
+
intent: match.intent,
|
|
3602
|
+
// Add context for attachment icon click
|
|
3603
|
+
isAttachmentIcon: true,
|
|
3604
|
+
fileName: fileName,
|
|
3605
|
+
csvColumnIndex: match.columnIndex
|
|
3606
|
+
};
|
|
3607
|
+
|
|
3608
|
+
// Show dropdown using DropdownManager
|
|
3609
|
+
this.options.dropdownManager.showDropdown(iconEl, matchData);
|
|
3610
|
+
|
|
3611
|
+
if (this.options.debug) {
|
|
3612
|
+
console.log('[AttachmentManager] Showing dropdown for CSV column:', match.header);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
/**
|
|
3617
|
+
* Add attachment to container
|
|
3618
|
+
* @param {File} file - File to attach
|
|
3619
|
+
*/
|
|
3620
|
+
async addAttachment(file) {
|
|
3621
|
+
if (!file.name.endsWith('.csv')) {
|
|
3622
|
+
alert('Only CSV files are supported');
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// Check if already attached
|
|
3627
|
+
if (this.attachedFiles.has(file.name)) {
|
|
3628
|
+
console.warn('[AttachmentManager] File already attached:', file.name);
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
// Read file to get metadata
|
|
3633
|
+
const text = await file.text();
|
|
3634
|
+
const metadata = this.parseCSVMetadata(text);
|
|
3635
|
+
|
|
3636
|
+
// Scan CSV headers for trigger matches
|
|
3637
|
+
const matches = this.scanCSVHeaders(metadata.headers || []);
|
|
3638
|
+
|
|
3639
|
+
// Create wrapper with card and icon
|
|
3640
|
+
const wrapper = this.createAttachmentCard(file, text, metadata, matches);
|
|
3641
|
+
|
|
3642
|
+
// Store file reference with matches and text
|
|
3643
|
+
this.attachedFiles.set(file.name, { file, text, wrapper, metadata, matches });
|
|
3644
|
+
|
|
3645
|
+
// Add to container
|
|
3646
|
+
this.options.container.appendChild(wrapper);
|
|
3647
|
+
this.options.container.classList.add('has-attachments');
|
|
3648
|
+
|
|
3649
|
+
// Trigger callback
|
|
3650
|
+
if (this.options.onAttachmentAdd) {
|
|
3651
|
+
this.options.onAttachmentAdd({ file, metadata, matches });
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
if (this.options.debug) {
|
|
3655
|
+
console.log('[AttachmentManager] Added:', file.name, metadata, 'matches:', matches);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
/**
|
|
3660
|
+
* Remove attachment
|
|
3661
|
+
* @param {string} fileName - File name to remove
|
|
3662
|
+
* @param {HTMLElement} wrapper - Wrapper element
|
|
3663
|
+
*/
|
|
3664
|
+
removeAttachment(fileName, wrapper) {
|
|
3665
|
+
wrapper.remove();
|
|
3666
|
+
const attachment = this.attachedFiles.get(fileName);
|
|
3667
|
+
this.attachedFiles.delete(fileName);
|
|
3668
|
+
|
|
3669
|
+
// Remove has-attachments class if no attachments left
|
|
3670
|
+
if (this.attachedFiles.size === 0) {
|
|
3671
|
+
this.options.container.classList.remove('has-attachments');
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
// Trigger callback
|
|
3675
|
+
if (this.options.onAttachmentRemove && attachment) {
|
|
3676
|
+
this.options.onAttachmentRemove({ file: attachment.file, metadata: attachment.metadata });
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
if (this.options.debug) {
|
|
3680
|
+
console.log('[AttachmentManager] Removed:', fileName);
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
/**
|
|
3685
|
+
* Get all attached files
|
|
3686
|
+
* @returns {Array} - Array of { file, metadata }
|
|
3687
|
+
*/
|
|
3688
|
+
getAttachments() {
|
|
3689
|
+
return Array.from(this.attachedFiles.values());
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
/**
|
|
3693
|
+
* Clear all attachments
|
|
3694
|
+
*/
|
|
3695
|
+
clearAll() {
|
|
3696
|
+
this.attachedFiles.forEach((attachment, fileName) => {
|
|
3697
|
+
this.removeAttachment(fileName, attachment.wrapper);
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
/**
|
|
3702
|
+
* Cleanup
|
|
3703
|
+
*/
|
|
3704
|
+
destroy() {
|
|
3705
|
+
if (this.options.dropZone) {
|
|
3706
|
+
this.options.dropZone.removeEventListener('dragover', this.handleDragOver);
|
|
3707
|
+
this.options.dropZone.removeEventListener('dragleave', this.handleDragLeave);
|
|
3708
|
+
this.options.dropZone.removeEventListener('drop', this.handleDrop);
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
this.clearAll();
|
|
3712
|
+
|
|
3713
|
+
if (this.options.debug) {
|
|
3714
|
+
console.log('[AttachmentManager] Destroyed');
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// AttachmentStyleManager - Handles all styling for attachment cards
|
|
3720
|
+
// Single responsibility: apply consistent inline styles to attachment UI elements
|
|
3721
|
+
|
|
3722
|
+
class AttachmentStyleManager {
|
|
3723
|
+
/**
|
|
3724
|
+
* Create attachment style manager
|
|
3725
|
+
* @param {Object} options - Style options
|
|
3726
|
+
*/
|
|
3727
|
+
constructor(options = {}) {
|
|
3728
|
+
this.options = {
|
|
3729
|
+
// Card colors
|
|
3730
|
+
cardBackground: options.cardBackground || '#f0f9ff', // Light blue background
|
|
3731
|
+
cardBorder: options.cardBorder || '#e2e8f0',
|
|
3732
|
+
|
|
3733
|
+
// Text colors
|
|
3734
|
+
headerColor: options.headerColor || '#1e293b',
|
|
3735
|
+
metaColor: options.metaColor || '#64748b',
|
|
3736
|
+
|
|
3737
|
+
// Remove button colors
|
|
3738
|
+
removeBackground: options.removeBackground || '#ef4444',
|
|
3739
|
+
removeBackgroundHover: options.removeBackgroundHover || '#dc2626',
|
|
3740
|
+
|
|
3741
|
+
...options
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
/**
|
|
3746
|
+
* Apply wrapper styles (container for icon + card)
|
|
3747
|
+
* @param {HTMLElement} wrapper - Wrapper element
|
|
3748
|
+
*/
|
|
3749
|
+
applyWrapperStyles(wrapper) {
|
|
3750
|
+
Object.assign(wrapper.style, {
|
|
3751
|
+
display: 'flex',
|
|
3752
|
+
alignItems: 'center',
|
|
3753
|
+
gap: '8px',
|
|
3754
|
+
marginBottom: '6px'
|
|
3755
|
+
});
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
/**
|
|
3759
|
+
* Apply icon placeholder styles
|
|
3760
|
+
* @param {HTMLElement} placeholder - Icon placeholder element
|
|
3761
|
+
* @param {boolean} hasIcon - Whether icon is present
|
|
3762
|
+
*/
|
|
3763
|
+
applyIconPlaceholderStyles(placeholder, hasIcon) {
|
|
3764
|
+
Object.assign(placeholder.style, {
|
|
3765
|
+
width: '24px',
|
|
3766
|
+
minWidth: '24px',
|
|
3767
|
+
height: '48px',
|
|
3768
|
+
display: 'flex',
|
|
3769
|
+
alignItems: 'center',
|
|
3770
|
+
justifyContent: 'center',
|
|
3771
|
+
flexShrink: '0'
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
/**
|
|
3776
|
+
* Apply icon styles
|
|
3777
|
+
* @param {HTMLElement} icon - Icon element
|
|
3778
|
+
*/
|
|
3779
|
+
applyIconStyles(icon) {
|
|
3780
|
+
if (!icon) return;
|
|
3781
|
+
|
|
3782
|
+
Object.assign(icon.style, {
|
|
3783
|
+
width: '20px',
|
|
3784
|
+
height: '20px',
|
|
3785
|
+
cursor: 'pointer',
|
|
3786
|
+
transition: 'transform 0.2s'
|
|
3787
|
+
});
|
|
3788
|
+
|
|
3789
|
+
// Hover effect - slight scale
|
|
3790
|
+
icon.addEventListener('mouseenter', () => {
|
|
3791
|
+
icon.style.transform = 'scale(1.1)';
|
|
3792
|
+
});
|
|
3793
|
+
|
|
3794
|
+
icon.addEventListener('mouseleave', () => {
|
|
3795
|
+
icon.style.transform = 'scale(1)';
|
|
3796
|
+
});
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
/**
|
|
3800
|
+
* Apply card styles (reduced padding to prevent cutoff)
|
|
3801
|
+
* @param {HTMLElement} card - Card element
|
|
3802
|
+
*/
|
|
3803
|
+
applyCardStyles(card) {
|
|
3804
|
+
Object.assign(card.style, {
|
|
3805
|
+
position: 'relative',
|
|
3806
|
+
background: this.options.cardBackground,
|
|
3807
|
+
border: `1px solid ${this.options.cardBorder}`,
|
|
3808
|
+
borderRadius: '6px',
|
|
3809
|
+
padding: '6px 10px',
|
|
3810
|
+
display: 'flex',
|
|
3811
|
+
flexDirection: 'column',
|
|
3812
|
+
gap: '2px',
|
|
3813
|
+
boxSizing: 'border-box',
|
|
3814
|
+
maxHeight: '48px',
|
|
3815
|
+
overflow: 'hidden',
|
|
3816
|
+
flex: '1' // Take remaining space in wrapper
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
/**
|
|
3821
|
+
* Apply remove button styles (dark icon, no background)
|
|
3822
|
+
* @param {HTMLElement} button - Remove button element
|
|
3823
|
+
*/
|
|
3824
|
+
applyRemoveButtonStyles(button) {
|
|
3825
|
+
Object.assign(button.style, {
|
|
3826
|
+
position: 'absolute',
|
|
3827
|
+
top: '4px', // Adjusted for reduced padding
|
|
3828
|
+
right: '6px',
|
|
3829
|
+
background: 'transparent',
|
|
3830
|
+
color: '#64748b',
|
|
3831
|
+
border: 'none',
|
|
3832
|
+
borderRadius: '3px',
|
|
3833
|
+
width: '18px',
|
|
3834
|
+
height: '18px',
|
|
3835
|
+
fontSize: '18px',
|
|
3836
|
+
lineHeight: '1',
|
|
3837
|
+
cursor: 'pointer',
|
|
3838
|
+
display: 'flex',
|
|
3839
|
+
alignItems: 'center',
|
|
3840
|
+
justifyContent: 'center',
|
|
3841
|
+
transition: 'color 0.2s',
|
|
3842
|
+
padding: '0',
|
|
3843
|
+
fontWeight: '400'
|
|
3844
|
+
});
|
|
3845
|
+
|
|
3846
|
+
// Hover effect - darker color
|
|
3847
|
+
button.addEventListener('mouseenter', () => {
|
|
3848
|
+
button.style.color = '#1e293b'; // Darker on hover
|
|
3849
|
+
});
|
|
3850
|
+
|
|
3851
|
+
button.addEventListener('mouseleave', () => {
|
|
3852
|
+
button.style.color = '#64748b'; // Back to gray
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
/**
|
|
3857
|
+
* Apply header (file name) styles - smaller size
|
|
3858
|
+
* @param {HTMLElement} header - Header element
|
|
3859
|
+
*/
|
|
3860
|
+
applyHeaderStyles(header) {
|
|
3861
|
+
Object.assign(header.style, {
|
|
3862
|
+
fontWeight: '600',
|
|
3863
|
+
fontSize: '11px', // Further reduced from 12px to 11px
|
|
3864
|
+
color: this.options.headerColor,
|
|
3865
|
+
paddingRight: '22px',
|
|
3866
|
+
wordBreak: 'break-word',
|
|
3867
|
+
lineHeight: '1.2' // Tighter line height
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
/**
|
|
3872
|
+
* Apply metadata row styles
|
|
3873
|
+
* @param {HTMLElement} metaRow - Metadata row element
|
|
3874
|
+
*/
|
|
3875
|
+
applyMetaStyles(metaRow) {
|
|
3876
|
+
Object.assign(metaRow.style, {
|
|
3877
|
+
display: 'flex',
|
|
3878
|
+
gap: '10px', // Slightly reduced from 12px
|
|
3879
|
+
fontSize: '10px', // Reduced from 11px
|
|
3880
|
+
color: this.options.metaColor,
|
|
3881
|
+
lineHeight: '1.2'
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
/**
|
|
3886
|
+
* Update theme colors dynamically
|
|
3887
|
+
* @param {Object} newColors - New color options
|
|
3888
|
+
*/
|
|
3889
|
+
updateTheme(newColors) {
|
|
3890
|
+
Object.assign(this.options, newColors);
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
// CSVModalManager - Handles CSV modal display with trigger matching
|
|
3895
|
+
// Single responsibility: manage CSV modal lifecycle and rendering
|
|
3896
|
+
|
|
3897
|
+
class CSVModalManager {
|
|
3898
|
+
/**
|
|
3899
|
+
* Create CSV modal manager
|
|
3900
|
+
* @param {Object} options - Configuration options
|
|
3901
|
+
*/
|
|
3902
|
+
constructor(options = {}) {
|
|
3903
|
+
this.options = {
|
|
3904
|
+
styleManager: options.styleManager || null,
|
|
3905
|
+
commandScanner: options.commandScanner || null,
|
|
3906
|
+
dropdownManager: options.dropdownManager || null,
|
|
3907
|
+
onCellClick: options.onCellClick || null,
|
|
3908
|
+
debug: options.debug || false,
|
|
3909
|
+
...options
|
|
3910
|
+
};
|
|
3911
|
+
|
|
3912
|
+
this.modal = null;
|
|
3913
|
+
this.currentCSVData = null;
|
|
3914
|
+
this.parsedData = null;
|
|
3915
|
+
|
|
3916
|
+
if (this.options.debug) {
|
|
3917
|
+
console.log('[CSVModalManager] Initialized');
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
/**
|
|
3922
|
+
* Parse CSV text into 2D array
|
|
3923
|
+
* @param {string} csvText - Raw CSV text
|
|
3924
|
+
* @returns {Array} - 2D array of cells
|
|
3925
|
+
*/
|
|
3926
|
+
parseCSV(csvText) {
|
|
3927
|
+
const lines = csvText.trim().split('\n');
|
|
3928
|
+
return lines.map(line => {
|
|
3929
|
+
// Simple CSV parsing (handles basic cases)
|
|
3930
|
+
return line.split(',').map(cell => cell.trim());
|
|
3931
|
+
});
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
/**
|
|
3935
|
+
* Check if a cell value matches any triggers
|
|
3936
|
+
* @param {string} cellValue - Cell content
|
|
3937
|
+
* @returns {Object|null} - Match data or null
|
|
3938
|
+
*/
|
|
3939
|
+
checkCellForTriggers(cellValue) {
|
|
3940
|
+
if (!this.options.commandScanner || !cellValue) {
|
|
3941
|
+
return null;
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
const commandMap = this.options.commandScanner.commandMap;
|
|
3945
|
+
if (!commandMap || !commandMap['tql-triggers']) {
|
|
3946
|
+
return null;
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
// Check all trigger states (error, warning, info)
|
|
3950
|
+
const triggers = commandMap['tql-triggers'];
|
|
3951
|
+
const allTriggers = [
|
|
3952
|
+
...(triggers.error || []),
|
|
3953
|
+
...(triggers.warning || []),
|
|
3954
|
+
...(triggers.info || [])
|
|
3955
|
+
];
|
|
3956
|
+
|
|
3957
|
+
// Check each trigger
|
|
3958
|
+
for (const trigger of allTriggers) {
|
|
3959
|
+
// Handle 'match' type triggers
|
|
3960
|
+
if (trigger.type === 'match' && trigger.match) {
|
|
3961
|
+
for (const matchText of trigger.match) {
|
|
3962
|
+
if (cellValue.toLowerCase() === matchText.toLowerCase()) {
|
|
3963
|
+
return {
|
|
3964
|
+
text: cellValue,
|
|
3965
|
+
trigger,
|
|
3966
|
+
matchType: 'exact',
|
|
3967
|
+
messageState: trigger.handler?.['message-state'] || 'info'
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
// Handle 'csv-match-column' type triggers (for CSV headers)
|
|
3974
|
+
if (trigger.type === 'csv-match-column' && trigger.match) {
|
|
3975
|
+
for (const matchText of trigger.match) {
|
|
3976
|
+
if (cellValue.toLowerCase() === matchText.toLowerCase()) {
|
|
3977
|
+
return {
|
|
3978
|
+
text: cellValue,
|
|
3979
|
+
trigger,
|
|
3980
|
+
matchType: 'csv-column',
|
|
3981
|
+
messageState: trigger.handler?.['message-state'] || 'info'
|
|
3982
|
+
};
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
// Handle 'regex' type triggers
|
|
3988
|
+
if (trigger.type === 'regex' && trigger.regex) {
|
|
3989
|
+
for (const pattern of trigger.regex) {
|
|
3990
|
+
const regex = new RegExp(pattern);
|
|
3991
|
+
if (regex.test(cellValue)) {
|
|
3992
|
+
return {
|
|
3993
|
+
text: cellValue,
|
|
3994
|
+
trigger,
|
|
3995
|
+
matchType: 'regex',
|
|
3996
|
+
messageState: trigger.handler?.['message-state'] || 'info'
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
/**
|
|
4007
|
+
* Create table cell with optional trigger highlighting
|
|
4008
|
+
* @param {string} cellValue - Cell content
|
|
4009
|
+
* @param {boolean} isHeader - Whether this is a header cell
|
|
4010
|
+
* @param {number} rowIndex - Row index
|
|
4011
|
+
* @param {number} colIndex - Column index
|
|
4012
|
+
* @returns {HTMLElement} - Table cell element
|
|
4013
|
+
*/
|
|
4014
|
+
createCell(cellValue, isHeader, rowIndex, colIndex) {
|
|
4015
|
+
const cell = document.createElement(isHeader ? 'th' : 'td');
|
|
4016
|
+
|
|
4017
|
+
// Check for trigger matches
|
|
4018
|
+
const match = this.checkCellForTriggers(cellValue);
|
|
4019
|
+
|
|
4020
|
+
if (match) {
|
|
4021
|
+
// Create highlighted span
|
|
4022
|
+
const span = document.createElement('span');
|
|
4023
|
+
span.className = 'tq-csv-match';
|
|
4024
|
+
span.textContent = cellValue;
|
|
4025
|
+
span.setAttribute('data-message-state', match.messageState);
|
|
4026
|
+
span.setAttribute('data-row', rowIndex);
|
|
4027
|
+
span.setAttribute('data-col', colIndex);
|
|
4028
|
+
|
|
4029
|
+
// Apply highlighting styles
|
|
4030
|
+
if (this.options.styleManager) {
|
|
4031
|
+
this.options.styleManager.applyCellMatchStyles(span, match.messageState);
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
// Add click handler to show dropdown
|
|
4035
|
+
span.addEventListener('click', (e) => {
|
|
4036
|
+
e.stopPropagation();
|
|
4037
|
+
this.handleCellMatchClick(span, match, rowIndex, colIndex);
|
|
4038
|
+
});
|
|
4039
|
+
|
|
4040
|
+
cell.appendChild(span);
|
|
4041
|
+
} else {
|
|
4042
|
+
cell.textContent = cellValue;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
// Apply cell styles
|
|
4046
|
+
if (this.options.styleManager) {
|
|
4047
|
+
if (isHeader) {
|
|
4048
|
+
this.options.styleManager.applyHeaderCellStyles(cell);
|
|
4049
|
+
} else {
|
|
4050
|
+
this.options.styleManager.applyDataCellStyles(cell);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
return cell;
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
/**
|
|
4058
|
+
* Handle click on matched cell
|
|
4059
|
+
* @param {HTMLElement} cellEl - Cell element
|
|
4060
|
+
* @param {Object} match - Match data
|
|
4061
|
+
* @param {number} rowIndex - Row index
|
|
4062
|
+
* @param {number} colIndex - Column index
|
|
4063
|
+
*/
|
|
4064
|
+
handleCellMatchClick(cellEl, match, rowIndex, colIndex) {
|
|
4065
|
+
if (!this.options.dropdownManager) {
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// Create match data for dropdown
|
|
4070
|
+
const matchData = {
|
|
4071
|
+
text: match.text,
|
|
4072
|
+
command: {
|
|
4073
|
+
id: `csv-cell-${rowIndex}-${colIndex}`,
|
|
4074
|
+
matchType: match.matchType,
|
|
4075
|
+
messageState: match.messageState,
|
|
4076
|
+
category: match.trigger.category,
|
|
4077
|
+
intent: {
|
|
4078
|
+
category: match.trigger.category,
|
|
4079
|
+
handler: match.trigger.handler
|
|
4080
|
+
},
|
|
4081
|
+
handler: match.trigger.handler
|
|
4082
|
+
},
|
|
4083
|
+
intent: {
|
|
4084
|
+
category: match.trigger.category,
|
|
4085
|
+
handler: match.trigger.handler
|
|
4086
|
+
},
|
|
4087
|
+
// Store cell position for updates
|
|
4088
|
+
csvCellPosition: {
|
|
4089
|
+
rowIndex,
|
|
4090
|
+
colIndex,
|
|
4091
|
+
isHeader: rowIndex === 0
|
|
4092
|
+
}
|
|
4093
|
+
};
|
|
4094
|
+
|
|
4095
|
+
// Show dropdown
|
|
4096
|
+
this.options.dropdownManager.showDropdown(cellEl, matchData);
|
|
4097
|
+
|
|
4098
|
+
if (this.options.debug) {
|
|
4099
|
+
console.log('[CSVModalManager] Showing dropdown for cell:', match.text, 'at', rowIndex, colIndex);
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
/**
|
|
4104
|
+
* Update column header with selected option
|
|
4105
|
+
* @param {number} colIndex - Column index
|
|
4106
|
+
* @param {string} appendText - Text to append (e.g., "/PST")
|
|
4107
|
+
*/
|
|
4108
|
+
updateColumnHeader(colIndex, appendText) {
|
|
4109
|
+
if (!this.parsedData || !this.parsedData[0]) {
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
// Update parsed data
|
|
4114
|
+
const originalHeader = this.parsedData[0][colIndex];
|
|
4115
|
+
|
|
4116
|
+
// Remove any existing suffix (text after /)
|
|
4117
|
+
const baseHeader = originalHeader.split('/')[0];
|
|
4118
|
+
this.parsedData[0][colIndex] = baseHeader + appendText;
|
|
4119
|
+
|
|
4120
|
+
// Update the displayed table
|
|
4121
|
+
this.refreshTable();
|
|
4122
|
+
|
|
4123
|
+
if (this.options.debug) {
|
|
4124
|
+
console.log('[CSVModalManager] Updated column', colIndex, 'from', originalHeader, 'to', this.parsedData[0][colIndex]);
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
/**
|
|
4129
|
+
* Refresh the table display
|
|
4130
|
+
*/
|
|
4131
|
+
refreshTable() {
|
|
4132
|
+
if (!this.modal || !this.parsedData) {
|
|
4133
|
+
return;
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
// Find table container
|
|
4137
|
+
const tableContainer = this.modal.querySelector('.tq-csv-table-container');
|
|
4138
|
+
if (!tableContainer) {
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
// Clear and rebuild table
|
|
4143
|
+
tableContainer.innerHTML = '';
|
|
4144
|
+
const table = this.createTable(this.parsedData);
|
|
4145
|
+
tableContainer.appendChild(table);
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
/**
|
|
4149
|
+
* Create HTML table from parsed CSV data
|
|
4150
|
+
* @param {Array} data - 2D array of cells
|
|
4151
|
+
* @returns {HTMLElement} - Table element
|
|
4152
|
+
*/
|
|
4153
|
+
createTable(data) {
|
|
4154
|
+
const table = document.createElement('table');
|
|
4155
|
+
table.className = 'tq-csv-table';
|
|
4156
|
+
|
|
4157
|
+
// Create header row
|
|
4158
|
+
if (data.length > 0) {
|
|
4159
|
+
const thead = document.createElement('thead');
|
|
4160
|
+
const headerRow = document.createElement('tr');
|
|
4161
|
+
|
|
4162
|
+
data[0].forEach((cellValue, colIndex) => {
|
|
4163
|
+
const cell = this.createCell(cellValue, true, 0, colIndex);
|
|
4164
|
+
headerRow.appendChild(cell);
|
|
4165
|
+
});
|
|
4166
|
+
|
|
4167
|
+
thead.appendChild(headerRow);
|
|
4168
|
+
table.appendChild(thead);
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
// Create data rows
|
|
4172
|
+
if (data.length > 1) {
|
|
4173
|
+
const tbody = document.createElement('tbody');
|
|
4174
|
+
|
|
4175
|
+
for (let i = 1; i < data.length; i++) {
|
|
4176
|
+
const row = document.createElement('tr');
|
|
4177
|
+
|
|
4178
|
+
data[i].forEach((cellValue, colIndex) => {
|
|
4179
|
+
const cell = this.createCell(cellValue, false, i, colIndex);
|
|
4180
|
+
row.appendChild(cell);
|
|
4181
|
+
});
|
|
4182
|
+
|
|
4183
|
+
tbody.appendChild(row);
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
table.appendChild(tbody);
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
// Apply table styles
|
|
4190
|
+
if (this.options.styleManager) {
|
|
4191
|
+
this.options.styleManager.applyTableStyles(table);
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
return table;
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
/**
|
|
4198
|
+
* Show CSV modal
|
|
4199
|
+
* @param {Object} csvData - { file, text, metadata }
|
|
4200
|
+
*/
|
|
4201
|
+
show(csvData) {
|
|
4202
|
+
// Close any existing modal
|
|
4203
|
+
this.hide();
|
|
4204
|
+
|
|
4205
|
+
this.currentCSVData = csvData;
|
|
4206
|
+
this.parsedData = this.parseCSV(csvData.text);
|
|
4207
|
+
|
|
4208
|
+
// Create modal backdrop
|
|
4209
|
+
const backdrop = document.createElement('div');
|
|
4210
|
+
backdrop.className = 'tq-csv-modal-backdrop';
|
|
4211
|
+
|
|
4212
|
+
// Create modal
|
|
4213
|
+
const modal = document.createElement('div');
|
|
4214
|
+
modal.className = 'tq-csv-modal';
|
|
4215
|
+
|
|
4216
|
+
// Create header
|
|
4217
|
+
const header = document.createElement('div');
|
|
4218
|
+
header.className = 'tq-csv-modal-header';
|
|
4219
|
+
|
|
4220
|
+
const title = document.createElement('div');
|
|
4221
|
+
title.className = 'tq-csv-modal-title';
|
|
4222
|
+
title.textContent = csvData.file.name;
|
|
4223
|
+
|
|
4224
|
+
const closeBtn = document.createElement('button');
|
|
4225
|
+
closeBtn.className = 'tq-csv-modal-close';
|
|
4226
|
+
closeBtn.innerHTML = '×';
|
|
4227
|
+
closeBtn.onclick = () => this.hide();
|
|
4228
|
+
|
|
4229
|
+
header.appendChild(title);
|
|
4230
|
+
header.appendChild(closeBtn);
|
|
4231
|
+
|
|
4232
|
+
// Create table container (scrollable)
|
|
4233
|
+
const tableContainer = document.createElement('div');
|
|
4234
|
+
tableContainer.className = 'tq-csv-table-container';
|
|
4235
|
+
|
|
4236
|
+
// Create table
|
|
4237
|
+
const table = this.createTable(this.parsedData);
|
|
4238
|
+
tableContainer.appendChild(table);
|
|
4239
|
+
|
|
4240
|
+
// Assemble modal
|
|
4241
|
+
modal.appendChild(header);
|
|
4242
|
+
modal.appendChild(tableContainer);
|
|
4243
|
+
|
|
4244
|
+
// Apply styles
|
|
4245
|
+
if (this.options.styleManager) {
|
|
4246
|
+
this.options.styleManager.applyBackdropStyles(backdrop);
|
|
4247
|
+
this.options.styleManager.applyModalStyles(modal);
|
|
4248
|
+
this.options.styleManager.applyHeaderStyles(header);
|
|
4249
|
+
this.options.styleManager.applyTitleStyles(title);
|
|
4250
|
+
this.options.styleManager.applyCloseButtonStyles(closeBtn);
|
|
4251
|
+
this.options.styleManager.applyTableContainerStyles(tableContainer);
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
// Add to document
|
|
4255
|
+
backdrop.appendChild(modal);
|
|
4256
|
+
document.body.appendChild(backdrop);
|
|
4257
|
+
|
|
4258
|
+
this.modal = backdrop;
|
|
4259
|
+
|
|
4260
|
+
// Close on backdrop click
|
|
4261
|
+
backdrop.addEventListener('click', (e) => {
|
|
4262
|
+
if (e.target === backdrop) {
|
|
4263
|
+
this.hide();
|
|
4264
|
+
}
|
|
4265
|
+
});
|
|
4266
|
+
|
|
4267
|
+
// Close on Escape key
|
|
4268
|
+
this.escapeHandler = (e) => {
|
|
4269
|
+
if (e.key === 'Escape') {
|
|
4270
|
+
this.hide();
|
|
4271
|
+
}
|
|
4272
|
+
};
|
|
4273
|
+
document.addEventListener('keydown', this.escapeHandler);
|
|
4274
|
+
|
|
4275
|
+
if (this.options.debug) {
|
|
4276
|
+
console.log('[CSVModalManager] Modal shown for:', csvData.file.name);
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
/**
|
|
4281
|
+
* Hide modal
|
|
4282
|
+
*/
|
|
4283
|
+
hide() {
|
|
4284
|
+
if (this.modal) {
|
|
4285
|
+
this.modal.remove();
|
|
4286
|
+
this.modal = null;
|
|
4287
|
+
this.currentCSVData = null;
|
|
4288
|
+
this.parsedData = null;
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
if (this.escapeHandler) {
|
|
4292
|
+
document.removeEventListener('keydown', this.escapeHandler);
|
|
4293
|
+
this.escapeHandler = null;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
if (this.options.debug) {
|
|
4297
|
+
console.log('[CSVModalManager] Modal hidden');
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
/**
|
|
4302
|
+
* Cleanup
|
|
4303
|
+
*/
|
|
4304
|
+
destroy() {
|
|
4305
|
+
this.hide();
|
|
4306
|
+
if (this.options.debug) {
|
|
4307
|
+
console.log('[CSVModalManager] Destroyed');
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
// CSVModalStyleManager - Handles styling for CSV modal
|
|
4313
|
+
// Single responsibility: apply consistent styles to CSV modal elements
|
|
4314
|
+
|
|
4315
|
+
class CSVModalStyleManager {
|
|
4316
|
+
/**
|
|
4317
|
+
* Create CSV modal style manager
|
|
4318
|
+
* @param {Object} options - Style options
|
|
4319
|
+
*/
|
|
4320
|
+
constructor(options = {}) {
|
|
4321
|
+
this.options = {
|
|
4322
|
+
// Modal colors
|
|
4323
|
+
backdropColor: options.backdropColor || 'rgba(0, 0, 0, 0.5)',
|
|
4324
|
+
modalBackground: options.modalBackground || '#ffffff',
|
|
4325
|
+
|
|
4326
|
+
// Header colors
|
|
4327
|
+
headerBackground: options.headerBackground || '#f8fafc',
|
|
4328
|
+
headerBorder: options.headerBorder || '#e2e8f0',
|
|
4329
|
+
|
|
4330
|
+
// Table colors
|
|
4331
|
+
tableBorder: options.tableBorder || '#e2e8f0',
|
|
4332
|
+
headerCellBackground: options.headerCellBackground || '#f1f5f9',
|
|
4333
|
+
cellBorder: options.cellBorder || '#e2e8f0',
|
|
4334
|
+
|
|
4335
|
+
// Highlight colors (matching textarea triggers)
|
|
4336
|
+
errorHighlight: options.errorHighlight || '#fee2e2',
|
|
4337
|
+
errorBorder: options.errorBorder || '#ef4444',
|
|
4338
|
+
warningHighlight: options.warningHighlight || '#fef3c7',
|
|
4339
|
+
warningBorder: options.warningBorder || '#f59e0b',
|
|
4340
|
+
infoHighlight: options.infoHighlight || '#dbeafe',
|
|
4341
|
+
infoBorder: options.infoBorder || '#3b82f6',
|
|
4342
|
+
|
|
4343
|
+
...options
|
|
4344
|
+
};
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
/**
|
|
4348
|
+
* Apply backdrop styles
|
|
4349
|
+
* @param {HTMLElement} backdrop - Backdrop element
|
|
4350
|
+
*/
|
|
4351
|
+
applyBackdropStyles(backdrop) {
|
|
4352
|
+
Object.assign(backdrop.style, {
|
|
4353
|
+
position: 'fixed',
|
|
4354
|
+
top: '0',
|
|
4355
|
+
left: '0',
|
|
4356
|
+
width: '100%',
|
|
4357
|
+
height: '100%',
|
|
4358
|
+
backgroundColor: this.options.backdropColor,
|
|
4359
|
+
display: 'flex',
|
|
4360
|
+
alignItems: 'center',
|
|
4361
|
+
justifyContent: 'center',
|
|
4362
|
+
zIndex: '10000',
|
|
4363
|
+
padding: '20px'
|
|
4364
|
+
});
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
/**
|
|
4368
|
+
* Apply modal styles
|
|
4369
|
+
* @param {HTMLElement} modal - Modal element
|
|
4370
|
+
*/
|
|
4371
|
+
applyModalStyles(modal) {
|
|
4372
|
+
Object.assign(modal.style, {
|
|
4373
|
+
backgroundColor: this.options.modalBackground,
|
|
4374
|
+
borderRadius: '8px',
|
|
4375
|
+
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.2)',
|
|
4376
|
+
maxWidth: '90vw',
|
|
4377
|
+
maxHeight: '90vh',
|
|
4378
|
+
width: '100%',
|
|
4379
|
+
height: '100%',
|
|
4380
|
+
display: 'flex',
|
|
4381
|
+
flexDirection: 'column',
|
|
4382
|
+
overflow: 'hidden'
|
|
4383
|
+
});
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
/**
|
|
4387
|
+
* Apply header styles
|
|
4388
|
+
* @param {HTMLElement} header - Header element
|
|
4389
|
+
*/
|
|
4390
|
+
applyHeaderStyles(header) {
|
|
4391
|
+
Object.assign(header.style, {
|
|
4392
|
+
display: 'flex',
|
|
4393
|
+
alignItems: 'center',
|
|
4394
|
+
justifyContent: 'space-between',
|
|
4395
|
+
padding: '16px 20px',
|
|
4396
|
+
borderBottom: `1px solid ${this.options.headerBorder}`,
|
|
4397
|
+
backgroundColor: this.options.headerBackground,
|
|
4398
|
+
flexShrink: '0'
|
|
4399
|
+
});
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
/**
|
|
4403
|
+
* Apply title styles
|
|
4404
|
+
* @param {HTMLElement} title - Title element
|
|
4405
|
+
*/
|
|
4406
|
+
applyTitleStyles(title) {
|
|
4407
|
+
Object.assign(title.style, {
|
|
4408
|
+
fontSize: '18px',
|
|
4409
|
+
fontWeight: '600',
|
|
4410
|
+
color: '#1e293b'
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
/**
|
|
4415
|
+
* Apply close button styles
|
|
4416
|
+
* @param {HTMLElement} button - Close button element
|
|
4417
|
+
*/
|
|
4418
|
+
applyCloseButtonStyles(button) {
|
|
4419
|
+
Object.assign(button.style, {
|
|
4420
|
+
background: 'transparent',
|
|
4421
|
+
border: 'none',
|
|
4422
|
+
fontSize: '28px',
|
|
4423
|
+
color: '#64748b',
|
|
4424
|
+
cursor: 'pointer',
|
|
4425
|
+
lineHeight: '1',
|
|
4426
|
+
padding: '0',
|
|
4427
|
+
width: '32px',
|
|
4428
|
+
height: '32px',
|
|
4429
|
+
display: 'flex',
|
|
4430
|
+
alignItems: 'center',
|
|
4431
|
+
justifyContent: 'center',
|
|
4432
|
+
borderRadius: '4px',
|
|
4433
|
+
transition: 'background-color 0.2s, color 0.2s'
|
|
4434
|
+
});
|
|
4435
|
+
|
|
4436
|
+
button.addEventListener('mouseenter', () => {
|
|
4437
|
+
button.style.backgroundColor = '#f1f5f9';
|
|
4438
|
+
button.style.color = '#1e293b';
|
|
4439
|
+
});
|
|
4440
|
+
|
|
4441
|
+
button.addEventListener('mouseleave', () => {
|
|
4442
|
+
button.style.backgroundColor = 'transparent';
|
|
4443
|
+
button.style.color = '#64748b';
|
|
4444
|
+
});
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
/**
|
|
4448
|
+
* Apply table container styles (scrollable)
|
|
4449
|
+
* @param {HTMLElement} container - Table container element
|
|
4450
|
+
*/
|
|
4451
|
+
applyTableContainerStyles(container) {
|
|
4452
|
+
Object.assign(container.style, {
|
|
4453
|
+
flex: '1',
|
|
4454
|
+
overflow: 'auto',
|
|
4455
|
+
padding: '20px'
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
/**
|
|
4460
|
+
* Apply table styles
|
|
4461
|
+
* @param {HTMLElement} table - Table element
|
|
4462
|
+
*/
|
|
4463
|
+
applyTableStyles(table) {
|
|
4464
|
+
Object.assign(table.style, {
|
|
4465
|
+
borderCollapse: 'collapse',
|
|
4466
|
+
width: '100%',
|
|
4467
|
+
fontSize: '14px',
|
|
4468
|
+
backgroundColor: '#ffffff'
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
/**
|
|
4473
|
+
* Apply header cell styles
|
|
4474
|
+
* @param {HTMLElement} cell - Header cell element
|
|
4475
|
+
*/
|
|
4476
|
+
applyHeaderCellStyles(cell) {
|
|
4477
|
+
Object.assign(cell.style, {
|
|
4478
|
+
padding: '12px 16px',
|
|
4479
|
+
textAlign: 'left',
|
|
4480
|
+
fontWeight: '600',
|
|
4481
|
+
backgroundColor: this.options.headerCellBackground,
|
|
4482
|
+
border: `1px solid ${this.options.tableBorder}`,
|
|
4483
|
+
color: '#1e293b',
|
|
4484
|
+
position: 'sticky',
|
|
4485
|
+
top: '0',
|
|
4486
|
+
zIndex: '10',
|
|
4487
|
+
whiteSpace: 'nowrap'
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
|
|
4491
|
+
/**
|
|
4492
|
+
* Apply data cell styles
|
|
4493
|
+
* @param {HTMLElement} cell - Data cell element
|
|
4494
|
+
*/
|
|
4495
|
+
applyDataCellStyles(cell) {
|
|
4496
|
+
Object.assign(cell.style, {
|
|
4497
|
+
padding: '10px 16px',
|
|
4498
|
+
border: `1px solid ${this.options.cellBorder}`,
|
|
4499
|
+
color: '#334155',
|
|
4500
|
+
whiteSpace: 'nowrap'
|
|
4501
|
+
});
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
/**
|
|
4505
|
+
* Apply match highlighting styles to cell content
|
|
4506
|
+
* @param {HTMLElement} span - Span element
|
|
4507
|
+
* @param {string} messageState - Message state (error, warning, info)
|
|
4508
|
+
*/
|
|
4509
|
+
applyCellMatchStyles(span, messageState) {
|
|
4510
|
+
const colorMap = {
|
|
4511
|
+
'error': {
|
|
4512
|
+
background: this.options.errorHighlight,
|
|
4513
|
+
border: this.options.errorBorder
|
|
4514
|
+
},
|
|
4515
|
+
'warning': {
|
|
4516
|
+
background: this.options.warningHighlight,
|
|
4517
|
+
border: this.options.warningBorder
|
|
4518
|
+
},
|
|
4519
|
+
'info': {
|
|
4520
|
+
background: this.options.infoHighlight,
|
|
4521
|
+
border: this.options.infoBorder
|
|
4522
|
+
}
|
|
4523
|
+
};
|
|
4524
|
+
|
|
4525
|
+
const colors = colorMap[messageState] || colorMap.info;
|
|
4526
|
+
|
|
4527
|
+
Object.assign(span.style, {
|
|
4528
|
+
backgroundColor: colors.background,
|
|
4529
|
+
borderBottom: `2px solid ${colors.border}`,
|
|
4530
|
+
padding: '2px 4px',
|
|
4531
|
+
borderRadius: '3px',
|
|
4532
|
+
cursor: 'pointer',
|
|
4533
|
+
transition: 'background-color 0.2s',
|
|
4534
|
+
display: 'inline-block'
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4537
|
+
// Hover effect
|
|
4538
|
+
span.addEventListener('mouseenter', () => {
|
|
4539
|
+
span.style.backgroundColor = this.adjustBrightness(colors.background, -10);
|
|
4540
|
+
});
|
|
4541
|
+
|
|
4542
|
+
span.addEventListener('mouseleave', () => {
|
|
4543
|
+
span.style.backgroundColor = colors.background;
|
|
4544
|
+
});
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4547
|
+
/**
|
|
4548
|
+
* Adjust color brightness
|
|
4549
|
+
* @param {string} color - Hex color
|
|
4550
|
+
* @param {number} amount - Amount to adjust (-255 to 255)
|
|
4551
|
+
* @returns {string} - Adjusted color
|
|
4552
|
+
*/
|
|
4553
|
+
adjustBrightness(color, amount) {
|
|
4554
|
+
// Simple brightness adjustment for hex colors
|
|
4555
|
+
const hex = color.replace('#', '');
|
|
4556
|
+
const num = parseInt(hex, 16);
|
|
4557
|
+
const r = Math.max(0, Math.min(255, (num >> 16) + amount));
|
|
4558
|
+
const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount));
|
|
4559
|
+
const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount));
|
|
4560
|
+
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
/**
|
|
4564
|
+
* Update theme colors dynamically
|
|
4565
|
+
* @param {Object} newColors - New color options
|
|
4566
|
+
*/
|
|
4567
|
+
updateTheme(newColors) {
|
|
4568
|
+
Object.assign(this.options, newColors);
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
|
|
3252
4572
|
// TrustQuery - Lightweight library to make textareas interactive
|
|
3253
4573
|
// Turns matching words into interactive elements with hover bubbles and click actions
|
|
3254
4574
|
|
|
@@ -3794,5 +5114,5 @@ class TrustQuery {
|
|
|
3794
5114
|
}
|
|
3795
5115
|
}
|
|
3796
5116
|
|
|
3797
|
-
export { TrustQuery as default };
|
|
5117
|
+
export { AttachmentManager, AttachmentStyleManager, CSVModalManager, CSVModalStyleManager, TrustQuery as default };
|
|
3798
5118
|
//# sourceMappingURL=trustquery.js.map
|