|
1 | | -import { Component, EventEmitter, Input, Output } from '@angular/core'; |
| 1 | +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; |
2 | 2 | import { BlockParameterDto, SettingInputMode } from 'src/app/main/dtos/config/block-descriptor.dto'; |
3 | 3 | import { BlockSettingDto } from 'src/app/main/dtos/config/block-instance.dto'; |
4 | 4 | import { ConfigStackerComponent } from '../../config-stacker.component'; |
5 | 5 |
|
| 6 | +enum ByteArrayViewMode { |
| 7 | + Base64 = 'base64', |
| 8 | + Hex = 'hex', |
| 9 | +} |
| 10 | + |
6 | 11 | @Component({ |
7 | 12 | selector: 'app-byte-array-setting', |
8 | 13 | templateUrl: './byte-array-setting.component.html', |
9 | 14 | styleUrls: ['./byte-array-setting.component.scss'], |
10 | 15 | }) |
11 | | -export class ByteArraySettingComponent { |
| 16 | +export class ByteArraySettingComponent implements OnChanges { |
12 | 17 | @Input() parameter: BlockParameterDto | null = null; |
13 | 18 | @Input() setting!: BlockSettingDto; |
14 | 19 | @Input() stacker!: ConfigStackerComponent; |
15 | 20 | @Output() onChange: EventEmitter<void> = new EventEmitter<void>(); |
16 | 21 |
|
17 | 22 | SettingInputMode = SettingInputMode; |
| 23 | + ByteArrayViewMode = ByteArrayViewMode; |
| 24 | + |
| 25 | + viewMode = ByteArrayViewMode.Base64; |
| 26 | + displayValue = ''; |
| 27 | + isValid = true; |
| 28 | + isTouched = false; |
| 29 | + |
| 30 | + private lastBase64Value = ''; |
| 31 | + |
| 32 | + ngOnChanges(changes: SimpleChanges): void { |
| 33 | + if (!changes['setting'] && this.getBase64Value() === this.lastBase64Value) { |
| 34 | + return; |
| 35 | + } |
| 36 | + |
| 37 | + this.syncDisplayValue(); |
| 38 | + } |
18 | 39 |
|
19 | 40 | changeMode(mode: SettingInputMode) { |
20 | 41 | this.setting.inputMode = mode; |
21 | 42 | this.onChange.emit(); |
22 | 43 | } |
23 | 44 |
|
24 | | - // Given a base64 string, sets the value of the setting. |
25 | | - // biome-ignore lint/suspicious/noExplicitAny: This function is only called with events from the input element. |
26 | | - setValue(event: any) { |
27 | | - // Make sure the input is a valid base64 string. |
28 | | - if ( |
29 | | - event === null || |
30 | | - event === undefined || |
31 | | - !/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(event) |
32 | | - ) { |
| 45 | + changeViewMode(mode: ByteArrayViewMode) { |
| 46 | + if (this.viewMode === mode) { |
33 | 47 | return; |
34 | 48 | } |
35 | 49 |
|
36 | | - this.setting.value = event; |
37 | | - this.valueChanged(); |
| 50 | + this.viewMode = mode; |
| 51 | + this.syncDisplayValue(); |
38 | 52 | } |
39 | 53 |
|
40 | | - valueChanged() { |
| 54 | + valueChanging(event: Event) { |
| 55 | + this.displayValue = (event.target as HTMLInputElement).value; |
| 56 | + this.applyDisplayValue(); |
| 57 | + this.isTouched = true; |
| 58 | + } |
| 59 | + |
| 60 | + inputChanged(event: Event) { |
| 61 | + this.displayValue = (event.target as HTMLInputElement).value; |
| 62 | + this.applyDisplayValue(); |
| 63 | + this.isTouched = true; |
| 64 | + } |
| 65 | + |
| 66 | + emitValueChanged() { |
41 | 67 | this.onChange.emit(); |
42 | 68 | } |
| 69 | + |
| 70 | + computeClass(): string { |
| 71 | + let finalClass = 'input-small w-100 monospace'; |
| 72 | + |
| 73 | + if (this.isTouched) { |
| 74 | + finalClass += this.isValid ? ' input-valid' : ' input-invalid'; |
| 75 | + } |
| 76 | + |
| 77 | + return finalClass; |
| 78 | + } |
| 79 | + |
| 80 | + private applyDisplayValue() { |
| 81 | + const base64Value = this.tryConvertToBase64(this.displayValue); |
| 82 | + this.isValid = base64Value !== null; |
| 83 | + |
| 84 | + if (base64Value === null) { |
| 85 | + return; |
| 86 | + } |
| 87 | + |
| 88 | + this.setting.value = base64Value; |
| 89 | + this.lastBase64Value = base64Value; |
| 90 | + this.emitValueChanged(); |
| 91 | + } |
| 92 | + |
| 93 | + private syncDisplayValue() { |
| 94 | + const base64Value = this.getBase64Value(); |
| 95 | + this.lastBase64Value = base64Value; |
| 96 | + this.displayValue = this.viewMode === ByteArrayViewMode.Hex |
| 97 | + ? this.base64ToHex(base64Value) |
| 98 | + : base64Value; |
| 99 | + this.isValid = true; |
| 100 | + this.isTouched = false; |
| 101 | + } |
| 102 | + |
| 103 | + private getBase64Value(): string { |
| 104 | + return typeof this.setting?.value === 'string' ? this.setting.value : ''; |
| 105 | + } |
| 106 | + |
| 107 | + private tryConvertToBase64(value: string): string | null { |
| 108 | + if (this.viewMode === ByteArrayViewMode.Hex) { |
| 109 | + return this.tryHexToBase64(value); |
| 110 | + } |
| 111 | + |
| 112 | + return this.isValidBase64(value) ? value : null; |
| 113 | + } |
| 114 | + |
| 115 | + private isValidBase64(value: string): boolean { |
| 116 | + return /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(value); |
| 117 | + } |
| 118 | + |
| 119 | + private tryHexToBase64(value: string): string | null { |
| 120 | + const normalized = value.replace(/\s/g, '').replace(/0x/g, ''); |
| 121 | + |
| 122 | + if (!/^([0-9a-fA-F]{2})*$/.test(normalized)) { |
| 123 | + return null; |
| 124 | + } |
| 125 | + |
| 126 | + const bytes = []; |
| 127 | + for (let i = 0; i < normalized.length; i += 2) { |
| 128 | + bytes.push(String.fromCharCode(Number.parseInt(normalized.substring(i, i + 2), 16))); |
| 129 | + } |
| 130 | + |
| 131 | + return btoa(bytes.join('')); |
| 132 | + } |
| 133 | + |
| 134 | + private base64ToHex(value: string): string { |
| 135 | + if (!this.isValidBase64(value)) { |
| 136 | + return ''; |
| 137 | + } |
| 138 | + |
| 139 | + return Array.from(atob(value), (char) => char.charCodeAt(0).toString(16).padStart(2, '0')).join(''); |
| 140 | + } |
43 | 141 | } |
0 commit comments