@es-joy/jsoe 0.0.1

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.
Files changed (43) hide show
  1. package/.editorconfig +15 -0
  2. package/.eslintignore +7 -0
  3. package/.eslintrc.cjs +120 -0
  4. package/CHANGES.md +5 -0
  5. package/LICENSE-MIT.txt +22 -0
  6. package/README.md +82 -0
  7. package/package.json +101 -0
  8. package/rollup.config.js +29 -0
  9. package/server.js +25 -0
  10. package/src/deepEqual.js +72 -0
  11. package/src/formats/indexedDBKey.js +66 -0
  12. package/src/formats/json.js +64 -0
  13. package/src/formats/schemaAndArbitrary.js +20 -0
  14. package/src/formats/schemaOnly.js +529 -0
  15. package/src/formats/structuredCloning.js +372 -0
  16. package/src/formats.js +229 -0
  17. package/src/fundamentalTypes/BooleanObjectType.js +55 -0
  18. package/src/fundamentalTypes/NumberObjectType.js +41 -0
  19. package/src/fundamentalTypes/StringObjectType.js +37 -0
  20. package/src/fundamentalTypes/arrayReferenceType.js +203 -0
  21. package/src/fundamentalTypes/arrayType.js +898 -0
  22. package/src/fundamentalTypes/bigintType.js +53 -0
  23. package/src/fundamentalTypes/dateType.js +129 -0
  24. package/src/fundamentalTypes/nullType.js +33 -0
  25. package/src/fundamentalTypes/numberType.js +56 -0
  26. package/src/fundamentalTypes/objectReferenceType.js +51 -0
  27. package/src/fundamentalTypes/objectType.js +30 -0
  28. package/src/fundamentalTypes/regexpType.js +95 -0
  29. package/src/fundamentalTypes/sparseUndefinedType.js +43 -0
  30. package/src/fundamentalTypes/stringType.js +31 -0
  31. package/src/fundamentalTypes/undefinedType.js +37 -0
  32. package/src/index.js +7 -0
  33. package/src/subTypes/blobHTMLType.js +160 -0
  34. package/src/subTypes/falseType.js +42 -0
  35. package/src/subTypes/trueType.js +42 -0
  36. package/src/superTypes/InfinitiesSuperType.js +49 -0
  37. package/src/superTypes/SpecialNumberSuperType.js +62 -0
  38. package/src/typeChoices.js +125 -0
  39. package/src/types.js +665 -0
  40. package/src/utils/dialogs.js +115 -0
  41. package/src/utils/jsonPointer.js +89 -0
  42. package/src/utils/templateUtils.js +106 -0
  43. package/src/utils/types.js +6 -0
@@ -0,0 +1,898 @@
1
+ import {jml, nbsp} from '../../node_modules/jamilih/dist/jml-es.js';
2
+
3
+ import Types, {getPropertyValueFromLegend} from '../types.js';
4
+ import {$e, U, DOM} from '../utils/templateUtils.js';
5
+ import dialogs from '../utils/dialogs.js';
6
+ import {
7
+ resolveJSONPointer, getJSONPointerParts, reduceJSONPointerParts
8
+ } from '../utils/jsonPointer.js';
9
+
10
+ /**
11
+ * @type {TypeObject}
12
+ */
13
+ const arrayType = {
14
+ option: ['Array'],
15
+ array: true,
16
+ regexEndings: [',', ']'],
17
+ stringRegexBegin: /^\[/u,
18
+ stringRegexEnd: /^\]/u,
19
+ toValue (s, {
20
+ format,
21
+ /* istanbul ignore next -- Just a guard */
22
+ endMatchTypeObjs = [],
23
+ remnant: innerContents,
24
+ rootHolder
25
+ }) {
26
+ const {sparse} = this;
27
+ const state = sparse
28
+ ? 'arrayNonindexKeys'
29
+ // ? 'sparseArrays'
30
+ : (this.array ? 'array' : 'object');
31
+ const retObj = this.array ? [] : {};
32
+ let stringVal = innerContents !== undefined
33
+ ? innerContents
34
+ /* istanbul ignore next -- Unreachable? */
35
+ : s;
36
+ const checkEnd = (beginOnly) => {
37
+ if (beginOnly && endMatchTypeObjs.length) {
38
+ const endMatch = stringVal.match(
39
+ endMatchTypeObjs.slice(-1)[0].stringRegexEnd
40
+ );
41
+ if (endMatch) {
42
+ endMatchTypeObjs.pop(); // Safe now to extract
43
+ stringVal = stringVal.slice(endMatch[0].length);
44
+ return true;
45
+ }
46
+ }
47
+ return false;
48
+ };
49
+ if (this.array) {
50
+ let idx = 0;
51
+ const parse = () => {
52
+ if (!stringVal) {
53
+ return;
54
+ }
55
+ const sparseEntriesOrSpaces = stringVal.match(sparse ? /^[,\s]+/u : /^\s*,?\s*/u);
56
+ if (sparseEntriesOrSpaces) {
57
+ const ws = sparseEntriesOrSpaces[0].match(/\s/gu);
58
+ idx += sparseEntriesOrSpaces[0].length - (ws ? ws.length : 0);
59
+ stringVal = stringVal.slice(sparseEntriesOrSpaces[0].length);
60
+ if (!stringVal) {
61
+ return;
62
+ }
63
+ }
64
+ let v, beginOnly, assign;
65
+ try {
66
+ [v, stringVal, beginOnly, assign] = Types.getValueForString(
67
+ stringVal,
68
+ {
69
+ firstRun: false,
70
+ format,
71
+ state,
72
+ endMatchTypeObjs, rootHolder, parent: retObj, parentPath: idx
73
+ }
74
+ );
75
+ } catch (err) {
76
+ console.log('e', err);
77
+ return;
78
+ }
79
+ if (assign) {
80
+ retObj[idx] = v;
81
+ }
82
+ if (checkEnd(beginOnly)) {
83
+ return;
84
+ }
85
+ parse();
86
+ };
87
+ parse();
88
+ if (sparse && idx && idx > retObj.length - 1) {
89
+ retObj.length = idx + 1;
90
+ }
91
+ return {value: retObj, remnant: stringVal};
92
+ }
93
+ const parse = (notFirstRun) => {
94
+ if (!stringVal) {
95
+ return;
96
+ }
97
+ if (notFirstRun && stringVal[0] === ',') {
98
+ stringVal = stringVal.slice(1);
99
+ }
100
+ const propAndColon = stringVal.match(/^\s*(?:([^:"}]+)|"((?:[^\\"]|\\\\|\\")*)")(\s*:\s*)/u); // Todo (low): Use some identifier production
101
+ if (!propAndColon) { // End of object
102
+ return;
103
+ }
104
+ const [, prop, propHadQuotes] = propAndColon;
105
+ const pr = prop !== undefined ? prop : propHadQuotes;
106
+ stringVal = stringVal.slice(propAndColon[0].length);
107
+ let v, beginOnly, assign;
108
+ try {
109
+ [v, stringVal, beginOnly, assign] = Types.getValueForString(
110
+ stringVal,
111
+ {
112
+ firstRun: false, format, state,
113
+ endMatchTypeObjs, rootHolder, parent: retObj, parentPath: pr
114
+ }
115
+ );
116
+ } catch (err) {
117
+ // console.log(
118
+ // 'errrrr', stringVal,
119
+ // JSON.stringify(endMatchTypeObjs)
120
+ // );
121
+ return;
122
+ }
123
+ // console.log('vvv', stringVal, beginOnly, v);
124
+ if (assign) {
125
+ retObj[pr] = v;
126
+ }
127
+ if (checkEnd(beginOnly)) {
128
+ return;
129
+ }
130
+ parse(true);
131
+ };
132
+ parse();
133
+ return {value: retObj, remnant: stringVal};
134
+ },
135
+ getValue ({root, stateObj = {}, currentPath = ''}) {
136
+ const top = currentPath === '';
137
+ const arrayItems = $e(root, '.arrayItems');
138
+ const fieldsets = arrayItems.children;
139
+ const ret = root.dataset.type === 'object'
140
+ ? {}
141
+ : !this.sparse
142
+ ? []
143
+ : Array.from({length: Number.parseInt(DOM.filterChildElements(
144
+ root,
145
+ ['div', 'div', 'label', 'input']
146
+ )[0].value)});
147
+
148
+ // Todo: Should this be renamed per return arguments to
149
+ // `getRefOrVal` or is it ok?
150
+ const getValOrRef = (root, currentPathPart) => {
151
+ const value = Types.getValueForRoot(
152
+ root, stateObj, currentPath + '/' + currentPathPart
153
+ );
154
+ if (stateObj.handlingReference) {
155
+ // We deal with references later once object is fully constructed
156
+ stateObj.handlingReference = false;
157
+ return [false];
158
+ }
159
+ return [true, value];
160
+ };
161
+ [...fieldsets].forEach((fieldset) => {
162
+ const legend = fieldset.firstElementChild;
163
+ const propVal = getPropertyValueFromLegend(legend);
164
+ const root = $e(fieldset, 'div[data-type]');
165
+ /* istanbul ignore if -- Should err first? */
166
+ if (!root) {
167
+ return;
168
+ }
169
+
170
+ const objectProperty = $e(legend, 'input');
171
+ const [isVal, value] = getValOrRef(
172
+ root,
173
+ propVal
174
+ );
175
+ if (isVal) {
176
+ if (objectProperty) {
177
+ ret[propVal] = value;
178
+ } else {
179
+ ret.push(value);
180
+ }
181
+ }
182
+ });
183
+ if (top) {
184
+ Object.entries(stateObj.paths || []).forEach(
185
+ ([referencePath, {referentPath /* , expectArrayReferent */}]) => {
186
+ const referentObj = referentPath === ''
187
+ ? ret
188
+ : resolveJSONPointer({
189
+ path: referentPath,
190
+ obj: ret
191
+ });
192
+
193
+ referencePath = getJSONPointerParts(referencePath);
194
+ const referenceFinalPathPart = referencePath.pop();
195
+ const referenceParentObj = referencePath.reduce(
196
+ (obj, pathPart) => reduceJSONPointerParts(obj, pathPart),
197
+ ret
198
+ );
199
+ referenceParentObj[referenceFinalPathPart] = referentObj;
200
+ }
201
+ );
202
+ }
203
+ return ret;
204
+ },
205
+
206
+ // Try to keep in sync with basic structure of `editUI`
207
+ viewUI ({typeNamespace, type, value, topRoot, resultType, format}) {
208
+ // const {sparse} = this;
209
+ let itemIndex = -1;
210
+ const buildLegend = ({
211
+ /* className, type, arrayItems, */
212
+ itemIndex, typeNamespace, propName
213
+ }) => {
214
+ return ['legend', [
215
+ this.array
216
+ ? 'Item'
217
+ : 'Property',
218
+ ':',
219
+ nbsp.repeat(2),
220
+ ['span', {
221
+ class: `propertyName-${typeNamespace}`
222
+ }, [
223
+ propName !== undefined
224
+ ? propName
225
+ // eslint-disable-next-line max-len -- Long
226
+ /* istanbul ignore next -- Won't reach here as typeson will always give keypath? */
227
+ : itemIndex
228
+ ]]
229
+ ]];
230
+ };
231
+ const div = jml('div', {
232
+ class: 'arrayHolder',
233
+ dataset: {type},
234
+ $custom: {
235
+ $addAndSetArrayElement ({
236
+ propName, type, value, bringIntoFocus,
237
+ schemaContent, schemaState
238
+ }) {
239
+ const fieldset = this.$addArrayElement({propName});
240
+ const root = Types.getUIForModeAndType({
241
+ resultType,
242
+ readonly: true,
243
+ typeNamespace, type, topRoot,
244
+ bringIntoFocus,
245
+ format, schemaContent, schemaState,
246
+ value,
247
+ hasValue: true // type === 'sparseArrays' && value
248
+ });
249
+ jml(root, fieldset);
250
+ return root;
251
+ },
252
+ $addArrayElement ({propName}) {
253
+ itemIndex++;
254
+ const arrayItems = this.$getArrayItems();
255
+ const className = `${type}Item`;
256
+ const fieldset = jml('fieldset', [
257
+ buildLegend({
258
+ className, itemIndex, type, typeNamespace,
259
+ arrayItems, propName
260
+ })
261
+ ], arrayItems);
262
+ return fieldset;
263
+ },
264
+ $getArrayItems () {
265
+ return this.lastElementChild.lastElementChild;
266
+ }
267
+ }
268
+ }, [
269
+ DOM.initialCaps(type).replace(/s$/u, ''), nbsp.repeat(2),
270
+ ['button', {$on: {click (e) {
271
+ e.preventDefault();
272
+ const {target} = e;
273
+ const arrayContents = $e(
274
+ target.closest('.arrayHolder'),
275
+ '.arrayContents'
276
+ );
277
+ arrayContents.hidden = !arrayContents.hidden;
278
+ target.textContent = arrayContents.hidden ? '+' : '-';
279
+ }}}, ['-']],
280
+ ['div', {class: 'arrayContents'}, [
281
+ this.array
282
+ ? ['div', [
283
+ 'Array length: ',
284
+ ['span', [
285
+ (value && value.length) || 0
286
+ ]]
287
+ ]]
288
+ : '',
289
+ ['div', {
290
+ class: 'arrayItems'
291
+ }]
292
+ ]]
293
+ ]);
294
+ return [div];
295
+ },
296
+ getInput ({root}) {
297
+ // One element we are guaranteed to have for adding validation
298
+ return $e(root, 'button');
299
+ },
300
+ // Unlike other items, we don't use the `value`, as it wil always be
301
+ // an array (or object if called by that method) and we handle the
302
+ // population of the array in the callback
303
+ editUI ({
304
+ typeNamespace, buildTypeChoices, format, resultType,
305
+ type, topRoot, value, bringIntoFocus = true
306
+ }) {
307
+ const {sparse} = this;
308
+ const itemAdjust = type === 'object' ? 1 : 0;
309
+ let itemIndex = itemAdjust - 1;
310
+ const editableProperties = type !== 'array';
311
+ const bringFocus = (input, alwaysFocus) => {
312
+ if (bringIntoFocus || alwaysFocus) {
313
+ input.scrollIntoView();
314
+ input.focus();
315
+ }
316
+ };
317
+ const $swapGroup = function (holder, direction) {
318
+ const group = holder.parentElement;
319
+ const swapGroup = group[
320
+ (direction === 'up' ? 'previous' : 'next') + 'ElementSibling'
321
+ ];
322
+ /* istanbul ignore if -- Just a guard */
323
+ if (!swapGroup || swapGroup.nodeName.toLowerCase() !== 'fieldset') {
324
+ return;
325
+ }
326
+ if (!sparse) {
327
+ const swapCountElem = DOM.filterChildElements(
328
+ swapGroup, ['legend', `.${type}Item`]
329
+ )[0];
330
+ const baseCountElem = DOM.filterChildElements(group, [
331
+ 'legend', `.${type}Item`
332
+ ])[0];
333
+
334
+ const swap = swapCountElem.textContent;
335
+ const base = baseCountElem.textContent;
336
+ swapCountElem.textContent = base;
337
+ baseCountElem.textContent = swap;
338
+ } else {
339
+ const swapCountElem = group.$getPropertyInput();
340
+ const baseCountElem = swapGroup.$getPropertyInput();
341
+ if (typeof swapCountElem.$parseInt() === 'number' ||
342
+ typeof baseCountElem.$parseInt() === 'number'
343
+ ) {
344
+ const swap = swapCountElem.value;
345
+ const base = baseCountElem.value;
346
+ swapCountElem.value = base;
347
+ baseCountElem.value = swap;
348
+ }
349
+ }
350
+ /*
351
+ const baseID = group.id;
352
+ const swapID = swapGroup.id;
353
+ swapGroup.id = baseID;
354
+ group.id = swapID;
355
+ */
356
+ if (direction === 'up') {
357
+ swapGroup.before(group);
358
+ } else {
359
+ group.before(swapGroup);
360
+ }
361
+ Types.validateAllReferences({topRoot}); // Needed
362
+ this.$redrawMoveArrows();
363
+ };
364
+ const $redrawMoveArrows = function () {
365
+ DOM.filterChildElements(this, [
366
+ 'fieldset', `.${type}Item-arrowHolder-${typeNamespace}`
367
+ ]).forEach((holder, j, arr) => {
368
+ DOM.removeChildren(holder);
369
+ let up = true, down = true;
370
+ if (arr.length === 1) { // Nowhere to move
371
+ return;
372
+ }
373
+ if (j === 0) {
374
+ up = false;
375
+ } else if (j === arr.length - 1) {
376
+ down = false;
377
+ }
378
+ if (up) {
379
+ jml('button', {$on: {click: () => {
380
+ this.$swapGroup(holder, 'up');
381
+ }}}, [U.upArrow], holder);
382
+ }
383
+ if (down) {
384
+ jml('button', {$on: {click: () => {
385
+ this.$swapGroup(holder, 'down');
386
+ }}}, [U.downArrow], holder);
387
+ }
388
+ });
389
+ };
390
+ const $validateLegend = function () {
391
+ const propertyNameInputs = this.$arrayItems.$getPropertyInputs();
392
+ const invalidStr = propertyNameInputs.some((input) => {
393
+ return this !== input && input.value === this.value;
394
+ })
395
+ ? 'Duplicate property name'
396
+ : '';
397
+
398
+ // Don't validate if erring previously for other reason
399
+ if (invalidStr) {
400
+ this.setCustomValidity(
401
+ invalidStr
402
+ );
403
+ this.reportValidity();
404
+ }
405
+
406
+ // Others may be ok (or problematic) now too
407
+ propertyNameInputs.some((input, /* typeNamespace, */ _i, arr) => {
408
+ const invalidStr = arr.some((item) => {
409
+ return input !== item && input.value === item.value;
410
+ })
411
+ ? 'Duplicate property name'
412
+ : '';
413
+
414
+ if (input.validity.valid ||
415
+ // If we just found it had an invalid length, don't
416
+ // make it valid now
417
+ /* istanbul ignore next -- How to replicate? */
418
+ input.validationMessage !== 'Invalid length'
419
+ ) {
420
+ input.setCustomValidity(
421
+ invalidStr
422
+ );
423
+ // input.reportValidity(); // Might not want this as changes focus
424
+ }
425
+ return invalidStr; // Don't give a chance to become valid
426
+ });
427
+ };
428
+ const buildLegend = ({
429
+ className, splice, itemIndex, /* type, */ typeNamespace,
430
+ arrayItems, propName
431
+ }) => {
432
+ const $validateLength = async function (avoidDialog) {
433
+ if (!sparse) {
434
+ return undefined;
435
+ }
436
+ const inputsExceedingLength = arrayItems.$inputsExceedingLength();
437
+ const exceedsLength = inputsExceedingLength.length;
438
+ if (avoidDialog || !exceedsLength || !(/^\d+$/u).test(this.value)) {
439
+ return undefined;
440
+ }
441
+ await dialogs.confirm({
442
+ message: 'You are attempting to add an (integer-based) ' +
443
+ 'array item beyond the length. Click "Ok" to allow to ' +
444
+ 'permit and extend the array length or "Cancel" otherwise.'
445
+ });
446
+ const arrLengthInput = $e(arrayItems.previousElementSibling, 'input');
447
+ const highest = inputsExceedingLength.map(
448
+ (i) => Number.parseInt(i.value)
449
+ ).sort().slice(-1)[0];
450
+ arrLengthInput.value = highest + 1;
451
+ return this.$validateLength(true);
452
+ };
453
+ return editableProperties
454
+ ? ['legend', [
455
+ sparse
456
+ ? 'Item'
457
+ : {'#': [
458
+ 'Property ',
459
+ ['span', {className}, [String(itemIndex)]],
460
+ ':'
461
+ ]},
462
+ nbsp.repeat(2),
463
+ ['input', {
464
+ value: sparse
465
+ ? (
466
+ splice === 'append'
467
+ ? ''
468
+ : (propName !== undefined ? propName : itemIndex)
469
+ )
470
+ : propName || '',
471
+ dataset: {prop: true, object: true},
472
+ /*
473
+ // Works but we do want to let the user input non-integer
474
+ type: sparse ? 'number' : 'text',
475
+ step: sparse ? 1 : null,
476
+ pattern: sparse ? '\\d' : '',
477
+ */
478
+ $custom: {
479
+ async $validate () {
480
+ try {
481
+ try {
482
+ await this.$validateLength();
483
+ } catch (err) {
484
+ // Give chance for other validations if cancelled
485
+ }
486
+ // Don't give chance to validate positively if failed
487
+ /* await */ this.$validateLegend();
488
+ // Todo (low): We could make this more efficient by waiting
489
+ // until all added (when pre-populating)
490
+ Types.validateAllReferences({topRoot}); // Needed
491
+ } catch (err) {}
492
+ },
493
+ $parseInt () {
494
+ if (!(/^\d+$/u).test(this.value)) {
495
+ return false;
496
+ }
497
+ return Number.parseInt(this.value);
498
+ },
499
+ $validateLegend,
500
+ $arrayItems: arrayItems,
501
+ $validateLength,
502
+ $resort ({alwaysFocus}) {
503
+ const inputs = this.$arrayItems.$getPropertyInputs();
504
+ if (inputs.length === 1) {
505
+ return;
506
+ }
507
+ const getFieldset = (el) => el.closest('fieldset');
508
+ const thisFieldset = getFieldset(this);
509
+ const intVal = this.$parseInt();
510
+ if (intVal === false) {
511
+ inputs.reverse().some((input) => {
512
+ if (input === this) { // No need to search further
513
+ return true;
514
+ }
515
+ const intValOlder = input.$parseInt();
516
+ // Not sorting non-integers
517
+ if (typeof intValOlder !== 'number') {
518
+ return false;
519
+ }
520
+ getFieldset(input).after(thisFieldset);
521
+ bringFocus(this, alwaysFocus);
522
+ arrayItems.$redrawMoveArrows();
523
+ return true;
524
+ });
525
+ return;
526
+ }
527
+ const getNearest = (latest) => {
528
+ let nearest;
529
+ inputs.some((input) => {
530
+ if (input === this) {
531
+ return false;
532
+ }
533
+ const intValOlder = input.$parseInt();
534
+ if (typeof intValOlder !== 'number') {
535
+ if (!nearest) {
536
+ // May be no other higher ints or
537
+ // no other ints at all (but some others)
538
+ nearest = input;
539
+ }
540
+ return false;
541
+ }
542
+ const cmp = (a, b) => {
543
+ return latest ? a > b : a < b;
544
+ };
545
+ if (cmp(intVal, intValOlder) &&
546
+ (!nearest || cmp(
547
+ intValOlder, Number.parseInt(nearest.value)
548
+ ))
549
+ ) {
550
+ nearest = input;
551
+ return (intVal + (latest ? -1 : 1)) === intValOlder;
552
+ }
553
+ return false;
554
+ });
555
+ if (!nearest) {
556
+ return false;
557
+ }
558
+ const method = latest ? 'after' : 'before';
559
+ // Ensure move *after* splice that will occur after this
560
+ setTimeout(() => {
561
+ getFieldset(nearest)[method](thisFieldset);
562
+ bringFocus(this, alwaysFocus);
563
+ arrayItems.$redrawMoveArrows();
564
+ });
565
+ return true;
566
+ };
567
+ if (!getNearest()) {
568
+ getNearest(true);
569
+ }
570
+ }
571
+ },
572
+ $on: {
573
+ change (e) {
574
+ this.$validate();
575
+ // We don't want form `onchange` to run
576
+ // `$checkForKeyDuplicates` again
577
+ e.stopPropagation();
578
+ this.$resort({alwaysFocus: true});
579
+ }
580
+ },
581
+ class: `propertyName-${typeNamespace}`
582
+ }]
583
+ ]]
584
+ : ['legend', [
585
+ 'Item ',
586
+ ['span', {
587
+ dataset: {prop: true, array: true},
588
+ className
589
+ }, [
590
+ propName !== undefined ? propName : String(itemIndex)
591
+ ]]
592
+ ]];
593
+ };
594
+ const decrementItemIndex = (arrayItems) => {
595
+ if (sparse) {
596
+ itemIndex = arrayItems.$getPropertyInputs().reduce(
597
+ (highest, input) => {
598
+ const intVal = input.$parseInt();
599
+ return intVal !== false && intVal > highest ? intVal : highest;
600
+ },
601
+ -1
602
+ );
603
+ } else {
604
+ itemIndex--;
605
+ }
606
+ };
607
+ const $addArrayElement = function ({propName, splice, alwaysFocus}) {
608
+ // console.log('propName', propName);
609
+ const arrayItems = this.$getArrayItems();
610
+ if (sparse) {
611
+ if (propName) {
612
+ const intVal = propName.match(/^\d+$/u) && Number.parseInt(propName);
613
+ if (typeof intVal === 'number') {
614
+ itemIndex = intVal;
615
+ }
616
+ } else if (typeof splice === 'number') {
617
+ itemIndex = splice + 1;
618
+ } else if (typeof splice !== 'string') {
619
+ itemIndex = arrayItems.$getPropertyInputs().reduce(
620
+ (highest, input) => {
621
+ const intVal = input.$parseInt();
622
+ return intVal !== false && intVal > highest ? intVal : highest;
623
+ },
624
+ -1
625
+ ) + 1;
626
+ }
627
+ } else {
628
+ itemIndex++;
629
+ }
630
+ const thisButton = this; // eslint-disable-line consistent-this
631
+ const className = `${type}Item`;
632
+ const fieldset = jml('fieldset', {
633
+ $custom: {
634
+ $getPropertyInput () {
635
+ return DOM.filterChildElements(
636
+ this,
637
+ ['legend', `input.propertyName-${typeNamespace}`]
638
+ ).pop();
639
+ }
640
+ }
641
+ }, [
642
+ buildLegend({
643
+ className, splice, itemIndex, type, typeNamespace,
644
+ arrayItems, propName
645
+ })
646
+ ], arrayItems);
647
+ // We must ensure fieldset is built before passing it
648
+ jml({'#': [
649
+ ...buildTypeChoices({
650
+ resultType,
651
+ topRoot,
652
+ format,
653
+ state: type,
654
+ itemIndex,
655
+ typeNamespace
656
+ }),
657
+ nbsp.repeat(2),
658
+ ['button', {$on: {click (e) {
659
+ e.preventDefault();
660
+ // e.stopPropagation();
661
+ let splice;
662
+ if (sparse) {
663
+ const prevInputVal = fieldset.$getPropertyInput().$parseInt();
664
+ splice = prevInputVal === false ? 'append' : prevInputVal;
665
+ }
666
+ thisButton.$addArrayElement({
667
+ splice, alwaysFocus: true
668
+ });
669
+ const newArrayFieldset = arrayItems.lastElementChild;
670
+ fieldset.after(newArrayFieldset);
671
+ if (sparse) {
672
+ const newPrevInput = newArrayFieldset.$getPropertyInput();
673
+ bringFocus(newPrevInput, true);
674
+ } else {
675
+ DOM.filterChildElements(
676
+ arrayItems, [
677
+ 'fieldset', 'legend', '.' + className
678
+ ]
679
+ ).forEach((span, i) => {
680
+ span.textContent = Number.parseInt(i) + itemAdjust;
681
+ });
682
+ }
683
+ // Maybe not needed as addition (without renumbering)
684
+ // wouldn't yet add type
685
+ Types.validateAllReferences({topRoot});
686
+ arrayItems.$redrawMoveArrows();
687
+ }}}, ['+']],
688
+ ['button', {$on: {click (e) {
689
+ e.preventDefault();
690
+ // e.stopPropagation();
691
+ fieldset.remove();
692
+ decrementItemIndex(arrayItems);
693
+ if (!sparse) {
694
+ DOM.filterChildElements(arrayItems, [
695
+ 'fieldset', 'legend', '.' + className
696
+ ]).forEach((span, i) => {
697
+ span.textContent = Number.parseInt(i) + itemAdjust;
698
+ });
699
+ }
700
+ // Maybe not needed as removal would remove circular
701
+ Types.validateAllReferences({topRoot});
702
+ arrayItems.$redrawMoveArrows();
703
+ }}}, ['x']],
704
+ ['span', {class: `${type}Item-arrowHolder-${typeNamespace}`}, []]
705
+ ]}, fieldset);
706
+ // Need to validate if adding more than one property (in
707
+ // case two have empty string)
708
+ if (editableProperties) {
709
+ const pns = arrayItems.$getPropertyInputs();
710
+ /* istanbul ignore else -- Just a guard */
711
+ if (pns.length) {
712
+ const input = pns.pop();
713
+ input.$validate();
714
+ input.$resort({alwaysFocus});
715
+ bringFocus(input, alwaysFocus);
716
+ }
717
+ }
718
+ arrayItems.$redrawMoveArrows();
719
+ };
720
+
721
+ const $getArrayLength = function () {
722
+ return $e(this.previousElementSibling, 'input').value;
723
+ };
724
+ const $inputsExceedingLength = function () {
725
+ const highestExpectedIndex = this.$getArrayLength() - 1;
726
+ return this.$getPropertyInputs().filter(
727
+ (input) => input.value.match(/^\d+$/u)
728
+ ).filter((input) => {
729
+ // We cycle through all elements to set the proper validity on them
730
+ const val = Number.parseInt(input.value);
731
+ const exceedsLength = val > highestExpectedIndex;
732
+ input.setCustomValidity(
733
+ exceedsLength
734
+ ? 'Invalid length'
735
+ : ''
736
+ );
737
+ input.reportValidity(); // Is this having an effect now?
738
+ return exceedsLength;
739
+ });
740
+ };
741
+ const div = jml('div', {
742
+ dataset: {type},
743
+ // is: 'array-or-object-editor',
744
+ $custom: {
745
+ $addAndSetArrayElement ({
746
+ propName, type, value, bringIntoFocus
747
+ // , schemaContent, schemaState
748
+ }) {
749
+ this.$addArrayElement({propName});
750
+ const typeChoices = this.$getTypeChoices();
751
+ typeChoices.$setType({type, baseValue: value, bringIntoFocus});
752
+ const root = typeChoices.$getTypeRoot();
753
+ const typeObj = Types.availableTypes[type];
754
+ if (typeObj.setValue) typeObj.setValue({root, value});
755
+ Types.validate({type, root, topRoot});
756
+ return root;
757
+ },
758
+ $getAddArrayElement () {
759
+ const el = this
760
+ .lastElementChild.firstElementChild.nextElementSibling;
761
+ return sparse ? el.nextElementSibling : el;
762
+ },
763
+ $addArrayElement ({propName, splice, alwaysFocus}) {
764
+ const addArrayElement = this.$getAddArrayElement();
765
+ addArrayElement.$addArrayElement({propName, splice, alwaysFocus});
766
+ },
767
+ $getArrayItems () {
768
+ const addArrayElement = this.$getAddArrayElement();
769
+ return addArrayElement.previousElementSibling;
770
+ },
771
+ $getTypeChoices () {
772
+ const arrayItems = this.$getArrayItems();
773
+ return $e(
774
+ arrayItems.lastElementChild,
775
+ `.typeChoices-${typeNamespace}`
776
+ );
777
+ }
778
+ },
779
+ $on: {click (e) {
780
+ // We needed to stop preventing the default for the
781
+ // invalid date checkbox; is this sufficient to prevent
782
+ // other stray clicks apparently meant for the array
783
+ // and object reference checking?
784
+ if (e.target.type !== 'checkbox' && e.target.type !== 'radio') {
785
+ e.preventDefault();
786
+ }
787
+ }}
788
+ }, [
789
+ DOM.initialCaps(type).replace(/s$/u, ''), nbsp.repeat(2),
790
+ ['button', {$on: {click (e) {
791
+ e.preventDefault();
792
+ const {target} = e;
793
+ const arrayContents = $e(div, '.arrayContents');
794
+ arrayContents.hidden = !arrayContents.hidden;
795
+ target.textContent = arrayContents.hidden ? '+' : '-';
796
+ }}}, ['-']],
797
+ ['div', {class: 'arrayContents'}, [
798
+ sparse
799
+ ? ['div', [
800
+ ['label', [
801
+ 'Array length: ',
802
+ ['input', {
803
+ type: 'number',
804
+ value: (value && value.length) || 0,
805
+ step: 1,
806
+ size: 4,
807
+ pattern: '\\d',
808
+ $on: {
809
+ async change () {
810
+ const arrayItems = $e(
811
+ this.closest('.arrayContents'), '.arrayItems'
812
+ );
813
+ const propInputsBeyondLength =
814
+ arrayItems.$inputsExceedingLength();
815
+ try {
816
+ if (propInputsBeyondLength.length) {
817
+ await dialogs.confirm({
818
+ message: 'Your new length will truncate the ' +
819
+ 'array causing items to be removed. Continue?'
820
+ });
821
+ propInputsBeyondLength.forEach((input) => {
822
+ const fieldset = input.closest('fieldset');
823
+ fieldset.remove();
824
+ });
825
+ // Maybe not needed as removal would remove circular
826
+ Types.validateAllReferences({topRoot});
827
+ }
828
+ this.$oldvalue = this.value;
829
+ } catch (err) {
830
+ this.value = this.$oldvalue; // Revert
831
+ }
832
+ }
833
+ }
834
+ }]
835
+ ]]
836
+ ]]
837
+ : '',
838
+ ['div', {
839
+ class: 'arrayItems',
840
+ $custom: {
841
+ $swapGroup, $redrawMoveArrows, $getArrayLength,
842
+ $inputsExceedingLength,
843
+ $getPropertyInputs () {
844
+ return DOM.filterChildElements(
845
+ this,
846
+ ['fieldset', 'legend', `input.propertyName-${typeNamespace}`]
847
+ );
848
+ }
849
+ }
850
+ }],
851
+ ['button', {
852
+ class: 'addArrayElement',
853
+ // is: 'add-array-element',
854
+ $custom: {$addArrayElement, $getArrayItems () {
855
+ return this.previousElementSibling;
856
+ }},
857
+ $on: {click () {
858
+ this.$addArrayElement({alwaysFocus: true});
859
+ // Maybe not needed as addition (without renumbering)
860
+ // wouldn't yet add type
861
+ Types.validateAllReferences({topRoot});
862
+ }}
863
+ }, ['+ Item']],
864
+ ['button', {$on: {click () {
865
+ const arrayContents = this.closest('.arrayContents');
866
+ const arrayItems = $e(arrayContents, '.arrayItems');
867
+ const lastElement = arrayItems.lastElementChild;
868
+ if (lastElement) {
869
+ lastElement.remove();
870
+ decrementItemIndex(arrayItems);
871
+ $e(arrayContents, '.arrayItems').$redrawMoveArrows();
872
+ }
873
+ // Maybe not needed as removal would remove circular
874
+ Types.validateAllReferences({topRoot});
875
+ }}}, ['- Last item']],
876
+ // We could only add this when there was more than one
877
+ ['button', {$on: {click () {
878
+ const arrayItems = $e(
879
+ this.closest('.arrayContents'),
880
+ '.arrayItems'
881
+ );
882
+ DOM.removeChildren(arrayItems);
883
+ if (sparse) {
884
+ decrementItemIndex(arrayItems);
885
+ } else {
886
+ itemIndex = -1;
887
+ }
888
+ // Maybe not needed as removal would remove circular
889
+ Types.validateAllReferences({topRoot});
890
+ }}}, ['x All']]
891
+ ]]
892
+ ]);
893
+ topRoot = topRoot || div;
894
+ return [div];
895
+ }
896
+ };
897
+
898
+ export default arrayType;