@haloduck/ui 2.0.44 → 2.0.46
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/fesm2022/haloduck-ui.mjs +178 -166
- package/fesm2022/haloduck-ui.mjs.map +1 -1
- package/haloduck-ui-2.0.46.tgz +0 -0
- package/index.d.ts +1 -0
- package/package.json +1 -1
- package/public/i18n/haloduck/en.json +2 -1
- package/public/i18n/haloduck/ko.json +2 -1
- package/src/tailwind.css +18 -0
package/fesm2022/haloduck-ui.mjs
CHANGED
|
@@ -2333,6 +2333,167 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
|
|
|
2333
2333
|
args: ['canvas', { static: true }]
|
|
2334
2334
|
}] } });
|
|
2335
2335
|
|
|
2336
|
+
class TagInputComponent {
|
|
2337
|
+
label;
|
|
2338
|
+
inputEl;
|
|
2339
|
+
placeholder = '';
|
|
2340
|
+
disabled = false;
|
|
2341
|
+
allowDuplicates = false;
|
|
2342
|
+
// Two-way binding support independent of CVA
|
|
2343
|
+
set value(tags) {
|
|
2344
|
+
if (Array.isArray(tags)) {
|
|
2345
|
+
this.tags = tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
|
|
2346
|
+
}
|
|
2347
|
+
else {
|
|
2348
|
+
this.tags = [];
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
valueChange = new EventEmitter();
|
|
2352
|
+
tags = [];
|
|
2353
|
+
inputValue = '';
|
|
2354
|
+
onChange = () => { };
|
|
2355
|
+
onTouched = () => { };
|
|
2356
|
+
writeValue(value) {
|
|
2357
|
+
if (Array.isArray(value)) {
|
|
2358
|
+
this.tags = value.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
|
|
2359
|
+
}
|
|
2360
|
+
else {
|
|
2361
|
+
this.tags = [];
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
registerOnChange(fn) {
|
|
2365
|
+
this.onChange = fn;
|
|
2366
|
+
}
|
|
2367
|
+
registerOnTouched(fn) {
|
|
2368
|
+
this.onTouched = fn;
|
|
2369
|
+
}
|
|
2370
|
+
setDisabledState(isDisabled) {
|
|
2371
|
+
this.disabled = isDisabled;
|
|
2372
|
+
}
|
|
2373
|
+
ngAfterViewInit() {
|
|
2374
|
+
// hide label if no projected content
|
|
2375
|
+
if (this.label && this.label.nativeElement) {
|
|
2376
|
+
const hasContent = this.label.nativeElement.textContent?.trim();
|
|
2377
|
+
if (!hasContent) {
|
|
2378
|
+
this.label.nativeElement.style.display = 'none';
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
focus() {
|
|
2383
|
+
this.inputEl?.nativeElement?.focus();
|
|
2384
|
+
}
|
|
2385
|
+
onInput(event) {
|
|
2386
|
+
const input = event.target;
|
|
2387
|
+
this.inputValue = input.value;
|
|
2388
|
+
this.onTouched();
|
|
2389
|
+
}
|
|
2390
|
+
onBlur() {
|
|
2391
|
+
if (this.disabled)
|
|
2392
|
+
return;
|
|
2393
|
+
this.inputValue = '';
|
|
2394
|
+
this.onTouched();
|
|
2395
|
+
}
|
|
2396
|
+
onKeydown(event) {
|
|
2397
|
+
if (this.disabled)
|
|
2398
|
+
return;
|
|
2399
|
+
// Commit on comma or Enter
|
|
2400
|
+
if (event.key === ',' || event.key === 'Enter') {
|
|
2401
|
+
event.preventDefault();
|
|
2402
|
+
this.commitCurrentInput();
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
// Backspace behavior
|
|
2406
|
+
if (event.key === 'Backspace') {
|
|
2407
|
+
if (this.inputValue.length > 0) {
|
|
2408
|
+
return; // default backspace in input
|
|
2409
|
+
}
|
|
2410
|
+
if (this.tags.length > 0) {
|
|
2411
|
+
event.preventDefault();
|
|
2412
|
+
this.removeTag(this.tags.length - 1);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
removeTag(index) {
|
|
2418
|
+
if (this.disabled)
|
|
2419
|
+
return;
|
|
2420
|
+
if (index < 0 || index >= this.tags.length)
|
|
2421
|
+
return;
|
|
2422
|
+
this.tags = this.tags.filter((_, i) => i !== index);
|
|
2423
|
+
this.emitChanges();
|
|
2424
|
+
}
|
|
2425
|
+
commitCurrentInput() {
|
|
2426
|
+
const raw = this.inputValue.trim();
|
|
2427
|
+
if (!raw) {
|
|
2428
|
+
this.inputValue = '';
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
// If user pasted multiple comma-separated values, split and add all
|
|
2432
|
+
const parts = raw
|
|
2433
|
+
.split(',')
|
|
2434
|
+
.map((p) => p.trim())
|
|
2435
|
+
.filter((p) => p.length > 0);
|
|
2436
|
+
for (const part of parts) {
|
|
2437
|
+
this.addTag(part);
|
|
2438
|
+
}
|
|
2439
|
+
this.inputValue = '';
|
|
2440
|
+
}
|
|
2441
|
+
addTag(tag) {
|
|
2442
|
+
if (!this.allowDuplicates) {
|
|
2443
|
+
const exists = this.tags.some((t) => t.toLowerCase() === tag.toLowerCase());
|
|
2444
|
+
if (exists) {
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
this.tags = [...this.tags, tag];
|
|
2449
|
+
this.emitChanges();
|
|
2450
|
+
}
|
|
2451
|
+
emitChanges() {
|
|
2452
|
+
const cleaned = this.tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
|
|
2453
|
+
if (cleaned.length !== this.tags.length || cleaned.some((t, i) => t !== this.tags[i])) {
|
|
2454
|
+
this.tags = cleaned;
|
|
2455
|
+
}
|
|
2456
|
+
this.onChange(this.tags);
|
|
2457
|
+
this.valueChange.emit(this.tags);
|
|
2458
|
+
}
|
|
2459
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2460
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: TagInputComponent, isStandalone: true, selector: "haloduck-tag-input", inputs: { placeholder: "placeholder", disabled: "disabled", allowDuplicates: "allowDuplicates", value: "value" }, outputs: { valueChange: "valueChange" }, providers: [
|
|
2461
|
+
{
|
|
2462
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2463
|
+
useExisting: forwardRef(() => TagInputComponent),
|
|
2464
|
+
multi: true,
|
|
2465
|
+
},
|
|
2466
|
+
provideTranslocoScope('haloduck'),
|
|
2467
|
+
], viewQueries: [{ propertyName: "label", first: true, predicate: ["label"], descendants: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-2\">\n <label\n #label\n class=\"block text-sm/6 font-medium text-light-on-control dark:text-dark-on-control text-left\"\n >\n <ng-content></ng-content>\n </label>\n\n <div\n class=\"tag-input-wrapper block w-full rounded-md bg-light-control dark:bg-dark-control disabled:bg-light-control/60 dark:disabled:bg-dark-control/80 px-2 py-1.5 text-base text-light-on-control dark:text-dark-on-control disabled:cursor-not-allowed disabled:text-light-on-control/60 dark:disabled:text-dark-on-control/80 outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive placeholder:text-light-inactive dark:placeholder:text-dark-inactive sm:text-sm/6\"\n >\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (tag of tags; track tag; let i = $index) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-light-secondary dark:bg-dark-secondary text-light-on-secondary dark:text-dark-on-secondary px-2 py-0.5 text-xs\"\n >\n {{ tag }}\n @if (!disabled) {\n <button\n type=\"button\"\n (click)=\"removeTag(i)\"\n class=\"text-light-on-secondary/80 hover:text-light-on-secondary dark:text-dark-on-secondary/80 dark:hover:text-dark-on-secondary hover:cursor-pointer\"\n >\n \u00D7\n </button>\n }\n </span>\n }\n\n <input\n #inputEl\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n class=\"flex-1 min-w-[8rem] bg-transparent outline-none placeholder:text-light-inactive dark:placeholder:text-dark-inactive\"\n [value]=\"inputValue\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.tag-input-wrapper:focus-within{outline-width:2px;outline-offset:2px;outline-color:var(--color-light-primary)!important}@media (prefers-color-scheme: dark){.tag-input-wrapper:focus-within{outline-color:var(--color-dark-primary)!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
2468
|
+
}
|
|
2469
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, decorators: [{
|
|
2470
|
+
type: Component,
|
|
2471
|
+
args: [{ selector: 'haloduck-tag-input', imports: [CommonModule], providers: [
|
|
2472
|
+
{
|
|
2473
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2474
|
+
useExisting: forwardRef(() => TagInputComponent),
|
|
2475
|
+
multi: true,
|
|
2476
|
+
},
|
|
2477
|
+
provideTranslocoScope('haloduck'),
|
|
2478
|
+
], template: "<div class=\"flex flex-col gap-2\">\n <label\n #label\n class=\"block text-sm/6 font-medium text-light-on-control dark:text-dark-on-control text-left\"\n >\n <ng-content></ng-content>\n </label>\n\n <div\n class=\"tag-input-wrapper block w-full rounded-md bg-light-control dark:bg-dark-control disabled:bg-light-control/60 dark:disabled:bg-dark-control/80 px-2 py-1.5 text-base text-light-on-control dark:text-dark-on-control disabled:cursor-not-allowed disabled:text-light-on-control/60 dark:disabled:text-dark-on-control/80 outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive placeholder:text-light-inactive dark:placeholder:text-dark-inactive sm:text-sm/6\"\n >\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (tag of tags; track tag; let i = $index) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-light-secondary dark:bg-dark-secondary text-light-on-secondary dark:text-dark-on-secondary px-2 py-0.5 text-xs\"\n >\n {{ tag }}\n @if (!disabled) {\n <button\n type=\"button\"\n (click)=\"removeTag(i)\"\n class=\"text-light-on-secondary/80 hover:text-light-on-secondary dark:text-dark-on-secondary/80 dark:hover:text-dark-on-secondary hover:cursor-pointer\"\n >\n \u00D7\n </button>\n }\n </span>\n }\n\n <input\n #inputEl\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n class=\"flex-1 min-w-[8rem] bg-transparent outline-none placeholder:text-light-inactive dark:placeholder:text-dark-inactive\"\n [value]=\"inputValue\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.tag-input-wrapper:focus-within{outline-width:2px;outline-offset:2px;outline-color:var(--color-light-primary)!important}@media (prefers-color-scheme: dark){.tag-input-wrapper:focus-within{outline-color:var(--color-dark-primary)!important}}\n"] }]
|
|
2479
|
+
}], propDecorators: { label: [{
|
|
2480
|
+
type: ViewChild,
|
|
2481
|
+
args: ['label']
|
|
2482
|
+
}], inputEl: [{
|
|
2483
|
+
type: ViewChild,
|
|
2484
|
+
args: ['inputEl']
|
|
2485
|
+
}], placeholder: [{
|
|
2486
|
+
type: Input
|
|
2487
|
+
}], disabled: [{
|
|
2488
|
+
type: Input
|
|
2489
|
+
}], allowDuplicates: [{
|
|
2490
|
+
type: Input
|
|
2491
|
+
}], value: [{
|
|
2492
|
+
type: Input
|
|
2493
|
+
}], valueChange: [{
|
|
2494
|
+
type: Output
|
|
2495
|
+
}] } });
|
|
2496
|
+
|
|
2336
2497
|
const ERROR_NOT_ACCEPTABLE_FILE_TYPE = 'NOT_ACCEPTABLE_FILE_TYPE';
|
|
2337
2498
|
const ERROR_OVER_SIZE = 'OVER_SIZE';
|
|
2338
2499
|
const ERROR_OVER_COUNT = 'OVER_COUNT';
|
|
@@ -2382,6 +2543,12 @@ class FileUploaderComponent {
|
|
|
2382
2543
|
this.fileRemoved.emit(removedFile);
|
|
2383
2544
|
this.onChange(this.files);
|
|
2384
2545
|
}
|
|
2546
|
+
onFileTagChanged(index, tags) {
|
|
2547
|
+
if (this.files[index]) {
|
|
2548
|
+
this.files[index].tag = tags;
|
|
2549
|
+
this.onChange(this.files);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2385
2552
|
onDragOver(event) {
|
|
2386
2553
|
event.preventDefault();
|
|
2387
2554
|
this.isDragOver = true;
|
|
@@ -2429,11 +2596,15 @@ class FileUploaderComponent {
|
|
|
2429
2596
|
if (notAcceptedFiles.length > 0) {
|
|
2430
2597
|
this.error.emit(notAcceptedFiles);
|
|
2431
2598
|
}
|
|
2432
|
-
// Compute preview URLs for the new files
|
|
2599
|
+
// Compute preview URLs for the new files and initialize tags
|
|
2433
2600
|
filteredFiles.forEach((file) => {
|
|
2434
2601
|
if (file.type.startsWith('image')) {
|
|
2435
2602
|
file.previewUrl = URL.createObjectURL(file);
|
|
2436
2603
|
}
|
|
2604
|
+
// Initialize tag array if not exists
|
|
2605
|
+
if (!file.tag) {
|
|
2606
|
+
file.tag = [];
|
|
2607
|
+
}
|
|
2437
2608
|
});
|
|
2438
2609
|
this.files = this.files.concat(filteredFiles);
|
|
2439
2610
|
this.filesAdded.emit(filteredFiles);
|
|
@@ -2468,7 +2639,9 @@ class FileUploaderComponent {
|
|
|
2468
2639
|
this.files.forEach((file) => {
|
|
2469
2640
|
file.isUploading = true;
|
|
2470
2641
|
file.isUploaded = false;
|
|
2471
|
-
|
|
2642
|
+
// Create a file with tag information for upload
|
|
2643
|
+
const fileWithTag = Object.assign(file, { tag: file.tag || [] });
|
|
2644
|
+
uploadTrigger$.push(this.uploadApi(fileWithTag, this.keyPrefix).pipe(
|
|
2472
2645
|
// Handle the upload API response
|
|
2473
2646
|
tap((key) => {
|
|
2474
2647
|
file.isUploading = false;
|
|
@@ -2498,18 +2671,18 @@ class FileUploaderComponent {
|
|
|
2498
2671
|
multi: true,
|
|
2499
2672
|
},
|
|
2500
2673
|
provideTranslocoScope('haloduck'),
|
|
2501
|
-
], ngImport: i0, template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex items-center justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n @if (isUploading) {\n
|
|
2674
|
+
], ngImport: i0, template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex flex-col sm:flex-row items-stretch sm:items-center justify-center sm:justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md gap-2\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n <div class=\"flex items-center space-x-4\">\n <div class=\"flex-1\">\n <haloduck-tag-input\n placeholder=\"{{ 'haloduck.ui.tag.Please input tags.' | transloco }}\"\n [(value)]=\"file.tag\"\n (valueChange)=\"onFileTagChanged(i, $event)\"\n ></haloduck-tag-input>\n </div>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </div>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: TagInputComponent, selector: "haloduck-tag-input", inputs: ["placeholder", "disabled", "allowDuplicates", "value"], outputs: ["valueChange"] }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
|
|
2502
2675
|
}
|
|
2503
2676
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FileUploaderComponent, decorators: [{
|
|
2504
2677
|
type: Component,
|
|
2505
|
-
args: [{ selector: 'haloduck-file-uploader', imports: [CommonModule, TranslocoModule], providers: [
|
|
2678
|
+
args: [{ selector: 'haloduck-file-uploader', imports: [CommonModule, TranslocoModule, TagInputComponent], providers: [
|
|
2506
2679
|
{
|
|
2507
2680
|
provide: NG_VALUE_ACCESSOR,
|
|
2508
2681
|
useExisting: forwardRef(() => FileUploaderComponent),
|
|
2509
2682
|
multi: true,
|
|
2510
2683
|
},
|
|
2511
2684
|
provideTranslocoScope('haloduck'),
|
|
2512
|
-
], template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex items-center justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n @if (isUploading) {\n
|
|
2685
|
+
], template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex flex-col sm:flex-row items-stretch sm:items-center justify-center sm:justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md gap-2\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n <div class=\"flex items-center space-x-4\">\n <div class=\"flex-1\">\n <haloduck-tag-input\n placeholder=\"{{ 'haloduck.ui.tag.Please input tags.' | transloco }}\"\n [(value)]=\"file.tag\"\n (valueChange)=\"onFileTagChanged(i, $event)\"\n ></haloduck-tag-input>\n </div>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </div>\n </li>\n }\n </ul>\n }\n</div>\n" }]
|
|
2513
2686
|
}], propDecorators: { disabled: [{
|
|
2514
2687
|
type: Input
|
|
2515
2688
|
}], urlPrefix: [{
|
|
@@ -3910,167 +4083,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
|
|
|
3910
4083
|
args: ['label']
|
|
3911
4084
|
}] } });
|
|
3912
4085
|
|
|
3913
|
-
class TagInputComponent {
|
|
3914
|
-
label;
|
|
3915
|
-
inputEl;
|
|
3916
|
-
placeholder = '';
|
|
3917
|
-
disabled = false;
|
|
3918
|
-
allowDuplicates = false;
|
|
3919
|
-
// Two-way binding support independent of CVA
|
|
3920
|
-
set value(tags) {
|
|
3921
|
-
if (Array.isArray(tags)) {
|
|
3922
|
-
this.tags = tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
|
|
3923
|
-
}
|
|
3924
|
-
else {
|
|
3925
|
-
this.tags = [];
|
|
3926
|
-
}
|
|
3927
|
-
}
|
|
3928
|
-
valueChange = new EventEmitter();
|
|
3929
|
-
tags = [];
|
|
3930
|
-
inputValue = '';
|
|
3931
|
-
onChange = () => { };
|
|
3932
|
-
onTouched = () => { };
|
|
3933
|
-
writeValue(value) {
|
|
3934
|
-
if (Array.isArray(value)) {
|
|
3935
|
-
this.tags = value.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
|
|
3936
|
-
}
|
|
3937
|
-
else {
|
|
3938
|
-
this.tags = [];
|
|
3939
|
-
}
|
|
3940
|
-
}
|
|
3941
|
-
registerOnChange(fn) {
|
|
3942
|
-
this.onChange = fn;
|
|
3943
|
-
}
|
|
3944
|
-
registerOnTouched(fn) {
|
|
3945
|
-
this.onTouched = fn;
|
|
3946
|
-
}
|
|
3947
|
-
setDisabledState(isDisabled) {
|
|
3948
|
-
this.disabled = isDisabled;
|
|
3949
|
-
}
|
|
3950
|
-
ngAfterViewInit() {
|
|
3951
|
-
// hide label if no projected content
|
|
3952
|
-
if (this.label && this.label.nativeElement) {
|
|
3953
|
-
const hasContent = this.label.nativeElement.textContent?.trim();
|
|
3954
|
-
if (!hasContent) {
|
|
3955
|
-
this.label.nativeElement.style.display = 'none';
|
|
3956
|
-
}
|
|
3957
|
-
}
|
|
3958
|
-
}
|
|
3959
|
-
focus() {
|
|
3960
|
-
this.inputEl?.nativeElement?.focus();
|
|
3961
|
-
}
|
|
3962
|
-
onInput(event) {
|
|
3963
|
-
const input = event.target;
|
|
3964
|
-
this.inputValue = input.value;
|
|
3965
|
-
this.onTouched();
|
|
3966
|
-
}
|
|
3967
|
-
onBlur() {
|
|
3968
|
-
if (this.disabled)
|
|
3969
|
-
return;
|
|
3970
|
-
this.inputValue = '';
|
|
3971
|
-
this.onTouched();
|
|
3972
|
-
}
|
|
3973
|
-
onKeydown(event) {
|
|
3974
|
-
if (this.disabled)
|
|
3975
|
-
return;
|
|
3976
|
-
// Commit on comma or Enter
|
|
3977
|
-
if (event.key === ',' || event.key === 'Enter') {
|
|
3978
|
-
event.preventDefault();
|
|
3979
|
-
this.commitCurrentInput();
|
|
3980
|
-
return;
|
|
3981
|
-
}
|
|
3982
|
-
// Backspace behavior
|
|
3983
|
-
if (event.key === 'Backspace') {
|
|
3984
|
-
if (this.inputValue.length > 0) {
|
|
3985
|
-
return; // default backspace in input
|
|
3986
|
-
}
|
|
3987
|
-
if (this.tags.length > 0) {
|
|
3988
|
-
event.preventDefault();
|
|
3989
|
-
this.removeTag(this.tags.length - 1);
|
|
3990
|
-
return;
|
|
3991
|
-
}
|
|
3992
|
-
}
|
|
3993
|
-
}
|
|
3994
|
-
removeTag(index) {
|
|
3995
|
-
if (this.disabled)
|
|
3996
|
-
return;
|
|
3997
|
-
if (index < 0 || index >= this.tags.length)
|
|
3998
|
-
return;
|
|
3999
|
-
this.tags = this.tags.filter((_, i) => i !== index);
|
|
4000
|
-
this.emitChanges();
|
|
4001
|
-
}
|
|
4002
|
-
commitCurrentInput() {
|
|
4003
|
-
const raw = this.inputValue.trim();
|
|
4004
|
-
if (!raw) {
|
|
4005
|
-
this.inputValue = '';
|
|
4006
|
-
return;
|
|
4007
|
-
}
|
|
4008
|
-
// If user pasted multiple comma-separated values, split and add all
|
|
4009
|
-
const parts = raw
|
|
4010
|
-
.split(',')
|
|
4011
|
-
.map((p) => p.trim())
|
|
4012
|
-
.filter((p) => p.length > 0);
|
|
4013
|
-
for (const part of parts) {
|
|
4014
|
-
this.addTag(part);
|
|
4015
|
-
}
|
|
4016
|
-
this.inputValue = '';
|
|
4017
|
-
}
|
|
4018
|
-
addTag(tag) {
|
|
4019
|
-
if (!this.allowDuplicates) {
|
|
4020
|
-
const exists = this.tags.some((t) => t.toLowerCase() === tag.toLowerCase());
|
|
4021
|
-
if (exists) {
|
|
4022
|
-
return;
|
|
4023
|
-
}
|
|
4024
|
-
}
|
|
4025
|
-
this.tags = [...this.tags, tag];
|
|
4026
|
-
this.emitChanges();
|
|
4027
|
-
}
|
|
4028
|
-
emitChanges() {
|
|
4029
|
-
const cleaned = this.tags.map((t) => (t ?? '').trim()).filter((t) => t.length > 0);
|
|
4030
|
-
if (cleaned.length !== this.tags.length || cleaned.some((t, i) => t !== this.tags[i])) {
|
|
4031
|
-
this.tags = cleaned;
|
|
4032
|
-
}
|
|
4033
|
-
this.onChange(this.tags);
|
|
4034
|
-
this.valueChange.emit(this.tags);
|
|
4035
|
-
}
|
|
4036
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4037
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: TagInputComponent, isStandalone: true, selector: "haloduck-tag-input", inputs: { placeholder: "placeholder", disabled: "disabled", allowDuplicates: "allowDuplicates", value: "value" }, outputs: { valueChange: "valueChange" }, providers: [
|
|
4038
|
-
{
|
|
4039
|
-
provide: NG_VALUE_ACCESSOR,
|
|
4040
|
-
useExisting: forwardRef(() => TagInputComponent),
|
|
4041
|
-
multi: true,
|
|
4042
|
-
},
|
|
4043
|
-
provideTranslocoScope('haloduck'),
|
|
4044
|
-
], viewQueries: [{ propertyName: "label", first: true, predicate: ["label"], descendants: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-2\">\n <label\n #label\n class=\"block text-sm/6 font-medium text-light-on-control dark:text-dark-on-control text-left\"\n >\n <ng-content></ng-content>\n </label>\n\n <div\n class=\"tag-input-wrapper block w-full rounded-md bg-light-control dark:bg-dark-control disabled:bg-light-control/60 dark:disabled:bg-dark-control/80 px-2 py-1.5 text-base text-light-on-control dark:text-dark-on-control disabled:cursor-not-allowed disabled:text-light-on-control/60 dark:disabled:text-dark-on-control/80 outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive placeholder:text-light-inactive dark:placeholder:text-dark-inactive sm:text-sm/6\"\n >\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (tag of tags; track tag; let i = $index) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-light-secondary dark:bg-dark-secondary text-light-on-secondary dark:text-dark-on-secondary px-2 py-0.5 text-xs\"\n >\n {{ tag }}\n @if (!disabled) {\n <button\n type=\"button\"\n (click)=\"removeTag(i)\"\n class=\"text-light-on-secondary/80 hover:text-light-on-secondary dark:text-dark-on-secondary/80 dark:hover:text-dark-on-secondary hover:cursor-pointer\"\n >\n \u00D7\n </button>\n }\n </span>\n }\n\n <input\n #inputEl\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n class=\"flex-1 min-w-[8rem] bg-transparent outline-none placeholder:text-light-inactive dark:placeholder:text-dark-inactive\"\n [value]=\"inputValue\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.tag-input-wrapper:focus-within{outline-width:2px;outline-offset:2px;outline-color:var(--color-light-primary)!important}@media (prefers-color-scheme: dark){.tag-input-wrapper:focus-within{outline-color:var(--color-dark-primary)!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
4045
|
-
}
|
|
4046
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TagInputComponent, decorators: [{
|
|
4047
|
-
type: Component,
|
|
4048
|
-
args: [{ selector: 'haloduck-tag-input', imports: [CommonModule], providers: [
|
|
4049
|
-
{
|
|
4050
|
-
provide: NG_VALUE_ACCESSOR,
|
|
4051
|
-
useExisting: forwardRef(() => TagInputComponent),
|
|
4052
|
-
multi: true,
|
|
4053
|
-
},
|
|
4054
|
-
provideTranslocoScope('haloduck'),
|
|
4055
|
-
], template: "<div class=\"flex flex-col gap-2\">\n <label\n #label\n class=\"block text-sm/6 font-medium text-light-on-control dark:text-dark-on-control text-left\"\n >\n <ng-content></ng-content>\n </label>\n\n <div\n class=\"tag-input-wrapper block w-full rounded-md bg-light-control dark:bg-dark-control disabled:bg-light-control/60 dark:disabled:bg-dark-control/80 px-2 py-1.5 text-base text-light-on-control dark:text-dark-on-control disabled:cursor-not-allowed disabled:text-light-on-control/60 dark:disabled:text-dark-on-control/80 outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive placeholder:text-light-inactive dark:placeholder:text-dark-inactive sm:text-sm/6\"\n >\n <div class=\"flex flex-wrap items-center gap-2\">\n @for (tag of tags; track tag; let i = $index) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-light-secondary dark:bg-dark-secondary text-light-on-secondary dark:text-dark-on-secondary px-2 py-0.5 text-xs\"\n >\n {{ tag }}\n @if (!disabled) {\n <button\n type=\"button\"\n (click)=\"removeTag(i)\"\n class=\"text-light-on-secondary/80 hover:text-light-on-secondary dark:text-dark-on-secondary/80 dark:hover:text-dark-on-secondary hover:cursor-pointer\"\n >\n \u00D7\n </button>\n }\n </span>\n }\n\n <input\n #inputEl\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n class=\"flex-1 min-w-[8rem] bg-transparent outline-none placeholder:text-light-inactive dark:placeholder:text-dark-inactive\"\n [value]=\"inputValue\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.tag-input-wrapper:focus-within{outline-width:2px;outline-offset:2px;outline-color:var(--color-light-primary)!important}@media (prefers-color-scheme: dark){.tag-input-wrapper:focus-within{outline-color:var(--color-dark-primary)!important}}\n"] }]
|
|
4056
|
-
}], propDecorators: { label: [{
|
|
4057
|
-
type: ViewChild,
|
|
4058
|
-
args: ['label']
|
|
4059
|
-
}], inputEl: [{
|
|
4060
|
-
type: ViewChild,
|
|
4061
|
-
args: ['inputEl']
|
|
4062
|
-
}], placeholder: [{
|
|
4063
|
-
type: Input
|
|
4064
|
-
}], disabled: [{
|
|
4065
|
-
type: Input
|
|
4066
|
-
}], allowDuplicates: [{
|
|
4067
|
-
type: Input
|
|
4068
|
-
}], value: [{
|
|
4069
|
-
type: Input
|
|
4070
|
-
}], valueChange: [{
|
|
4071
|
-
type: Output
|
|
4072
|
-
}] } });
|
|
4073
|
-
|
|
4074
4086
|
class TagViewerComponent {
|
|
4075
4087
|
label;
|
|
4076
4088
|
tags = [];
|