mbg-val-input 0.0.0 → 0.1.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,386 @@
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
192
  // if bType === "Enum"
24
193
  @property({ type: Array }) enumVals: string[] = [];
25
194
 
26
- @property({type: String}) label = 'Val';
195
+ @property({ type: String }) label = 'Val';
27
196
 
28
197
  @query('#input') input?: HTMLInputElement;
29
198
 
199
+ private updateValue(event: Event, userValue: string) {
200
+ this.value = userValue;
201
+ // eslint-disable-next-line no-param-reassign
202
+ (event.target as HTMLInputElement).value = userValue;
203
+ }
204
+
205
+ private parseBTypeSize() {
206
+ const match = this.bType?.match(/INT(\d+)/);
207
+ return match ? parseInt(match[1], 10) : 0;
208
+ }
209
+
210
+ private handleBigIntInput(event: Event) {
211
+ const input = (event.target as HTMLInputElement).value;
212
+ const validatedInput = sanitizeAndValidateInt(
213
+ input,
214
+ this.parseBTypeSize(),
215
+ true,
216
+ );
217
+ const validatedDefault = sanitizeAndValidateInt(
218
+ this.default,
219
+ this.parseBTypeSize(),
220
+ false,
221
+ );
222
+
223
+ if (validatedInput === '') {
224
+ this.updateValue(event, validatedDefault ?? '0');
225
+ return;
226
+ }
227
+
228
+ this.updateValue(event, validatedInput);
229
+ }
230
+
231
+ private handleIntInput(event: Event) {
232
+ const input = (event.target as HTMLInputElement).value;
233
+ const signed = !this.bType?.endsWith('U');
234
+ const validatedDefault = sanitizeAndValidateInt(
235
+ this.default,
236
+ this.parseBTypeSize(),
237
+ false,
238
+ signed,
239
+ );
240
+
241
+ // set default if input is empty
242
+ if (input === '') {
243
+ this.updateValue(event, validatedDefault ?? 0);
244
+ }
245
+ }
246
+
30
247
  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;
248
+ const min = signed ? -(2 ** (size - 1)) : 0;
249
+ const max = signed ? 2 ** (size - 1) - 1 : 2 ** size - 1;
250
+
251
+ const validatedDefault = sanitizeAndValidateInt(
252
+ this.default,
253
+ this.parseBTypeSize(),
254
+ false,
255
+ signed,
256
+ );
33
257
 
34
258
  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>`;
259
+ return html`<md-outlined-text-field
260
+ id="input"
261
+ label="${this.label}"
262
+ type="text"
263
+ value="${validatedDefault ?? nothing}"
264
+ @input="${this.handleBigIntInput}"
265
+ ></md-outlined-text-field>`;
36
266
  }
37
267
 
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>`;
268
+ return html`<md-outlined-text-field
269
+ id="input"
270
+ label="${this.label}"
271
+ type="number"
272
+ step="1"
273
+ min="${min}"
274
+ max="${max}"
275
+ value="${validatedDefault ?? nothing}"
276
+ @input="${this.handleIntInput}"
277
+ ></md-outlined-text-field>`;
278
+ }
279
+
280
+ private handleFloatInput(event: Event) {
281
+ const input = (event.target as HTMLInputElement).value;
282
+ const validatedInput = sanitizeAndValidateFloat(
283
+ input,
284
+ true,
285
+ this.bType === 'FLOAT32' ? 32 : 64,
286
+ );
287
+ const validatedDefault = sanitizeAndValidateFloat(
288
+ this.default,
289
+ false,
290
+ this.bType === 'FLOAT32' ? 32 : 64,
291
+ );
292
+
293
+ if (validatedInput === '') {
294
+ this.updateValue(event, validatedDefault ?? '0');
295
+ return;
296
+ }
297
+
298
+ this.updateValue(event, validatedInput);
299
+ }
300
+
301
+ floatInput() {
302
+ const validatedDefault = sanitizeAndValidateFloat(
303
+ this.default,
304
+ false,
305
+ this.bType === 'FLOAT32' ? 32 : 64,
306
+ );
307
+ return html`
308
+ <md-outlined-text-field
309
+ id="$input"
310
+ label="${this.label}"
311
+ type="text"
312
+ value="${validatedDefault ?? nothing}"
313
+ @input="${this.handleFloatInput}"
314
+ ></md-outlined-text-field>
315
+ `;
316
+ }
317
+
318
+ private handleOctetInput(event: Event) {
319
+ const input = (event.target as HTMLInputElement).value;
320
+ let sanitizedInput = sanitizeOctet(input);
321
+ if (sanitizedInput === '') {
322
+ sanitizedInput = sanitizeOctet(this.default) ?? '';
323
+ }
324
+ this.updateValue(event, sanitizedInput);
325
+ }
326
+
327
+ octetInput(size: 6 | 16 | 64) {
328
+ return html`
329
+ <md-outlined-text-field
330
+ id="input"
331
+ label="${this.label}"
332
+ value="${sanitizeOctet(this.default) ?? nothing}"
333
+ maxlength="${size * 2}"
334
+ @input="${this.handleOctetInput}"
335
+ ></md-outlined-text-field>
336
+ `;
337
+ }
338
+
339
+ private handleStringInput(event: Event) {
340
+ let input = (event.target as HTMLInputElement).value;
341
+ if (input === '') {
342
+ input = this.default ?? '';
343
+ }
344
+ this.updateValue(event, input);
345
+ }
346
+
347
+ stringInput(size: 32 | 64 | 65 | 129 | 255) {
348
+ return html`
349
+ <md-outlined-text-field
350
+ id="input"
351
+ label="${this.label}"
352
+ value="${this.default ?? nothing}"
353
+ maxlength="${size}"
354
+ @input="${this.handleStringInput}"
355
+ ></md-outlined-text-field>
356
+ `;
357
+ }
358
+
359
+ qualityInput() {
360
+ return html`
361
+ <md-filled-select id="input" label="${this.label}">
362
+ <md-select-option value="QUALITY_VALIDITY_GOOD" ?selected=${this.default === 'QUALITY_VALIDITY_GOOD'}>
363
+ <div slot="headline">QUALITY_VALIDITY_GOOD</div>
364
+ </md-select-option>
365
+ <md-select-option value="QUALITY_VALIDITY_INVALID" ?selected=${this.default === 'QUALITY_VALIDITY_INVALID'}>
366
+ <div slot="headline">QUALITY_VALIDITY_INVALID</div>
367
+ </md-select-option>
368
+ </md-select-option>
369
+ <md-select-option value="QUALITY_VALIDITY_RESERVED" ?selected=${this.default === 'QUALITY_VALIDITY_RESERVED'}>
370
+ <div slot="headline">QUALITY_VALIDITY_RESERVED</div>
371
+ </md-select-option>
372
+ </md-select-option>
373
+ <md-select-option value="QUALITY_VALIDITY_QUESTIONABLE" ?selected=${this.default === 'QUALITY_VALIDITY_QUESTIONABLE'}>
374
+ <div slot="headline">QUALITY_VALIDITY_QUESTIONABLE</div>
375
+ </md-select-option>
376
+ </md-filled-select>
377
+ `;
39
378
  }
40
379
 
41
380
  get value() {
42
381
  if (this.bType === 'BOOLEAN') {
43
- return (this.input as unknown as MdSwitch).selected? "true" : "false";
382
+ return (this.input as unknown as MdSwitch).selected ? 'true' : 'false';
44
383
  }
45
- return this.input?.value??"";
384
+ return this.input?.value ?? '';
46
385
  }
47
386
 
48
387
  set value(val: string) {
@@ -50,20 +389,45 @@ export class MbgValInput extends LitElement {
50
389
  }
51
390
 
52
391
  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),};
392
+ return {
393
+ BOOLEAN: html`<label
394
+ >${this.label}
395
+ <md-switch id="input" ?selected=${xmlBoolean(this.default)}></md-switch
396
+ ></label>`,
397
+ INT8: this.intInput(8),
398
+ INT16: this.intInput(16),
399
+ INT24: this.intInput(24),
400
+ INT32: this.intInput(32),
401
+ INT64: this.intInput(64),
402
+ INT128: this.intInput(128),
403
+ INT8U: this.intInput(8, false),
404
+ INT16U: this.intInput(16, false),
405
+ INT24U: this.intInput(24, false),
406
+ INT32U: this.intInput(32, false),
407
+ FLOAT32: this.floatInput(),
408
+ FLOAT64: this.floatInput(),
409
+ Octet6: this.octetInput(6),
410
+ Octet16: this.octetInput(16),
411
+ Octet64: this.octetInput(64),
412
+ VisString32: this.stringInput(32),
413
+ VisString64: this.stringInput(64),
414
+ VisString65: this.stringInput(65),
415
+ VisString129: this.stringInput(129),
416
+ VisString255: this.stringInput(255),
417
+ Unicode255: this.stringInput(255),
418
+ Quality: this.qualityInput(),
419
+ Currency: currencyInput(this.label, this.default),
420
+ };
64
421
  }
65
422
 
66
423
  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>`;
424
+ return (
425
+ this.inputs[this.bType ?? ''] ??
426
+ html`<md-outlined-text-field
427
+ id="input"
428
+ label="${this.label}"
429
+ value="${this.default ?? nothing}"
430
+ ></md-outlined-text-field>`
431
+ );
68
432
  }
69
433
  }