@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.
package/.eslintrc.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  module.exports = {
4
4
  extends: ['ash-nazg/sauron-node-overrides', 'plugin:cypress/recommended'],
5
5
  parserOptions: {
6
- ecmaVersion: 2021
6
+ ecmaVersion: 2022
7
7
  },
8
8
  settings: {
9
9
  jsdoc: {
@@ -24,6 +24,7 @@ module.exports = {
24
24
  'console',
25
25
  'Date.toISOString',
26
26
  'document.body',
27
+ 'document.createDocumentFragment',
27
28
  'document.querySelector',
28
29
  'document.querySelectorAll',
29
30
  'Error',
package/CHANGES.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # CHANGES TO `@es-joy/jsoe`
2
2
 
3
+ ## 0.2.0
4
+
5
+ - refactor(BREAKING): `typeChoices`->`formatAndTypeChoices`;
6
+ add different `typeChoices`, adding methods to both
7
+ - feat: add `getControlsForFormatAndValue` utility
8
+ - fix: ensure `setValue` removes existing content
9
+ - feat: add stylesheet
10
+
11
+ ## 0.1.0
12
+
13
+ - refactor: have `getFormatAndSchemaChoices` return fragment of options
14
+ - refactor: avoid placement dependency for return results of `typeChoices`
15
+ - refactor: return array of return objects for easier direct embedding
16
+ in Jamilih
17
+ - docs: better API docs
18
+ - docs: add demo
19
+ - chore: switch port for testing
20
+
21
+ ## 0.0.2
22
+
23
+ - fix: `node_modules` paths
24
+
3
25
  ## 0.0.1
4
26
 
5
27
  - initial commit
package/README.md CHANGED
@@ -4,7 +4,8 @@ JavaScript Object Editor.
4
4
 
5
5
  Editing of arbitrary JavaScript objects.
6
6
 
7
- See a [demo](https://brettz9.github.io/idb-manager/index-pages.html) of an
7
+ See a [demo](https://es-joy.github.io/jsoe/demo/) of this tool or
8
+ [a demo](https://brettz9.github.io/idb-manager/index-pages.html) of an
8
9
  app using this tool.
9
10
 
10
11
  ## Formats
@@ -52,7 +53,6 @@ separate enumeration.
52
53
 
53
54
  ## To-dos
54
55
 
55
- 1. Create demo
56
56
  1. Add tests of demo (lower priority as have tests in `idb-manager`)
57
57
  1. Support Schemas based on a JSON serialization of Zod
58
58
  1. Expand fundamental types
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>JSOE Demo</title>
6
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
7
+ <link rel="stylesheet" href="../src/jsoe.css" />
8
+ <script type="module" src="index.js"></script>
9
+ </head>
10
+ <body>
11
+ </body>
12
+ </html>
package/demo/index.js ADDED
@@ -0,0 +1,163 @@
1
+ import {jml, body} from '../vendor/jamilih/dist/jml-es.js';
2
+
3
+ import {
4
+ typeChoices,
5
+ getFormatAndSchemaChoices,
6
+ formatAndTypeChoices,
7
+ getControlsForFormatAndValue,
8
+ Types // , Formats
9
+ } from '../src/index.js';
10
+
11
+ const keyPathNotExpectedTypeChoices = formatAndTypeChoices({
12
+ hasKeyPath: false,
13
+ typeNamespace: 'demo-keypath-not-expected'
14
+ });
15
+
16
+ jml('section', {role: 'main'}, [
17
+ ['h2', [
18
+ 'Format and type choices: No key path expected (type can vary at root)'
19
+ ]],
20
+
21
+ // Put inside form so can validate
22
+ ['form', [
23
+ ...keyPathNotExpectedTypeChoices.domArray
24
+ ]],
25
+
26
+ ['button', {
27
+ $on: {
28
+ click () {
29
+ // eslint-disable-next-line no-alert -- Simple demo
30
+ alert(keyPathNotExpectedTypeChoices.getType());
31
+ }
32
+ }
33
+ }, ['Get type']],
34
+
35
+ ['button', {
36
+ $on: {
37
+ click () {
38
+ // eslint-disable-next-line no-alert -- Simple demo
39
+ alert(keyPathNotExpectedTypeChoices.validValuesSet());
40
+ }
41
+ }
42
+ }, ['Is valid']],
43
+
44
+ ['button', {
45
+ $on: {
46
+ click () {
47
+ console.log(keyPathNotExpectedTypeChoices.getValue());
48
+ }
49
+ }
50
+ }, ['Log value']],
51
+
52
+ ['button', {
53
+ $on: {
54
+ async click () {
55
+ await keyPathNotExpectedTypeChoices.setValue(42);
56
+ }
57
+ }
58
+ }, ['Initialize with a value']],
59
+
60
+ ['h2', [
61
+ 'Format and type choices: Key path expected (object required at root)'
62
+ ]],
63
+ ...formatAndTypeChoices({
64
+ hasKeyPath: true,
65
+ typeNamespace: 'demo-keypath-expected'
66
+ }).domArray,
67
+
68
+ ['h2', [
69
+ 'Format choices without type selection ' +
70
+ '(might use as retrieval return format)'
71
+ ]],
72
+ ['select', [
73
+ getFormatAndSchemaChoices()
74
+ ]],
75
+
76
+ ['h2', [
77
+ 'Type choices only'
78
+ ]],
79
+
80
+ (() => {
81
+ const typeSelection = typeChoices({
82
+ format: 'structuredCloning',
83
+ typeNamespace: 'demo-type-choices-only'
84
+ });
85
+
86
+ return ['form', {
87
+ $on: {
88
+ submit (e) {
89
+ e.preventDefault();
90
+ }
91
+ }
92
+ }, [
93
+ ...typeSelection.domArray,
94
+ ['button', {
95
+ $on: {
96
+ click () {
97
+ // eslint-disable-next-line no-alert -- Simple demo
98
+ alert(typeSelection.getType());
99
+ }
100
+ }
101
+ }, ['Get type']],
102
+
103
+ ['button', {
104
+ $on: {
105
+ click () {
106
+ // eslint-disable-next-line no-alert -- Simple demo
107
+ alert(typeSelection.validValuesSet());
108
+ }
109
+ }
110
+ }, ['Is valid']],
111
+
112
+ ['button', {
113
+ $on: {
114
+ click () {
115
+ console.log(typeSelection.getValue());
116
+ }
117
+ }
118
+ }, ['Log value']],
119
+
120
+ ['button', {
121
+ $on: {
122
+ async click () {
123
+ await typeSelection.setValue(42);
124
+ }
125
+ }
126
+ }, ['Initialize with a value']]
127
+ ]];
128
+ })(),
129
+
130
+ ['h2', [
131
+ 'Convert arbitrary value to an editable menu'
132
+ ]],
133
+
134
+ await getControlsForFormatAndValue(
135
+ 'structuredCloning', new Date('1999-01-01')
136
+ ),
137
+
138
+ ['h2', [
139
+ 'Convert arbitrary value to a readonly menu'
140
+ ]],
141
+
142
+ await getControlsForFormatAndValue(
143
+ 'structuredCloning',
144
+ new Date('1999-01-01'), {
145
+ readonly: true
146
+ }
147
+ ),
148
+
149
+ ['h2', [
150
+ 'Convert structured cloning string representation to value and log'
151
+ ]],
152
+ ['input', {
153
+ placeholder: 'e.g., ["abc", 17]',
154
+ $on: {
155
+ change () {
156
+ const value = Types.getValueForString(this.value, {
157
+ format: 'structuredCloning'
158
+ })[0];
159
+ console.log(value);
160
+ }
161
+ }
162
+ }]
163
+ ], body);
package/index.html ADDED
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>JSOE</title>
6
+ </head>
7
+ <body>
8
+ See <a href="./demo/">Demo</a>
9
+ </body>
10
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@es-joy/jsoe",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.js"
@@ -80,10 +80,10 @@
80
80
  "scripts": {
81
81
  "eslint": "eslint --ext=js,cjs,md,html .",
82
82
  "lint": "npm run eslint --",
83
- "coverage": "open-cli http://localhost:8086/coverage/lcov-report/",
84
- "open": "open-cli http://localhost:8086/",
85
- "start": "node server 8086",
86
- "start-instrumented": "node server 8086 instrumented",
83
+ "coverage": "open-cli http://localhost:8087/coverage/lcov-report/",
84
+ "open": "open-cli http://localhost:8087/demo/",
85
+ "start": "node server 8087",
86
+ "start-instrumented": "node server 8087 instrumented",
87
87
  "cypress:open": "cypress open",
88
88
  "rollup": "rollup -c",
89
89
  "instrument-add": "nyc instrument src instrumented",
@@ -0,0 +1,245 @@
1
+ import {jml} from '../vendor/jamilih/dist/jml-es.js';
2
+ import {buildTypeChoices} from './typeChoices.js';
3
+ import Types from './types.js';
4
+ import {getControlsForFormatAndValue} from './formats.js';
5
+ import {$e, DOM} from './utils/templateUtils.js';
6
+
7
+ /**
8
+ * Defaults to structured cloning.
9
+ * @todo Compose from format metadata, so can make user customizable.
10
+ * @param {object} cfg
11
+ * @param {string} [cfg.schema] (NOT IN USE)
12
+ * @param {boolean} [cfg.hasKeyPath] Whether or not a key path is expected; if
13
+ * true, an indexedDB key is not allowed here as a key does not support
14
+ * the object type which is needed for a key path.
15
+ * @returns {DocumentFragment}
16
+ */
17
+ export const getFormatAndSchemaChoices = ({schema, hasKeyPath} = {}) => {
18
+ const hasSchema = typeof schema === 'string';
19
+ return [
20
+ ['JSON only', {value: 'json'}],
21
+ ...(hasKeyPath
22
+ ? []
23
+ : [['IndexedDB key', {value: 'indexedDBKey'}]]),
24
+ ['Structured Clone (via Typeson JSON)', {
25
+ value: 'structuredCloning', selected: !hasSchema
26
+ }]
27
+ /* schema:
28
+ ...(hasSchema
29
+ ? [
30
+ [`Schema + arbitrary: ${schema}`, {
31
+ value: 'schemaAndArbitrary',
32
+ dataset: {schema}
33
+ }],
34
+ [`Schema only: ${schema}`, {
35
+ value: 'schemaOnly',
36
+ dataset: {schema},
37
+ selected: hasSchema
38
+ }]
39
+ ]
40
+ : []
41
+ )
42
+ */
43
+ /*
44
+ // This can be supported for editing only
45
+ ['Arbitrary (Non-Typeson-serializable will be read-only)', {
46
+ value: 'arbitrary',
47
+ title: 'Any value that the typeson-registry supports ' +
48
+ 'for structured cloning'
49
+ }]
50
+ */
51
+ ].map(([optText, optAtts]) => {
52
+ return jml('option', optAtts, [optText]);
53
+ }).reduce((frag, option) => {
54
+ frag.append(option);
55
+ return frag;
56
+ }, document.createDocumentFragment());
57
+ };
58
+
59
+ /**
60
+ * Builds a selector and container for types.
61
+ * @param {object} cfg
62
+ * @param {string} [cfg.schema] The schema name (NOT IN USE)
63
+ * @param {object} [cfg.schemaContent] The schema content (NOT IN USE)
64
+ * @param {boolean} [cfg.hasValue] Set to `true` if you are supplying
65
+ * your own value. If `false` and `hasKeyPath` is `true`,
66
+ * will initialize with an object.
67
+ * @param {boolean} [cfg.singleValue] (NOT IN USE)
68
+ * @param {boolean} [cfg.hasKeyPath] If this is set (because there is a keyPath
69
+ * to be found within the object) and `hasValue` is true, an object type
70
+ * will be set and required at the root level. This option will also
71
+ * prevent selection of indexedDB key at root (since a key cannot be a
72
+ * plain object).
73
+ * @param {string} [cfg.typeNamespace] Used to prevent conflicts with other
74
+ * instances of typeChoices on the page
75
+ * @returns {{
76
+ * formatChoices: FormatChoices,
77
+ * typesHolder: TypesHolder,
78
+ * domArray: [formatChoices: FormatChoices, typesHolder: TypesHolder],
79
+ * getValue: (stateObj: import('./types.js').StateObject,
80
+ * currentPath: string) => StructuredCloneValue,
81
+ * getType: () => string|undefined,
82
+ * validValuesSet: () => boolean
83
+ * }} The selector for types and the container for them. Both should be
84
+ * added to the page.
85
+ */
86
+ export function formatAndTypeChoices ({
87
+ schema,
88
+ schemaContent,
89
+ hasValue,
90
+ singleValue,
91
+ hasKeyPath,
92
+ typeNamespace
93
+ }) {
94
+ const format = 'structuredCloning';
95
+ const formatChoices = jml('select', {
96
+ class: 'formatChoices',
97
+ hidden: singleValue,
98
+ // is: 'main-type-choices',
99
+ $custom: {
100
+ /**
101
+ * Sets the desired format and rebuilds the type choices.
102
+ * @callback SetFormat
103
+ * @param {string} valueFormat
104
+ * @returns {void}
105
+ */
106
+
107
+ /**
108
+ * Rebuilds the type choices.
109
+ * @callback TypeChoiceBuilder
110
+ * @returns {void}
111
+ */
112
+
113
+ /**
114
+ * @typedef {HTMLSelectElement} FormatChoices
115
+ * @property {SetFormat} $setFormat
116
+ * @property {TypeChoiceBuilder} $buildTypeChoices
117
+ */
118
+
119
+ /**
120
+ * @type {SetFormat}
121
+ */
122
+ $setFormat (valueFormat) {
123
+ this.value = valueFormat;
124
+ this.$buildTypeChoices();
125
+ },
126
+
127
+ /**
128
+ * @type {TypeChoiceBuilder}
129
+ */
130
+ $buildTypeChoices () {
131
+ DOM.removeChildren(typesHolder);
132
+ jml({'#': buildTypeChoices({
133
+ topRoot: $e(typesHolder, 'div[data-type]'),
134
+ resultType: 'both',
135
+ format: this.value,
136
+ typeNamespace,
137
+ requireObject: hasKeyPath,
138
+ objectHasValue: hasValue,
139
+ schema,
140
+ schemaContent
141
+ }).domArray}, typesHolder);
142
+ }
143
+ },
144
+ $on: {change () {
145
+ this.$buildTypeChoices();
146
+ }}
147
+ }, [getFormatAndSchemaChoices({schema, hasKeyPath})]);
148
+
149
+ /**
150
+ * @callback TypeRootGetter
151
+ * @returns {void}
152
+ */
153
+
154
+ /**
155
+ * @callback TypeSelectGetter
156
+ * @returns {void}
157
+ */
158
+
159
+ /**
160
+ * @typedef {HTMLDivElement} TypesHolder
161
+ * @property {TypeRootGetter} $getTypeRoot
162
+ * @property {TypeSelectGetter} $getTypeSelect
163
+ */
164
+
165
+ const typesHolder = jml('div', {class: 'typesHolder', $custom: {
166
+ /**
167
+ * @type {TypeRootGetter}
168
+ */
169
+ $getTypeRoot () {
170
+ return $e(this, 'div[data-type]');
171
+ },
172
+ /**
173
+ * @type {TypeSelectGetter}
174
+ */
175
+ $getTypeSelect () {
176
+ return $e(this, `.typeChoices-${typeNamespace}`);
177
+ }
178
+ }});
179
+
180
+ jml({'#': buildTypeChoices({
181
+ resultType: 'both',
182
+ topRoot: $e(typesHolder, 'div[data-type]'),
183
+ format,
184
+ typeNamespace,
185
+ requireObject: hasKeyPath,
186
+ objectHasValue: hasValue,
187
+ schema,
188
+ schemaContent
189
+ }).domArray}, typesHolder);
190
+
191
+ return {
192
+ formatChoices,
193
+ typesHolder,
194
+ // Easier for Jamilih
195
+ domArray: [formatChoices, typesHolder],
196
+
197
+ // Normal API
198
+
199
+ /**
200
+ * @param {import('./types.js').StateObject} [stateObj] Will
201
+ * auto-set `typeNamespace` and `format`
202
+ * @param {string} [currentPath]
203
+ * @returns {StructuredCloneValue}
204
+ */
205
+ getValue (stateObj, currentPath) {
206
+ const root = typesHolder.$getTypeRoot();
207
+ return Types.getValueForRoot(root, {
208
+ typeNamespace,
209
+ format: formatChoices.value,
210
+ ...stateObj
211
+ }, currentPath);
212
+ },
213
+
214
+ /**
215
+ * @returns {string|undefined}
216
+ */
217
+ getType () {
218
+ const root = typesHolder.$getTypeRoot();
219
+ return Types.getTypeForRoot(root);
220
+ },
221
+
222
+ /**
223
+ * @returns {boolean}
224
+ */
225
+ validValuesSet () {
226
+ const root = typesHolder.$getTypeRoot();
227
+ const form = root.closest('form');
228
+ return Types.validValuesSet({form, typeNamespace});
229
+ },
230
+
231
+ /**
232
+ * @param {StructuredCloneValue} value
233
+ * @param {import('./types.js').StateObject} stateObj
234
+ * @returns {Promise<void>}
235
+ */
236
+ async setValue (value, stateObj) {
237
+ const rootEditUI = await getControlsForFormatAndValue(
238
+ formatChoices.value, value, stateObj
239
+ );
240
+ const type = Types.getTypeForRoot(rootEditUI);
241
+ const sel = typesHolder.$getTypeSelect();
242
+ sel.$addTypeAndEditUI({type, editUI: rootEditUI});
243
+ }
244
+ };
245
+ }
@@ -46,7 +46,7 @@ export const convertFromTypeson = (typesonType) => {
46
46
  };
47
47
 
48
48
  /**
49
- * @type {FormatIterator}
49
+ * @type {import('./structuredCloning.js').FormatIterator}
50
50
  */
51
51
  export const iterate = (records, stateObj) => {
52
52
  stateObj.format = 'indexedDBKey';
@@ -8,7 +8,7 @@ export const types = () => [
8
8
  ];
9
9
 
10
10
  /**
11
- * @type {FormatIterator}
11
+ * @type {import('./structuredCloning.js').FormatIterator}
12
12
  */
13
13
  export const iterate = (records, stateObj) => {
14
14
  // Todo (low): Add a more optimal (`JSON.stringify`-based iterator)
@@ -1,7 +1,7 @@
1
1
  import * as structuredCloning from './structuredCloning.js';
2
2
 
3
3
  /**
4
- * @type {FormatIterator}
4
+ * @type {import('./structuredCloning.js').FormatIterator}
5
5
  */
6
6
  export const iterate = (records, stateObj) => {
7
7
  console.log('records', records, stateObj);
@@ -8,7 +8,7 @@ import * as structuredCloning from './structuredCloning.js';
8
8
  import deepEqual from '../deepEqual.js';
9
9
 
10
10
  /**
11
- * @type {FormatIterator}
11
+ * @type {import('./structuredCloning.js').FormatIterator}
12
12
  */
13
13
  export const iterate = (records, stateObj) => {
14
14
  console.log('records', records, stateObj);
@@ -1,5 +1,6 @@
1
1
  import Formats from '../formats.js';
2
2
  import Types from '../types.js';
3
+ import {buildTypeChoices} from '../typeChoices.js';
3
4
  import {
4
5
  Typeson, unescapeKeyPathComponent, structuredCloningThrowing
5
6
  } from '../../vendor/typeson-registry/dist/index.js';
@@ -16,22 +17,7 @@ import * as json from './json.js';
16
17
  */
17
18
 
18
19
  /**
19
- * @typedef {{
20
- * typeNamespace: string,
21
- * "readonly": boolean,
22
- * format: string,
23
- * schemaContent: string,
24
- * getPossibleSchemasForPathAndType: (
25
- * keypath: string,
26
- * parentPath: string,
27
- * arrayOrObjectPropertyName: string,
28
- * valueType: string
29
- * ) => StateObject
30
- * }} StateObject
31
- */
32
-
33
- /**
34
- * @param {StateObject} stateObj
20
+ * @param {import('../types.js').StateObject} stateObj
35
21
  * @returns {EncapsulateObserver}
36
22
  */
37
23
  const encapsulateObserver = (stateObj) => {
@@ -119,7 +105,7 @@ const encapsulateObserver = (stateObj) => {
119
105
  typeNamespace,
120
106
  type: newType,
121
107
  bringIntoFocus: false,
122
- buildTypeChoices: Formats.buildTypeChoices,
108
+ buildTypeChoices,
123
109
  format,
124
110
  schemaContent,
125
111
  schemaState: getPossibleSchemasForPathAndType,
@@ -260,11 +246,7 @@ const canonicalToAvailableType = (format, state, valType, v) => {
260
246
  /**
261
247
  * @callback FormatIterator
262
248
  * @param {StructuredCloneValue} records
263
- * @param {{
264
- * format: string
265
- * error: Error
266
- * rootUI: Element
267
- * }} stateObj
249
+ * @param {import('../types.js').StateObject} stateObj
268
250
  * @returns {Promise<Element>}
269
251
  */
270
252