mbg-val-input 0.0.1 → 0.2.0

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.
@@ -2,47 +2,416 @@
2
2
  import { html, css, LitElement, TemplateResult, nothing } from 'lit';
3
3
  import { property, query } from 'lit/decorators.js';
4
4
  import type { MdSwitch } from '@material/web/switch/switch.js';
5
+ import { currencyInput } from './currency-input.js';
5
6
 
6
7
  import '@material/web/textfield/outlined-text-field.js';
8
+ import '@material/web/select/filled-select.js';
9
+ import '@material/web/select/select-option.js';
7
10
  import '@material/web/switch/switch.js';
8
11
 
9
12
  function xmlBoolean(val?: string) {
10
- return ['true', '1'].includes(val?.toLowerCase().trim()??"false");
13
+ return ['true', '1'].includes(val?.toLowerCase().trim() ?? 'false');
14
+ }
15
+
16
+ function isValidInt(value: number, size: number, signed: boolean): boolean {
17
+ const min = signed ? -(2 ** (size - 1)) : 0;
18
+ const max = signed ? 2 ** (size - 1) - 1 : 2 ** size - 1;
19
+ return value >= min && value <= max;
20
+ }
21
+
22
+ function isValidBigInt(value: bigint, size: 64 | 128): boolean {
23
+ if (size === 64) {
24
+ return value >= -9223372036854775808n && value <= 9223372036854775807n;
25
+ }
26
+ if (size === 128) {
27
+ return (
28
+ value >= -170141183460469231731687303715884105728n &&
29
+ value <= 170141183460469231731687303715884105727n
30
+ );
31
+ }
32
+ return false;
33
+ }
34
+
35
+ function sanitizeAndValidateInt(
36
+ value: string,
37
+ size: number,
38
+ isUserInput: boolean,
39
+ isSigned = true,
40
+ ) {
41
+ // allow leading negative sign
42
+ const signed = value[0] === '-' ? '-' : '';
43
+ const negative = signed === '-';
44
+
45
+ // remove leading zeros and invalid characters
46
+ let sanitized = value.replace(/[^0-9]/g, '').replace(/^0+(?=\d)/, '');
47
+
48
+ // handle special cases
49
+ if (negative && !sanitized.startsWith('-')) {
50
+ sanitized = signed + sanitized;
51
+ }
52
+ if (sanitized === '-0') {
53
+ sanitized = '0';
54
+ }
55
+
56
+ // ensure input was properly sanitized
57
+ const intRegex = /^-?\d*$/;
58
+ if (!intRegex.test(sanitized)) {
59
+ return '';
60
+ }
61
+
62
+ // allow leading negative sign only if it is from user input
63
+ if (sanitized === '') {
64
+ if (isUserInput) {
65
+ return sanitized;
66
+ }
67
+ return '';
68
+ }
69
+
70
+ // parse and validate
71
+ if (sanitized !== '') {
72
+ if (size === 64 || size === 128) {
73
+ const parsedValue = BigInt(sanitized);
74
+ if (Number.isNaN(parsedValue) || !isValidBigInt(parsedValue, size)) {
75
+ return '';
76
+ }
77
+ } else {
78
+ const parsedValue = Number(sanitized);
79
+ if (
80
+ Number.isNaN(parsedValue) ||
81
+ !isValidInt(parsedValue, size, isSigned)
82
+ ) {
83
+ return '';
84
+ }
85
+ }
86
+ }
87
+
88
+ return sanitized;
89
+ }
90
+
91
+ function isValidFloat(value: number, size: 32 | 64): boolean {
92
+ if (size === 32) {
93
+ return value >= -3.4028235e38 && value <= 3.4028235e38;
94
+ }
95
+ if (size === 64) {
96
+ return value >= -1.7976931348623157e308 && value <= 1.7976931348623157e308;
97
+ }
98
+ return false;
99
+ }
100
+
101
+ /** Removes invalid characters and validates the float value input */
102
+ function sanitizeAndValidateFloat(
103
+ value: string,
104
+ isUserInput: boolean,
105
+ size: 32 | 64,
106
+ ) {
107
+ let sanitized = '';
108
+
109
+ // allow leading negative sign
110
+ const signed = value[0] === '-' ? '-' : '';
111
+ const negative = signed === '-';
112
+
113
+ // account for Euler's number
114
+ if (value === 'e' || value === '-e') {
115
+ sanitized = '2.7182818284590452354';
116
+ return signed + sanitized;
117
+ }
118
+
119
+ // remove leading zeros and invalid characters
120
+ sanitized = value
121
+ .replace(/[^0-9eE.+-]/g, '')
122
+ .replace(/(?<![eE])\+|(?<![eE])-/, '');
123
+
124
+ // handle multiple exponents
125
+ const numExponents = sanitized.split(/[eE]/).length - 1;
126
+ if (numExponents > 1) {
127
+ sanitized = sanitized.replace(/[eE].*?[eE]/g, 'E0');
128
+ }
129
+ if (sanitized.endsWith('e') || sanitized.endsWith('E')) {
130
+ sanitized += '0';
131
+ }
132
+
133
+ // handle multiple decimal points
134
+ const numDecimalPoints = sanitized.split('.').length - 1;
135
+ if (numDecimalPoints > 1) {
136
+ sanitized = sanitized.replace(/\.(?=.*\.)/g, '');
137
+ }
138
+
139
+ // remove leading zeros
140
+ sanitized = sanitized.replace(/^0+(?=\d)/, '');
141
+ sanitized = sanitized.replace(/^-0+(?=\d)/, '-');
142
+
143
+ // handle special cases
144
+ if (sanitized === '0e' || sanitized === '-0e') {
145
+ return '0';
146
+ }
147
+ if (negative && !sanitized.startsWith('-')) {
148
+ sanitized = signed + sanitized;
149
+ }
150
+ if (sanitized === '.') {
151
+ sanitized = '0.';
152
+ }
153
+
154
+ // ensure input was properly sanitized
155
+ const floatRegex = /^-?\d*(\.\d*)?([eE][-+]?\d*)?$/;
156
+ if (!floatRegex.test(sanitized)) {
157
+ return '';
158
+ }
159
+
160
+ // allow leading negative sign only if it is from user input
161
+ if (sanitized === '-') {
162
+ if (isUserInput) {
163
+ return sanitized;
164
+ }
165
+ return '';
166
+ }
167
+
168
+ // parse into float and validate
169
+ if (sanitized !== '') {
170
+ const parsedValue = parseFloat(sanitized);
171
+ if (Number.isNaN(parsedValue) || !isValidFloat(parsedValue, size)) {
172
+ return '';
173
+ }
174
+ }
175
+
176
+ return sanitized;
177
+ }
178
+
179
+ function sanitizeOctet(value: string) {
180
+ return value.replace(/[^0-9a-fA-F]/g, '');
11
181
  }
12
182
 
13
183
  /** Web Component for inputting IEC61850-6 "Val" values */
14
184
  export class MbgValInput extends LitElement {
15
- static styles = css`
16
- `;
185
+ static styles = css``;
17
186
 
18
187
  @property({ type: String }) bType?: string;
19
-
188
+
20
189
  // If the DA/BDA is assigned a Val in DataTypeTemplates
21
- @property({ type: String }) default = "";
190
+ @property({ type: String }) default = '';
22
191
 
23
- // if bType === "Enum"
24
- @property({ type: Array }) enumVals: string[] = [];
192
+ // if bType === "Enum" - ordinal values
193
+ @property({ type: Array }) enumOrdinals: string[] = [];
25
194
 
26
- @property({type: String}) label = 'Val';
195
+ // if bType === "Enum" - string labels
196
+ @property({ type: Array }) enumLabels: string[] = [];
197
+
198
+ @property({ type: String }) label = 'Val';
27
199
 
28
200
  @query('#input') input?: HTMLInputElement;
29
201
 
202
+ private updateValue(event: Event, userValue: string) {
203
+ this.value = userValue;
204
+ // eslint-disable-next-line no-param-reassign
205
+ (event.target as HTMLInputElement).value = userValue;
206
+ }
207
+
208
+ private parseBTypeSize() {
209
+ const match = this.bType?.match(/INT(\d+)/);
210
+ return match ? parseInt(match[1], 10) : 0;
211
+ }
212
+
213
+ private handleBigIntInput(event: Event) {
214
+ const input = (event.target as HTMLInputElement).value;
215
+ const validatedInput = sanitizeAndValidateInt(
216
+ input,
217
+ this.parseBTypeSize(),
218
+ true,
219
+ );
220
+ const validatedDefault = sanitizeAndValidateInt(
221
+ this.default,
222
+ this.parseBTypeSize(),
223
+ false,
224
+ );
225
+
226
+ if (validatedInput === '') {
227
+ this.updateValue(event, validatedDefault ?? '0');
228
+ return;
229
+ }
230
+
231
+ this.updateValue(event, validatedInput);
232
+ }
233
+
234
+ private handleIntInput(event: Event) {
235
+ const input = (event.target as HTMLInputElement).value;
236
+ const signed = !this.bType?.endsWith('U');
237
+ const validatedDefault = sanitizeAndValidateInt(
238
+ this.default,
239
+ this.parseBTypeSize(),
240
+ false,
241
+ signed,
242
+ );
243
+
244
+ // set default if input is empty
245
+ if (input === '') {
246
+ this.updateValue(event, validatedDefault ?? 0);
247
+ }
248
+ }
249
+
30
250
  intInput(size: 8 | 16 | 24 | 32 | 64 | 128, signed = true) {
31
- const min = signed? -(2**(size-1)) : 0;
32
- const max = signed? 2**(size-1)-1 : 2**(size)-1;
251
+ const min = signed ? -(2 ** (size - 1)) : 0;
252
+ const max = signed ? 2 ** (size - 1) - 1 : 2 ** size - 1;
253
+
254
+ const validatedDefault = sanitizeAndValidateInt(
255
+ this.default,
256
+ this.parseBTypeSize(),
257
+ false,
258
+ signed,
259
+ );
33
260
 
34
261
  if (size > 52) {
35
- return html`<md-outlined-text-field id="input" label="${this.label}" type="number" step="1" value="${this.default??nothing}"></md-outlined-text-field>`;
262
+ return html`<md-outlined-text-field
263
+ id="input"
264
+ label="${this.label}"
265
+ type="text"
266
+ value="${validatedDefault ?? nothing}"
267
+ @input="${this.handleBigIntInput}"
268
+ ></md-outlined-text-field>`;
269
+ }
270
+
271
+ return html`<md-outlined-text-field
272
+ id="input"
273
+ label="${this.label}"
274
+ type="number"
275
+ step="1"
276
+ min="${min}"
277
+ max="${max}"
278
+ value="${validatedDefault ?? nothing}"
279
+ @input="${this.handleIntInput}"
280
+ ></md-outlined-text-field>`;
281
+ }
282
+
283
+ private handleFloatInput(event: Event) {
284
+ const input = (event.target as HTMLInputElement).value;
285
+ const validatedInput = sanitizeAndValidateFloat(
286
+ input,
287
+ true,
288
+ this.bType === 'FLOAT32' ? 32 : 64,
289
+ );
290
+ const validatedDefault = sanitizeAndValidateFloat(
291
+ this.default,
292
+ false,
293
+ this.bType === 'FLOAT32' ? 32 : 64,
294
+ );
295
+
296
+ if (validatedInput === '') {
297
+ this.updateValue(event, validatedDefault ?? '0');
298
+ return;
299
+ }
300
+
301
+ this.updateValue(event, validatedInput);
302
+ }
303
+
304
+ floatInput() {
305
+ const validatedDefault = sanitizeAndValidateFloat(
306
+ this.default,
307
+ false,
308
+ this.bType === 'FLOAT32' ? 32 : 64,
309
+ );
310
+ return html`
311
+ <md-outlined-text-field
312
+ id="$input"
313
+ label="${this.label}"
314
+ type="text"
315
+ value="${validatedDefault ?? nothing}"
316
+ @input="${this.handleFloatInput}"
317
+ ></md-outlined-text-field>
318
+ `;
319
+ }
320
+
321
+ private handleOctetInput(event: Event) {
322
+ const input = (event.target as HTMLInputElement).value;
323
+ let sanitizedInput = sanitizeOctet(input);
324
+ if (sanitizedInput === '') {
325
+ sanitizedInput = sanitizeOctet(this.default) ?? '';
326
+ }
327
+ this.updateValue(event, sanitizedInput);
328
+ }
329
+
330
+ octetInput(size: 6 | 16 | 64) {
331
+ return html`
332
+ <md-outlined-text-field
333
+ id="input"
334
+ label="${this.label}"
335
+ value="${sanitizeOctet(this.default) ?? nothing}"
336
+ maxlength="${size * 2}"
337
+ @input="${this.handleOctetInput}"
338
+ ></md-outlined-text-field>
339
+ `;
340
+ }
341
+
342
+ private handleStringInput(event: Event) {
343
+ let input = (event.target as HTMLInputElement).value;
344
+ if (input === '') {
345
+ input = this.default ?? '';
346
+ }
347
+ this.updateValue(event, input);
348
+ }
349
+
350
+ stringInput(size: 32 | 64 | 65 | 129 | 255) {
351
+ return html`
352
+ <md-outlined-text-field
353
+ id="input"
354
+ label="${this.label}"
355
+ value="${this.default ?? nothing}"
356
+ maxlength="${size}"
357
+ @input="${this.handleStringInput}"
358
+ ></md-outlined-text-field>
359
+ `;
360
+ }
361
+
362
+ qualityInput() {
363
+ return html`
364
+ <md-filled-select id="input" label="${this.label}">
365
+ <md-select-option value="QUALITY_VALIDITY_GOOD" ?selected=${this.default === 'QUALITY_VALIDITY_GOOD'}>
366
+ <div slot="headline">QUALITY_VALIDITY_GOOD</div>
367
+ </md-select-option>
368
+ <md-select-option value="QUALITY_VALIDITY_INVALID" ?selected=${this.default === 'QUALITY_VALIDITY_INVALID'}>
369
+ <div slot="headline">QUALITY_VALIDITY_INVALID</div>
370
+ </md-select-option>
371
+ </md-select-option>
372
+ <md-select-option value="QUALITY_VALIDITY_RESERVED" ?selected=${this.default === 'QUALITY_VALIDITY_RESERVED'}>
373
+ <div slot="headline">QUALITY_VALIDITY_RESERVED</div>
374
+ </md-select-option>
375
+ </md-select-option>
376
+ <md-select-option value="QUALITY_VALIDITY_QUESTIONABLE" ?selected=${this.default === 'QUALITY_VALIDITY_QUESTIONABLE'}>
377
+ <div slot="headline">QUALITY_VALIDITY_QUESTIONABLE</div>
378
+ </md-select-option>
379
+ </md-filled-select>
380
+ `;
381
+ }
382
+
383
+ enumInput() {
384
+ let ordinals: string[] = [];
385
+ let labels: string[] = [];
386
+
387
+ if (typeof this.enumOrdinals === 'string') {
388
+ ordinals = JSON.parse(this.enumOrdinals) as string[];
389
+ }
390
+ if (typeof this.enumLabels === 'string') {
391
+ labels = JSON.parse(this.enumLabels) as string[];
36
392
  }
37
393
 
38
- return html`<md-outlined-text-field id="input" label="${this.label}" type="number" step="1" min="${min}" max="${max}" value="${this.default??nothing}"></md-outlined-text-field>`;
394
+ return html`
395
+ <md-filled-select id="input" label="${this.label}">
396
+ ${ordinals.map(
397
+ (ordinal, index) => html`
398
+ <md-select-option
399
+ value="${ordinal}"
400
+ ?selected=${this.default === ordinal}
401
+ >
402
+ <div slot="headline">${ordinal} - ${labels[index]}</div>
403
+ </md-select-option>
404
+ `,
405
+ )}
406
+ </md-filled-select>
407
+ `;
39
408
  }
40
409
 
41
410
  get value() {
42
411
  if (this.bType === 'BOOLEAN') {
43
- return (this.input as unknown as MdSwitch).selected? "true" : "false";
412
+ return (this.input as unknown as MdSwitch).selected ? 'true' : 'false';
44
413
  }
45
- return this.input?.value??"";
414
+ return this.input?.value ?? '';
46
415
  }
47
416
 
48
417
  set value(val: string) {
@@ -50,20 +419,46 @@ export class MbgValInput extends LitElement {
50
419
  }
51
420
 
52
421
  get inputs(): Partial<Record<string, TemplateResult>> {
53
- return {BOOLEAN: html`<label>${this.label} <md-switch id="input" ?selected=${xmlBoolean(this.default)}></md-switch></label>`,
54
- INT8: this.intInput(8),
55
- INT16: this.intInput(16),
56
- INT24: this.intInput(24),
57
- INT32: this.intInput(32),
58
- INT64: this.intInput(64),
59
- INT128: this.intInput(128),
60
- INT8U: this.intInput(8, false),
61
- INT16U: this.intInput(16, false),
62
- INT24U: this.intInput(24, false),
63
- INT32U: this.intInput(32, false),};
422
+ return {
423
+ BOOLEAN: html`<label
424
+ >${this.label}
425
+ <md-switch id="input" ?selected=${xmlBoolean(this.default)}></md-switch
426
+ ></label>`,
427
+ INT8: this.intInput(8),
428
+ INT16: this.intInput(16),
429
+ INT24: this.intInput(24),
430
+ INT32: this.intInput(32),
431
+ INT64: this.intInput(64),
432
+ INT128: this.intInput(128),
433
+ INT8U: this.intInput(8, false),
434
+ INT16U: this.intInput(16, false),
435
+ INT24U: this.intInput(24, false),
436
+ INT32U: this.intInput(32, false),
437
+ FLOAT32: this.floatInput(),
438
+ FLOAT64: this.floatInput(),
439
+ Octet6: this.octetInput(6),
440
+ Octet16: this.octetInput(16),
441
+ Octet64: this.octetInput(64),
442
+ VisString32: this.stringInput(32),
443
+ VisString64: this.stringInput(64),
444
+ VisString65: this.stringInput(65),
445
+ VisString129: this.stringInput(129),
446
+ VisString255: this.stringInput(255),
447
+ Unicode255: this.stringInput(255),
448
+ Quality: this.qualityInput(),
449
+ Currency: currencyInput(this.label, this.default),
450
+ Enum: this.enumInput(),
451
+ };
64
452
  }
65
453
 
66
454
  render() {
67
- return this.inputs[this.bType??'']??html`<md-outlined-text-field id="input" label="${this.label}" value="${this.default??nothing}"></md-outlined-text-field>`;
455
+ return (
456
+ this.inputs[this.bType ?? ''] ??
457
+ html`<md-outlined-text-field
458
+ id="input"
459
+ label="${this.label}"
460
+ value="${this.default ?? nothing}"
461
+ ></md-outlined-text-field>`
462
+ );
68
463
  }
69
464
  }