Warning, /firebird/packages/firebird-ng/display/src/lib/components/data-selector/source-picker.component.ts is written in an unsupported language. File is not indexed.
0001 import { ChangeDetectionStrategy, Component, computed, effect, input, output, signal, untracked, viewChild } from '@angular/core';
0002 import { MatIcon } from '@angular/material/icon';
0003 import { MatIconButton } from '@angular/material/button';
0004 import { MatTooltip } from '@angular/material/tooltip';
0005 import type { DataSource } from '@dexvis/firebird-core';
0006 import { ResourceSelectComponent } from '../resource-select/resource-select.component';
0007
0008 /**
0009 * One data source field of the Manual tab: pick a known URL or type one
0010 * (Select), or drop/choose a local file (Upload). Used once for geometry and
0011 * once for events. Emits the URL string, the File, or '' when cleared. The
0012 * file is never uploaded anywhere: loaders read it in place.
0013 */
0014 @Component({
0015 selector: 'firebird-source-picker',
0016 changeDetection: ChangeDetectionStrategy.OnPush,
0017 imports: [MatIcon, MatIconButton, MatTooltip, ResourceSelectComponent],
0018 template: `
0019 <div class="picker-header">
0020 <span class="picker-label">{{ label() }}</span>
0021 <div class="mode-switch" role="tablist">
0022 <button type="button" class="mode" [class.active]="mode() === 'select'" (click)="setMode('select')">Select</button>
0023 <button type="button" class="mode" [class.active]="mode() === 'upload'" (click)="setMode('upload')">Upload</button>
0024 </div>
0025 </div>
0026
0027 @if (mode() === 'select') {
0028 <firebird-resource-select
0029 #urlSelect
0030 [options]="options()"
0031 [selected]="urlValue()"
0032 [label]="placeholder()"
0033 (valueChange)="onUrlTyped($event)">
0034 </firebird-resource-select>
0035 } @else {
0036 @if (fileValue(); as file) {
0037 <div class="file-row">
0038 <mat-icon>insert_drive_file</mat-icon>
0039 <span class="file-name" [title]="file.name">{{ file.name }}</span>
0040 <button mat-icon-button class="clear-btn" matTooltip="Remove this file" (click)="clearFile()">
0041 <mat-icon>close</mat-icon>
0042 </button>
0043 </div>
0044 }
0045 <label class="drop-zone"
0046 [class.drag-over]="dragOver()"
0047 (dragover)="onDragOver($event)"
0048 (dragleave)="onDragLeave($event)"
0049 (drop)="onDrop($event)">
0050 <input type="file" [accept]="accept()" hidden (change)="onFilePicked($event)"/>
0051 <mat-icon>upload_file</mat-icon>
0052 <span class="drop-hint">Drop a file here, or click to choose</span>
0053 @if (hint()) {
0054 <span class="drop-note">{{ hint() }}</span>
0055 }
0056 </label>
0057 }
0058 `,
0059 styles: `
0060 @use './shared';
0061
0062 :host {
0063 display: flex;
0064 flex-direction: column;
0065 gap: 6px;
0066 }
0067
0068 .picker-header {
0069 display: flex;
0070 align-items: center;
0071 justify-content: space-between;
0072 gap: 8px;
0073 }
0074
0075 .picker-label {
0076 @include shared.section-label;
0077 }
0078
0079 .mode-switch {
0080 display: inline-flex;
0081 border: 1px solid var(--mat-sys-outline, rgba(128, 128, 128, 0.6));
0082 border-radius: 6px;
0083 overflow: hidden;
0084 }
0085
0086 .mode {
0087 padding: 2px 10px;
0088 border: none;
0089 background: transparent;
0090 color: inherit;
0091 font: inherit;
0092 font-size: 0.75rem;
0093 cursor: pointer;
0094 opacity: 0.75;
0095
0096 &.active {
0097 opacity: 1;
0098 background: color-mix(in srgb, var(--mat-sys-primary, #4bac84) 25%, transparent);
0099 }
0100 }
0101
0102 .drop-zone {
0103 display: flex;
0104 flex-direction: column;
0105 align-items: center;
0106 gap: 4px;
0107 padding: 14px 12px;
0108 border: 2px dashed var(--mat-sys-outline, rgba(128, 128, 128, 0.6));
0109 border-radius: 10px;
0110 cursor: pointer;
0111 text-align: center;
0112 transition: border-color 0.15s, background 0.15s;
0113
0114 &:hover,
0115 &.drag-over {
0116 border-color: var(--mat-sys-primary, #4bac84);
0117 background: color-mix(in srgb, var(--mat-sys-primary, #4bac84) 8%, transparent);
0118 }
0119
0120 mat-icon {
0121 font-size: 26px;
0122 width: 26px;
0123 height: 26px;
0124 opacity: 0.8;
0125 }
0126 }
0127
0128 .drop-hint {
0129 font-size: 0.8rem;
0130 }
0131
0132 .drop-note {
0133 font-size: 0.72rem;
0134 opacity: 0.7;
0135 }
0136
0137 .file-row {
0138 display: flex;
0139 align-items: center;
0140 gap: 6px;
0141 font-size: 0.82rem;
0142
0143 mat-icon {
0144 font-size: 18px;
0145 width: 18px;
0146 height: 18px;
0147 opacity: 0.8;
0148 }
0149 }
0150
0151 .file-name {
0152 flex: 1;
0153 min-width: 0;
0154 overflow: hidden;
0155 text-overflow: ellipsis;
0156 white-space: nowrap;
0157 }
0158
0159 .clear-btn {
0160 width: 28px;
0161 height: 28px;
0162
0163 mat-icon {
0164 font-size: 18px;
0165 width: 18px;
0166 height: 18px;
0167 }
0168 }
0169 `,
0170 })
0171 export class SourcePickerComponent {
0172 readonly label = input.required<string>();
0173 /** URLs offered in the Select drop-down; any other URL can be typed. */
0174 readonly options = input<string[]>([]);
0175 readonly placeholder = input('URL');
0176 /** File input accept list, e.g. '.root' or '.root,.json,.zip'. */
0177 readonly accept = input('.root');
0178 /** Small note under the drop zone. */
0179 readonly hint = input('');
0180 /** Current value; the host owns it and passes it back after routing. */
0181 readonly value = input<DataSource | ''>('');
0182 /**
0183 * Bump to make the text field show `value` again when the host kept the
0184 * value but the typed text went elsewhere (a geometry file typed into the
0185 * events field moves to the geometry field; this field must then show what
0186 * it still holds, not the moved text).
0187 */
0188 readonly resyncToken = input(0);
0189 readonly valueChange = output<DataSource | ''>();
0190
0191 private readonly urlSelect = viewChild<ResourceSelectComponent>('urlSelect');
0192
0193 readonly mode = signal<'select' | 'upload'>('select');
0194 readonly dragOver = signal(false);
0195
0196 readonly urlValue = computed(() => (typeof this.value() === 'string' ? this.value() as string : ''));
0197 readonly fileValue = computed(() => (this.value() instanceof File ? this.value() as File : null));
0198
0199 constructor() {
0200 effect(() => {
0201 this.resyncToken();
0202 const select = this.urlSelect();
0203 if (select) select.selected = untracked(this.urlValue);
0204 });
0205 }
0206
0207 setMode(mode: 'select' | 'upload'): void {
0208 this.mode.set(mode);
0209 }
0210
0211 onUrlTyped(url: string): void {
0212 this.valueChange.emit((url ?? '').trim());
0213 }
0214
0215 onFilePicked(event: Event): void {
0216 const input = event.target as HTMLInputElement;
0217 const file = input.files?.[0];
0218 // Clear the input so picking the same file again still fires a change
0219 input.value = '';
0220 if (file) this.valueChange.emit(file);
0221 }
0222
0223 clearFile(): void {
0224 this.valueChange.emit('');
0225 }
0226
0227 onDragOver(event: DragEvent): void {
0228 event.preventDefault();
0229 this.dragOver.set(true);
0230 }
0231
0232 onDragLeave(event: DragEvent): void {
0233 event.preventDefault();
0234 this.dragOver.set(false);
0235 }
0236
0237 onDrop(event: DragEvent): void {
0238 event.preventDefault();
0239 this.dragOver.set(false);
0240 const file = event.dataTransfer?.files?.[0];
0241 if (file) this.valueChange.emit(file);
0242 }
0243 }