@es-joy/jsoe 0.0.2 → 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.
@@ -1,125 +1,249 @@
1
+ import Formats, {getControlsForFormatAndValue} from './formats.js';
2
+ import Types from './types.js';
1
3
  import {jml} from '../vendor/jamilih/dist/jml-es.js';
2
- import Formats from './formats.js';
4
+
3
5
  import {$e, DOM} from './utils/templateUtils.js';
6
+ import dialogs from './utils/dialogs.js';
4
7
 
5
8
  /**
6
- * @todo Compose from format metadata, so can make user customizable.
7
- * @param {object} cfg
8
- * @param {string} cfg.schema
9
- * @param {boolean} cfg.hasKeyPath
10
- * @returns {JamilihArray[]}
9
+ * An arbitrary Structured Clone, JSON, etc. value.
10
+ * @typedef {any} StructuredCloneValue
11
11
  */
12
- export const getFormatAndSchemaChoices = ({schema, hasKeyPath}) => {
13
- const hasSchema = typeof schema === 'string';
14
- return [
15
- ['JSON only', {value: 'json'}],
16
- ...(hasKeyPath
17
- ? []
18
- : [['IndexedDB key', {value: 'indexedDBKey'}]]),
19
- ['Structured Clone (via Typeson JSON)', {
20
- value: 'structuredCloning', selected: !hasSchema
21
- }]
22
- /* schema:
23
- ...(hasSchema
24
- ? [
25
- [`Schema + arbitrary: ${schema}`, {
26
- value: 'schemaAndArbitrary',
27
- dataset: {schema}
28
- }],
29
- [`Schema only: ${schema}`, {
30
- value: 'schemaOnly',
31
- dataset: {schema},
32
- selected: hasSchema
33
- }]
34
- ]
35
- : []
36
- )
37
- */
38
- /*
39
- // This can be supported for editing only
40
- ['Arbitrary (Non-Typeson-serializable will be read-only)', {
41
- value: 'arbitrary',
42
- title: 'Any value that the typeson-registry supports ' +
43
- 'for structured cloning'
44
- }]
45
- */
46
- ].map(([optText, optAtts]) => {
47
- return ['option', optAtts, [optText]];
48
- });
49
- };
50
12
 
51
13
  /**
52
- * Builds a selector and container for types.
14
+ * @callback BuildTypeChoices
53
15
  * @param {object} cfg
54
- * @param {string} cfg.schema The schema name
55
- * @param {object} cfg.schemaContent The schema content
56
- * @param {boolean} cfg.hasValue If false and `hasKeyPath` is `true`,
57
- * will initialize with an object
58
- * @param {boolean} cfg.singleValue
59
- * @param {boolean} cfg.hasKeyPath
16
+ * @param {string} cfg.format
60
17
  * @param {string} cfg.typeNamespace
61
- * @returns {{
62
- * mainTypeChoices: HTMLSelectElement,
63
- * typesHolder: HTMLDivElement
64
- * }} The selector for types and the container for them
18
+ * @param {StructuredCloneValue} cfg.value
19
+ * @param {boolean} [cfg.setValue=false]
20
+ * @param {string} cfg.state
21
+ * @param {string} cfg.keySelectClass
22
+ * @param {boolean} cfg.requireObject
23
+ * @param {boolean} cfg.objectHasValue
24
+ * @param {RootElement} cfg.topRoot Always a `div` element?
25
+ * @param {string} cfg.schema Schema name
26
+ * @param {string} cfg.schemaContent Schema contents
27
+ * @returns {[select: Element, typeContainer: Element]}
28
+ */
29
+
30
+ /**
31
+ * @type {BuildTypeChoices}
65
32
  */
66
- function typeChoices ({
33
+ export const buildTypeChoices = ({
34
+ format,
35
+ typeNamespace,
36
+ value,
37
+ setValue = false,
38
+ state,
39
+ // itemIndex = 0,
40
+ keySelectClass,
41
+ requireObject,
42
+ objectHasValue,
43
+ topRoot,
67
44
  schema,
68
- schemaContent,
69
- hasValue,
70
- singleValue,
71
- hasKeyPath,
72
- typeNamespace
73
- }) {
74
- const mainTypeChoices = jml('select', {
75
- class: 'mainTypeChoices',
76
- hidden: singleValue,
77
- // is: 'main-type-choices',
45
+ schemaContent
46
+ }) => {
47
+ // console.log('format', format, 'state', state, 'path', typeNamespace);
48
+ const typeOptions = requireObject
49
+ ? [Types.getOptionForType('object')]
50
+ : Types.getTypeOptionsForFormatAndState(format, state);
51
+
52
+ let editUI;
53
+ const sel = jml('select', {
54
+ hidden: requireObject,
55
+ class: `typeChoices-${typeNamespace}${keySelectClass
56
+ ? ' ' + keySelectClass
57
+ : ''
58
+ }`,
59
+ // is: 'type-choices',
78
60
  $custom: {
79
- $setFormat (valueFormat) {
80
- this.value = valueFormat;
81
- this.$buildTypeChoices();
61
+ $setType ({type, baseValue, bringIntoFocus}) {
62
+ this.value = type;
63
+ this.$setStyles();
64
+ this.$addAndValidateEditUI({baseValue, bringIntoFocus});
65
+ },
66
+ $setTypeNoEditUI ({type}) {
67
+ this.value = type;
68
+ this.$setStyles();
69
+ },
70
+ $setStyles () {
71
+ const {value: type} = this;
72
+ this.dataset.type = type; // Used for styling
73
+ const parEl = this.parentElement;
74
+ if (parEl.nodeName.toLowerCase() === 'fieldset') {
75
+ parEl.dataset.type = type;
76
+ DOM.filterChildElements(parEl, 'legend').forEach((legend) => {
77
+ legend.dataset.type = type;
78
+ });
79
+ }
80
+ },
81
+ $getTypeRoot () {
82
+ const container = this.$getContainer();
83
+ /* istanbul ignore if -- How to replicate? */
84
+ if (!container) {
85
+ return false;
86
+ }
87
+ return $e(container, 'div[data-type]');
82
88
  },
83
- $buildTypeChoices () {
84
- const typesHolder = this.nextElementSibling;
85
- DOM.removeChildren(typesHolder);
86
- jml({'#': Formats.buildTypeChoices({
87
- topRoot: $e(typesHolder, 'div[data-type]'),
88
- resultType: 'both',
89
- format: this.value,
89
+ $addAndValidateEditUI ({baseValue, bringIntoFocus} = {}) {
90
+ const {value: type} = this;
91
+
92
+ if (!type) { return; }
93
+ let topRoot = this.$getTopRoot();
94
+
95
+ // Todo (low): Try to avoid need for `baseValue`
96
+ // (needed by arrayNonindexKeys for setting an array
97
+ // length and avoiding errors); could set all
98
+ // values through here?
99
+ editUI = Types.getUIForModeAndType({
100
+ readonly: false,
90
101
  typeNamespace,
91
- requireObject: hasKeyPath,
92
- objectHasValue: hasValue,
93
- schema,
94
- schemaContent
95
- })}, typesHolder);
102
+ type,
103
+ bringIntoFocus,
104
+ hasValue: type === 'arrayNonindexKeys' && baseValue,
105
+ value: baseValue,
106
+ buildTypeChoices,
107
+ format,
108
+ topRoot
109
+ });
110
+ this.$addEditUI({editUI});
111
+ this.$validate();
112
+ topRoot = this.$getTopRoot(); // May be existing now
113
+ // Needed; Array/object ref somewhere could now be valid or invalid
114
+ Types.validateAllReferences({topRoot});
115
+ },
116
+ $addTypeAndEditUI ({type, editUI}) {
117
+ this.$setTypeNoEditUI({type});
118
+ this.$addEditUI({editUI});
119
+ },
120
+ $addEditUI ({editUI}) {
121
+ const container = this.$getContainer();
122
+ DOM.removeChildren(container);
123
+ jml(editUI, container);
124
+ },
125
+ $getContainer () {
126
+ return this.nextElementSibling;
127
+ },
128
+ $getTopRoot () {
129
+ return topRoot || this.$getTypeRoot();
130
+ },
131
+ $validate () {
132
+ const {value: type} = this;
133
+ const container = this.$getContainer();
134
+ if (!container.firstElementChild) {
135
+ return false;
136
+ }
137
+ const editUI = container.firstElementChild;
138
+ return Types.validate({
139
+ type, root: editUI, topRoot: this.$getTopRoot()
140
+ });
96
141
  }
97
142
  },
98
- $on: {change () {
99
- this.$buildTypeChoices();
143
+ $on: {change (e) {
144
+ // We don't want form `onchange` to run `$checkForKeyDuplicates`
145
+ // again (through `addAndValidateEditUI`->`validateAllReferences`)
146
+ e.stopPropagation();
147
+ this.$addAndValidateEditUI();
148
+ this.$setStyles();
100
149
  }}
101
- }, getFormatAndSchemaChoices({schema, hasKeyPath}));
102
- const typesHolder = jml('div', {class: 'typesHolder', $custom: {
103
- $getTypeRoot () {
104
- return $e(this, 'div[data-type]');
150
+ }, [
151
+ ['option', {value: ''}, [
152
+ '(Choose a type)'
153
+ ]],
154
+ ...typeOptions.map(
155
+ ([optText, optAtts]) => [
156
+ 'option',
157
+ optAtts ||
158
+ /* istanbul ignore next -- Should always have atts */
159
+ {},
160
+ [optText]
161
+ ]
162
+ )
163
+ ]);
164
+ if (setValue || (requireObject && !objectHasValue)) {
165
+ setTimeout(async () => {
166
+ if (!setValue) { // if (requireObject && !objectHasValue) {
167
+ // Todo (low): We could auto-populate keypath if has
168
+ // keypath (and we probably also only want if
169
+ // not autoincrement)
170
+ value = {};
171
+ }
172
+ try {
173
+ const rootEditUI = await Formats.availableFormats[format].iterate(
174
+ value,
175
+ {
176
+ readonly: false,
177
+ typeNamespace,
178
+ schema,
179
+ schemaContent
180
+ }
181
+ );
182
+ const type = Types.getTypeForRoot(rootEditUI);
183
+ sel.$addTypeAndEditUI({type, editUI: rootEditUI});
184
+ } catch (err) {
185
+ /* istanbul ignore next -- At least some errors handled earlier */
186
+ dialogs.alert({
187
+ message: 'The object to be added had types not supported ' +
188
+ 'by the current format.'
189
+ });
190
+ /* istanbul ignore next -- How to trigger? */
191
+ console.log('err', err);
192
+ }
193
+ });
194
+ }
195
+
196
+ const typeContainer = jml('div', {class: 'typeContainer'});
197
+
198
+ return {
199
+ domArray: [
200
+ sel,
201
+ typeContainer
202
+ ],
203
+
204
+ /**
205
+ * @param {import('./types.js').StateObject} [stateObj] Will
206
+ * auto-set `typeNamespace` and `format`
207
+ * @param {string} [currentPath]
208
+ * @returns {StructuredCloneValue}
209
+ */
210
+ getValue (stateObj, currentPath) {
211
+ const root = $e(typeContainer, 'div[data-type]');
212
+ return Types.getValueForRoot(root, {
213
+ typeNamespace,
214
+ format,
215
+ ...stateObj
216
+ }, currentPath);
105
217
  },
106
- $getTypeSelect () {
107
- return $e(this, `.typeChoices-${typeNamespace}`);
108
- }
109
- }});
110
218
 
111
- jml({'#': Formats.buildTypeChoices({
112
- resultType: 'both',
113
- topRoot: $e(typesHolder, 'div[data-type]'),
114
- format: 'structuredCloning',
115
- typeNamespace,
116
- requireObject: hasKeyPath,
117
- objectHasValue: hasValue,
118
- schema,
119
- schemaContent
120
- })}, typesHolder);
219
+ /**
220
+ * @returns {string|undefined}
221
+ */
222
+ getType () {
223
+ const root = $e(typeContainer, 'div[data-type]');
224
+ return Types.getTypeForRoot(root);
225
+ },
121
226
 
122
- return {mainTypeChoices, typesHolder};
123
- }
227
+ /**
228
+ * @returns {boolean}
229
+ */
230
+ validValuesSet () {
231
+ const root = $e(typeContainer, 'div[data-type]');
232
+ const form = root.closest('form');
233
+ return Types.validValuesSet({form, typeNamespace});
234
+ },
124
235
 
125
- export default typeChoices;
236
+ /**
237
+ * @param {StructuredCloneValue} value
238
+ * @param {import('./types.js').StateObject} stateObj
239
+ * @returns {Promise<void>}
240
+ */
241
+ async setValue (value, stateObj) {
242
+ const rootEditUI = await getControlsForFormatAndValue(
243
+ format, value, stateObj
244
+ );
245
+ const type = Types.getTypeForRoot(rootEditUI);
246
+ sel.$addTypeAndEditUI({type, editUI: rootEditUI});
247
+ }
248
+ };
249
+ };
package/src/types.js CHANGED
@@ -29,6 +29,7 @@ import InfinitiesSuperType from './superTypes/InfinitiesSuperType.js';
29
29
  import SpecialNumberSuperType from './superTypes/SpecialNumberSuperType.js';
30
30
 
31
31
  /**
32
+ * Utility to retrieve the property value given a legend element.
32
33
  * @param {HTMLLegendElement} legend
33
34
  * @returns {string}
34
35
  */
@@ -54,42 +55,34 @@ const Types = {};
54
55
  * present, use `stringRegex`
55
56
  * @property {RegExp} [stringRegexEnd] Used for string parsing. If not
56
57
  * present, use `stringRegex`
57
- * @property {
58
- * (ArbitraryValue) => boolean
59
- * } [valueMatch] Function to check whether this subtype matches
58
+ * @property {(ArbitraryValue) => boolean} [valueMatch] Function to
59
+ * check whether this subtype matches
60
60
  * @property {string} [superType] The greater fundamental type to which
61
61
  * the type belongs
62
62
  * @property {(s: string) => ArbitraryValue} toValue Converts from
63
63
  * string to value. May use `stringRegex` to find components.
64
- * @property {
65
- * (info: {root?: HTMLDivElement}) => ArbitraryValue
64
+ * @property {(info: {root?: HTMLDivElement}) =>
65
+ * ArbitraryValue
66
66
  * } getValue Gets the value for the type
67
- * @property {
68
- * (info: {root?: HTMLDivElement}) => void
69
- * } [setValue] Should set the value of the form's `getInput` element
70
- * @property {
71
- * (info: {
72
- * value?: ArbitraryValue,
73
- * typeNamespace?: string,
74
- * type?: string,
75
- * topRoot?: HTMLDivElement,
76
- * resultType?: "keys"|"values"|"both",
77
- * format?: string
78
- * }) => JamilihArray
79
- * } viewUI
80
- * @property {
81
- * (info: {
82
- * value?: ArbitraryValue,
83
- * typeNamespace?: string,
84
- * }) => JamilihArray
85
- * } editUI
86
- * @property {
87
- * (info: {root: HTMLDivElement}) =>
88
- * HTMLInputElement|HTMLTextareaElement|HTMLSelectElement
89
- * } getInput Gets the form control (with `value`)
90
- * @property {
91
- * (path: string, value: ArbitraryValue) => ArbitraryValue
92
- * } [resolveReference] Gets the reference. For array and object
67
+ * @property {(info: {root?: HTMLDivElement}) => void} [setValue] Should set
68
+ * the value of the form's `getInput` element
69
+ * @property {(info: {
70
+ * value?: ArbitraryValue,
71
+ * typeNamespace?: string,
72
+ * type?: string,
73
+ * topRoot?: HTMLDivElement,
74
+ * resultType?: "keys"|"values"|"both",
75
+ * format?: string
76
+ * }) => JamilihArray} viewUI
77
+ * @property {(info: {
78
+ * value?: ArbitraryValue,
79
+ * typeNamespace?: string,
80
+ * }) => JamilihArray} editUI
81
+ * @property {(info: {root: HTMLDivElement}) =>
82
+ * HTMLInputElement|HTMLTextareaElement|HTMLSelectElement} getInput Gets the
83
+ * form control (with `value`)
84
+ * @property {(path: string, value: ArbitraryValue) =>
85
+ * ArbitraryValue} [resolveReference] Gets the reference. For array and object
93
86
  * references types only
94
87
  * @property {(info: {root: HTMLDivElement, topRoot?: HTMLDivElement}) => {
95
88
  * message: string,
@@ -240,28 +233,52 @@ copyTypeObjs([
240
233
  ]);
241
234
 
242
235
  /**
236
+ * Utility to retrieve the type out of a type root element.
243
237
  * @public
244
- * @param {RootElement} root
245
- * @returns {string|boolean} Why would it not exist?
238
+ * @param {?RootElement} root
239
+ * @returns {string|undefined} Why would it not exist?
246
240
  */
247
241
  Types.getTypeForRoot = (root) => {
248
242
  return root && root.dataset.type;
249
243
  };
250
244
 
251
245
  /**
246
+ * @typedef {{
247
+ * typeNamespace: string,
248
+ * "readonly": boolean,
249
+ * format: string,
250
+ * error: Error,
251
+ * rootUI: Element,
252
+ * schemaContent: string,
253
+ * getPossibleSchemasForPathAndType: (
254
+ * keypath: string,
255
+ * parentPath: string,
256
+ * arrayOrObjectPropertyName: string,
257
+ * valueType: string
258
+ * ) => StateObject
259
+ * }} StateObject
260
+ */
261
+
262
+ /**
263
+ * Utility to get the value out of a type root element with a given
264
+ * state and path.
252
265
  * @public
253
266
  * @param {RootElement} root
254
267
  * @param {StateObject} stateObj
255
- * @param {string} currentPath
268
+ * @param {string} [currentPath]
256
269
  * @returns {StructuredCloneValue}
257
270
  */
258
271
  Types.getValueForRoot = (root, stateObj, currentPath) => {
259
- return Types.availableTypes[Types.getTypeForRoot(root)].getValue({
272
+ const typeObject = /** @type {TypeObject} */ (
273
+ Types.availableTypes[Types.getTypeForRoot(root)]
274
+ );
275
+ return typeObject.getValue({
260
276
  root, stateObj, currentPath
261
277
  });
262
278
  };
263
279
 
264
280
  /**
281
+ * Utility to get the form control (e.g., input element) for a root.
265
282
  * @public
266
283
  * @param {RootElement} root
267
284
  * @returns {null|HTMLInputElement}
@@ -276,6 +293,7 @@ Types.getFormControlForRoot = (root) => {
276
293
  };
277
294
 
278
295
  /**
296
+ * Utility to get the value for a root using its ancestor and state.
279
297
  * @public
280
298
  * @param {string|Element} selOrEl
281
299
  * @param {StateObject} stateObj