@es-joy/jsoe 0.1.0 → 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,137 +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
- * Defaults to structured cloning.
7
- * @todo Compose from format metadata, so can make user customizable.
8
- * @param {object} cfg
9
- * @param {string} [cfg.schema] (NOT IN USE)
10
- * @param {boolean} [cfg.hasKeyPath] Whether or not a key path is expected; if
11
- * true, an indexedDB key is not allowed here as a key does not support
12
- * the object type which is needed for a key path.
13
- * @returns {DocumentFragment}
9
+ * An arbitrary Structured Clone, JSON, etc. value.
10
+ * @typedef {any} StructuredCloneValue
14
11
  */
15
- export const getFormatAndSchemaChoices = ({schema, hasKeyPath} = {}) => {
16
- const hasSchema = typeof schema === 'string';
17
- return [
18
- ['JSON only', {value: 'json'}],
19
- ...(hasKeyPath
20
- ? []
21
- : [['IndexedDB key', {value: 'indexedDBKey'}]]),
22
- ['Structured Clone (via Typeson JSON)', {
23
- value: 'structuredCloning', selected: !hasSchema
24
- }]
25
- /* schema:
26
- ...(hasSchema
27
- ? [
28
- [`Schema + arbitrary: ${schema}`, {
29
- value: 'schemaAndArbitrary',
30
- dataset: {schema}
31
- }],
32
- [`Schema only: ${schema}`, {
33
- value: 'schemaOnly',
34
- dataset: {schema},
35
- selected: hasSchema
36
- }]
37
- ]
38
- : []
39
- )
40
- */
41
- /*
42
- // This can be supported for editing only
43
- ['Arbitrary (Non-Typeson-serializable will be read-only)', {
44
- value: 'arbitrary',
45
- title: 'Any value that the typeson-registry supports ' +
46
- 'for structured cloning'
47
- }]
48
- */
49
- ].map(([optText, optAtts]) => {
50
- return jml('option', optAtts, [optText]);
51
- }).reduce((frag, option) => {
52
- frag.append(option);
53
- return frag;
54
- }, document.createDocumentFragment());
55
- };
56
12
 
57
13
  /**
58
- * Builds a selector and container for types.
14
+ * @callback BuildTypeChoices
59
15
  * @param {object} cfg
60
- * @param {string} [cfg.schema] The schema name (NOT IN USE)
61
- * @param {object} [cfg.schemaContent] The schema content (NOT IN USE)
62
- * @param {boolean} [cfg.hasValue] Set to `true` if you are supplying
63
- * your own value. If `false` and `hasKeyPath` is `true`,
64
- * will initialize with an object.
65
- * @param {boolean} [cfg.singleValue] (NOT IN USE)
66
- * @param {boolean} [cfg.hasKeyPath] If this is set (because there is a keyPath
67
- * to be found within the object) and `hasValue` is true, an object type
68
- * will be set and required at the root level. This option will also
69
- * prevent selection of indexedDB key at root (since a key cannot be a
70
- * plain object).
71
- * @param {string} [cfg.typeNamespace] Used to prevent conflicts with other
72
- * instances of typeChoices on the page
73
- * @returns {[
74
- * mainTypeChoices: HTMLSelectElement,
75
- * typesHolder: HTMLDivElement
76
- * ]} The selector for types and the container for them. Both should be
77
- * added to the page.
16
+ * @param {string} cfg.format
17
+ * @param {string} cfg.typeNamespace
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}
78
32
  */
79
- 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,
80
44
  schema,
81
- schemaContent,
82
- hasValue,
83
- singleValue,
84
- hasKeyPath,
85
- typeNamespace
86
- }) {
87
- const mainTypeChoices = jml('select', {
88
- class: 'mainTypeChoices',
89
- hidden: singleValue,
90
- // 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',
91
60
  $custom: {
92
- $setFormat (valueFormat) {
93
- this.value = valueFormat;
94
- 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]');
95
88
  },
96
- $buildTypeChoices () {
97
- DOM.removeChildren(typesHolder);
98
- jml({'#': Formats.buildTypeChoices({
99
- topRoot: $e(typesHolder, 'div[data-type]'),
100
- resultType: 'both',
101
- 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,
102
101
  typeNamespace,
103
- requireObject: hasKeyPath,
104
- objectHasValue: hasValue,
105
- schema,
106
- schemaContent
107
- })}, 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
+ });
108
141
  }
109
142
  },
110
- $on: {change () {
111
- 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();
112
149
  }}
113
- }, [getFormatAndSchemaChoices({schema, hasKeyPath})]);
114
- const typesHolder = jml('div', {class: 'typesHolder', $custom: {
115
- $getTypeRoot () {
116
- 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);
117
217
  },
118
- $getTypeSelect () {
119
- return $e(this, `.typeChoices-${typeNamespace}`);
120
- }
121
- }});
122
218
 
123
- jml({'#': Formats.buildTypeChoices({
124
- resultType: 'both',
125
- topRoot: $e(typesHolder, 'div[data-type]'),
126
- format: 'structuredCloning',
127
- typeNamespace,
128
- requireObject: hasKeyPath,
129
- objectHasValue: hasValue,
130
- schema,
131
- schemaContent
132
- })}, typesHolder);
219
+ /**
220
+ * @returns {string|undefined}
221
+ */
222
+ getType () {
223
+ const root = $e(typeContainer, 'div[data-type]');
224
+ return Types.getTypeForRoot(root);
225
+ },
133
226
 
134
- return [mainTypeChoices, typesHolder];
135
- }
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
+ },
136
235
 
137
- 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