@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.
- package/.editorconfig +15 -0
- package/.eslintignore +7 -0
- package/.eslintrc.cjs +120 -0
- package/CHANGES.md +5 -0
- package/LICENSE-MIT.txt +22 -0
- package/README.md +82 -0
- package/package.json +101 -0
- package/rollup.config.js +29 -0
- package/server.js +25 -0
- package/src/deepEqual.js +72 -0
- package/src/formats/indexedDBKey.js +66 -0
- package/src/formats/json.js +64 -0
- package/src/formats/schemaAndArbitrary.js +20 -0
- package/src/formats/schemaOnly.js +529 -0
- package/src/formats/structuredCloning.js +372 -0
- package/src/formats.js +229 -0
- package/src/fundamentalTypes/BooleanObjectType.js +55 -0
- package/src/fundamentalTypes/NumberObjectType.js +41 -0
- package/src/fundamentalTypes/StringObjectType.js +37 -0
- package/src/fundamentalTypes/arrayReferenceType.js +203 -0
- package/src/fundamentalTypes/arrayType.js +898 -0
- package/src/fundamentalTypes/bigintType.js +53 -0
- package/src/fundamentalTypes/dateType.js +129 -0
- package/src/fundamentalTypes/nullType.js +33 -0
- package/src/fundamentalTypes/numberType.js +56 -0
- package/src/fundamentalTypes/objectReferenceType.js +51 -0
- package/src/fundamentalTypes/objectType.js +30 -0
- package/src/fundamentalTypes/regexpType.js +95 -0
- package/src/fundamentalTypes/sparseUndefinedType.js +43 -0
- package/src/fundamentalTypes/stringType.js +31 -0
- package/src/fundamentalTypes/undefinedType.js +37 -0
- package/src/index.js +7 -0
- package/src/subTypes/blobHTMLType.js +160 -0
- package/src/subTypes/falseType.js +42 -0
- package/src/subTypes/trueType.js +42 -0
- package/src/superTypes/InfinitiesSuperType.js +49 -0
- package/src/superTypes/SpecialNumberSuperType.js +62 -0
- package/src/typeChoices.js +125 -0
- package/src/types.js +665 -0
- package/src/utils/dialogs.js +115 -0
- package/src/utils/jsonPointer.js +89 -0
- package/src/utils/templateUtils.js +106 -0
- 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;
|