@kubohiroya/turbowarp-extension-data-editor 0.2.2 → 0.3.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/README.md +2 -0
- package/dist/build-info.json +3 -3
- package/dist/config/feature-flags.d.ts +5 -0
- package/dist/config/feature-flags.js +6 -0
- package/dist/form-presentation.d.ts +8 -0
- package/dist/form-presentation.js +62 -0
- package/dist/layout-view.d.ts +21 -0
- package/dist/layout-view.js +127 -0
- package/dist/layout.d.ts +31 -0
- package/dist/layout.js +63 -0
- package/dist/rjsf-templates.d.ts +8 -1
- package/dist/rjsf-templates.js +71 -1
- package/dist/rjsf.d.ts +11 -8
- package/dist/rjsf.js +24 -13
- package/dist/schema-presentation.d.ts +10 -0
- package/dist/schema-presentation.js +109 -0
- package/dist/source-editor.d.ts +2 -1
- package/dist/source-editor.js +23 -5
- package/dist/source-view.d.ts +1 -1
- package/dist/source-view.js +222 -122
- package/dist/turbowarp/appearance.d.ts +1 -1
- package/dist/turbowarp/appearance.js +23 -5
- package/dist/turbowarp/bridge.js +3 -2
- package/dist/turbowarp.d.ts +2 -1
- package/dist/turbowarp.js +3 -2
- package/dist/webpack4/metafile.json +136 -19
- package/dist/webpack4/rjsf.js +961 -279
- package/dist/webpack4/rjsf.js.map +4 -4
- package/dist/webpack4/turbowarp.js +20 -6
- package/dist/webpack4/turbowarp.js.map +2 -2
- package/docs/README.md +1 -0
- package/docs/common-layout.ja.md +125 -0
- package/docs/github-pages.md +5 -3
- package/package.json +1 -1
package/dist/source-view.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import * as ReactDOM from 'react-dom';
|
|
3
|
+
import { LayoutHost, ObjectLayout, useCommonLayout } from './layout-view.js';
|
|
3
4
|
import { createSourceCandidate, patchDecodedSource, readSourceDocument } from './source-editor.js';
|
|
4
5
|
import { cloneJson } from './data.js';
|
|
6
|
+
import { FormHelp, propertyLabel } from './form-presentation.js';
|
|
7
|
+
import { jsonTypes, presentationInitial, presentationTypes, presentationCondition } from './schema-presentation.js';
|
|
8
|
+
let nextFieldId = 0;
|
|
9
|
+
const SourcePaths = React.createContext([]);
|
|
5
10
|
const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
6
11
|
const pointer = (path, key) => `${path}/${key.replace(/~/g, '~0').replace(/\//g, '~1')}`;
|
|
7
12
|
const typeOf = (v) => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v;
|
|
@@ -17,6 +22,8 @@ const initial = (type) => {
|
|
|
17
22
|
};
|
|
18
23
|
function mergeHints(left, right) {
|
|
19
24
|
const result = { ...left, ...right };
|
|
25
|
+
if (Array.isArray(left.required) && Array.isArray(right.required))
|
|
26
|
+
result.required = [...new Set([...left.required, ...right.required])];
|
|
20
27
|
for (const field of ['properties', 'patternProperties']) {
|
|
21
28
|
if (!record(left[field]) && !record(right[field]))
|
|
22
29
|
continue;
|
|
@@ -48,33 +55,40 @@ function propertyHint(hints, key) {
|
|
|
48
55
|
}
|
|
49
56
|
return matches.length > 1 ? { allOf: matches } : matches.length ? matches[0] : hints.additionalProperties;
|
|
50
57
|
}
|
|
58
|
+
function sourceReference(variant, ref) {
|
|
59
|
+
const hash = ref.indexOf('#');
|
|
60
|
+
const uri = hash < 0 ? ref : ref.slice(0, hash);
|
|
61
|
+
const resources = [variant.schemaBundle.root, ...variant.schemaBundle.resources];
|
|
62
|
+
const resource = uri ? resources.find(r => r.retrievalUri === uri || (record(r.document) && r.document.$id === uri)) : variant.schemaBundle.root;
|
|
63
|
+
let target = resource === null || resource === void 0 ? void 0 : resource.document;
|
|
64
|
+
const fragment = hash < 0 ? '' : decodeURIComponent(ref.slice(hash + 1));
|
|
65
|
+
if (fragment.startsWith('/'))
|
|
66
|
+
for (const part of fragment.slice(1).split('/')) {
|
|
67
|
+
target = record(target) || Array.isArray(target) ? target[part.replace(/~1/g, '/').replace(/~0/g, '~')] : undefined;
|
|
68
|
+
}
|
|
69
|
+
return !fragment || fragment.startsWith('/') ? target : undefined;
|
|
70
|
+
}
|
|
51
71
|
/** Presentation hints never replace canonical validation or remove schema constraints. */
|
|
52
|
-
function resolveHints(variant, schema, data, seen = new Set()) {
|
|
72
|
+
function resolveHints(variant, schema, data, seen = new Set(), conditional = false) {
|
|
53
73
|
var _a;
|
|
54
74
|
if (!record(schema) || seen.has(schema))
|
|
55
75
|
return {};
|
|
56
76
|
seen.add(schema);
|
|
57
77
|
let result = { ...schema };
|
|
58
78
|
if (typeof schema.$ref === 'string') {
|
|
59
|
-
|
|
60
|
-
const hash = ref.indexOf('#');
|
|
61
|
-
const uri = hash < 0 ? ref : ref.slice(0, hash);
|
|
62
|
-
const resources = [variant.schemaBundle.root, ...variant.schemaBundle.resources];
|
|
63
|
-
const resource = uri ? resources.find(r => r.retrievalUri === uri || (record(r.document) && r.document.$id === uri)) : variant.schemaBundle.root;
|
|
64
|
-
let target = resource === null || resource === void 0 ? void 0 : resource.document;
|
|
65
|
-
const fragment = hash < 0 ? '' : decodeURIComponent(ref.slice(hash + 1));
|
|
66
|
-
if (fragment.startsWith('/'))
|
|
67
|
-
for (const part of fragment.slice(1).split('/')) {
|
|
68
|
-
target = record(target) || Array.isArray(target) ? target[part.replace(/~1/g, '/').replace(/~0/g, '~')] : undefined;
|
|
69
|
-
}
|
|
70
|
-
if (!fragment || fragment.startsWith('/'))
|
|
71
|
-
result = mergeHints(resolveHints(variant, target, data, seen), result);
|
|
79
|
+
result = mergeHints(resolveHints(variant, sourceReference(variant, schema.$ref), data, seen, conditional), result);
|
|
72
80
|
}
|
|
73
81
|
if (Array.isArray(schema.allOf))
|
|
74
82
|
for (const part of schema.allOf) {
|
|
75
|
-
const next = resolveHints(variant, part, data, seen);
|
|
83
|
+
const next = resolveHints(variant, part, data, seen, conditional);
|
|
76
84
|
result = mergeHints(result, next);
|
|
77
85
|
}
|
|
86
|
+
if (conditional && Object.hasOwn(schema, 'if')) {
|
|
87
|
+
const matches = presentationCondition(schema.if, data, ref => sourceReference(variant, ref));
|
|
88
|
+
const branch = matches === true ? schema.then : matches === false ? schema.else : undefined;
|
|
89
|
+
if (branch !== undefined)
|
|
90
|
+
result = mergeHints(result, resolveHints(variant, branch, data, seen, conditional));
|
|
91
|
+
}
|
|
78
92
|
const alternatives = (_a = schema.oneOf) !== null && _a !== void 0 ? _a : schema.anyOf;
|
|
79
93
|
if (Array.isArray(alternatives)) {
|
|
80
94
|
const choices = alternatives.filter(part => {
|
|
@@ -90,13 +104,13 @@ function resolveHints(variant, schema, data, seen = new Set()) {
|
|
|
90
104
|
return true;
|
|
91
105
|
});
|
|
92
106
|
if (choices.length === 1)
|
|
93
|
-
result = mergeHints(result, resolveHints(variant, choices[0], data, seen));
|
|
107
|
+
result = mergeHints(result, resolveHints(variant, choices[0], data, seen, conditional));
|
|
94
108
|
}
|
|
95
109
|
if (!Array.isArray(result.enum)) {
|
|
96
110
|
if (Object.hasOwn(result, 'const'))
|
|
97
111
|
result.enum = [result.const];
|
|
98
112
|
else if (Array.isArray(alternatives)) {
|
|
99
|
-
const enums = alternatives.map(part => resolveHints(variant, part, data, new Set(seen)));
|
|
113
|
+
const enums = alternatives.map(part => resolveHints(variant, part, data, new Set(seen), conditional));
|
|
100
114
|
if (enums.length && enums.every(part => Array.isArray(part.enum))) {
|
|
101
115
|
const values = enums.flatMap(part => part.enum);
|
|
102
116
|
result.enum = [...new Map(values.map(value => [JSON.stringify(value), value])).values()];
|
|
@@ -105,26 +119,38 @@ function resolveHints(variant, schema, data, seen = new Set()) {
|
|
|
105
119
|
}
|
|
106
120
|
return result;
|
|
107
121
|
}
|
|
108
|
-
function Tree({ value, path, schema, ui, variant, invalidNumbers, change: onChange }) {
|
|
122
|
+
function Tree({ value, path, schema, ui, variant, invalidNumbers, required, change: onChange }) {
|
|
109
123
|
var _a;
|
|
110
|
-
const
|
|
124
|
+
const errors = React.useContext(SourcePaths);
|
|
125
|
+
const commonLayout = useCommonLayout();
|
|
126
|
+
const hints = resolveHints(variant, schema, value, new Set(), commonLayout);
|
|
111
127
|
const uiHints = record(ui) ? ui : {};
|
|
112
128
|
const [newKey, setNewKey] = React.useState('');
|
|
129
|
+
const [id] = React.useState(() => `source-field-${++nextFieldId}`);
|
|
113
130
|
const change = (next, edit = { path }) => onChange(next, edit);
|
|
114
131
|
const choices = Array.isArray(hints.enum) ? hints.enum : Array.isArray(uiHints['ui:enum']) ? uiHints['ui:enum'] : undefined;
|
|
115
|
-
const
|
|
116
|
-
const
|
|
132
|
+
const key = path.slice(path.lastIndexOf('/') + 1).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
133
|
+
const label = commonLayout && typeof uiHints['ui:title'] === 'string' ? uiHints['ui:title'] : typeof hints.title === 'string' ? hints.title : commonLayout ? propertyLabel(key) : path || '/';
|
|
117
134
|
const type = typeOf(value);
|
|
135
|
+
const allowed = presentationTypes(schema, ref => sourceReference(variant, ref));
|
|
136
|
+
const fixedType = allowed.size === 1 && allowed.has(type);
|
|
137
|
+
const readonly = commonLayout && (hints.readOnly === true || uiHints['ui:readonly'] === true || uiHints['ui:disabled'] === true);
|
|
138
|
+
const props = { 'aria-label': label, ...(commonLayout ? { id, disabled: readonly, 'aria-describedby': typeof hints.description === 'string' && hints.description.trim() ? `${id}-help` : undefined } : {}) };
|
|
139
|
+
const typeControl = !commonLayout || !fixedType ? React.createElement("select", { "data-source-type-control": "", "aria-label": `Type ${path || '/'}`, disabled: readonly, value: type, onChange: e => change(initial(e.target.value)) }, (commonLayout && allowed.size ? [...new Set([type, ...allowed])] : jsonTypes).map(t => React.createElement("option", { key: t }, t))) : null;
|
|
140
|
+
const seed = (hint) => cloneJson(presentationInitial(hint));
|
|
118
141
|
let field;
|
|
119
142
|
if (choices) {
|
|
120
143
|
const serialized = JSON.stringify(value);
|
|
121
144
|
const options = choices.map(item => JSON.stringify(item));
|
|
122
145
|
if (!options.includes(serialized))
|
|
123
146
|
options.unshift(serialized);
|
|
124
|
-
field = React.createElement("
|
|
147
|
+
field = commonLayout && options.length === 1 ? React.createElement("input", { ...props, readOnly: true, value: typeof value === 'string' ? value : serialized })
|
|
148
|
+
: React.createElement("select", { ...props, value: serialized, onChange: e => change(JSON.parse(e.target.value)) }, options.map(item => React.createElement("option", { key: item, value: item }, commonLayout && typeof JSON.parse(item) === 'string' ? JSON.parse(item) : item)));
|
|
125
149
|
}
|
|
126
150
|
else if (typeof value === 'string') {
|
|
127
|
-
field =
|
|
151
|
+
field = commonLayout && uiHints['ui:widget'] !== 'textarea' && !value.includes('\n')
|
|
152
|
+
? React.createElement("input", { ...props, type: "text", value: value, onChange: e => change(e.target.value) })
|
|
153
|
+
: React.createElement("textarea", { ...props, value: value, onChange: e => change(e.target.value) });
|
|
128
154
|
}
|
|
129
155
|
else if (typeof value === 'number') {
|
|
130
156
|
field = React.createElement("input", { ...props, type: "text", inputMode: "decimal", value: (_a = invalidNumbers[path]) !== null && _a !== void 0 ? _a : String(value), onChange: e => {
|
|
@@ -137,84 +163,137 @@ function Tree({ value, path, schema, ui, variant, invalidNumbers, change: onChan
|
|
|
137
163
|
field = React.createElement("input", { ...props, type: "checkbox", checked: value, onChange: e => change(e.target.checked) });
|
|
138
164
|
}
|
|
139
165
|
else if (Array.isArray(value)) {
|
|
166
|
+
const itemHint = (index) => { var _a, _b; return Array.isArray(hints.prefixItems) ? (_a = hints.prefixItems[index]) !== null && _a !== void 0 ? _a : hints.items : Array.isArray(hints.items) ? (_b = hints.items[index]) !== null && _b !== void 0 ? _b : hints.additionalItems : hints.items; };
|
|
167
|
+
const tuple = Array.isArray(hints.prefixItems) || Array.isArray(hints.items);
|
|
168
|
+
const canRemove = !commonLayout || value.length > (typeof hints.minItems === 'number' ? hints.minItems : 0);
|
|
169
|
+
const canAdd = !commonLayout || (itemHint(value.length) !== false && (typeof hints.maxItems !== 'number' || value.length < hints.maxItems));
|
|
140
170
|
field = React.createElement("div", null,
|
|
141
|
-
value.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}),
|
|
152
|
-
React.createElement("button", { type: "button", "aria-label": `Add item ${path || '/'}`, onClick: () => change([...value, null], { path: pointer(path, String(value.length)) }) }, "Add item"));
|
|
171
|
+
commonLayout && value.length === 0 && React.createElement("p", { className: "editor-form-hint" }, "\u9805\u76EE\u306A\u3057"),
|
|
172
|
+
value.map((child, index) => React.createElement("div", { key: index, className: commonLayout ? 'editor-form-array-item' : undefined },
|
|
173
|
+
React.createElement(Tree, { invalidNumbers: invalidNumbers, value: child, path: pointer(path, String(index)), variant: variant, schema: itemHint(index), ui: uiHints.items, change: (next, edit) => { const copy = [...value]; copy[index] = next; change(copy, edit); } }),
|
|
174
|
+
canRemove && React.createElement("button", { type: "button", "aria-label": `Remove ${pointer(path, String(index))}`, onClick: () => change(value.filter((_, i) => i !== index), { path, removeIndex: index }) }, "Remove"),
|
|
175
|
+
(!commonLayout || !tuple && value.length > 1) && React.createElement("button", { type: "button", disabled: index === 0, "aria-label": `Move up ${pointer(path, String(index))}`, onClick: () => {
|
|
176
|
+
const copy = [...value];
|
|
177
|
+
[copy[index - 1], copy[index]] = [copy[index], copy[index - 1]];
|
|
178
|
+
change(copy, { path, swap: [index - 1, index] });
|
|
179
|
+
} }, "\u2191"))),
|
|
180
|
+
canAdd && React.createElement("button", { type: "button", "aria-label": `Add item ${path || '/'}`, onClick: () => change([...value, commonLayout ? seed(resolveHints(variant, itemHint(value.length), null)) : null], { path: pointer(path, String(value.length)) }) }, "Add item"));
|
|
153
181
|
}
|
|
154
182
|
else if (value !== null && typeof value === 'object') {
|
|
155
183
|
const properties = record(hints.properties) ? hints.properties : {};
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
const requiredKeys = new Set(Array.isArray(hints.required) ? hints.required : []);
|
|
185
|
+
const optional = Object.keys(properties).filter(name => !requiredKeys.has(name));
|
|
186
|
+
const missing = Object.keys(properties).filter(name => !Object.hasOwn(value, name));
|
|
187
|
+
const remove = (name) => { const copy = cloneJson(value); delete copy[name]; change(copy, { path: pointer(path, name) }); };
|
|
188
|
+
const add = (name) => {
|
|
189
|
+
const hint = resolveHints(variant, propertyHint(hints, name), null);
|
|
190
|
+
const copy = cloneJson(value);
|
|
191
|
+
Object.defineProperty(copy, name, { value: seed(hint), enumerable: true, writable: true, configurable: true });
|
|
192
|
+
change(copy, { path: pointer(path, name) });
|
|
193
|
+
setNewKey('');
|
|
194
|
+
};
|
|
195
|
+
const fields = Object.entries(value).map(([name, child]) => ({ name,
|
|
196
|
+
errors: errors.filter(error => error === pointer(path, name) || error.startsWith(pointer(path, name) + '/')).length,
|
|
197
|
+
content: React.createElement("div", { key: name },
|
|
198
|
+
React.createElement(Tree, { invalidNumbers: invalidNumbers, value: child, path: pointer(path, name), variant: variant, schema: propertyHint(hints, name), required: requiredKeys.has(name), ui: uiHints[name], change: (next, edit) => {
|
|
159
199
|
const copy = cloneJson(value);
|
|
160
|
-
Object.defineProperty(copy,
|
|
200
|
+
Object.defineProperty(copy, name, { value: next, enumerable: true, writable: true, configurable: true });
|
|
161
201
|
change(copy, edit);
|
|
162
202
|
} }),
|
|
163
|
-
React.createElement("button", { type: "button", "aria-label": `Remove ${pointer(path,
|
|
203
|
+
(!commonLayout || !Object.hasOwn(properties, name) && !requiredKeys.has(name)) && React.createElement("button", { type: "button", "aria-label": `Remove ${pointer(path, name)}`, onClick: () => remove(name) }, "Remove")) }));
|
|
204
|
+
const dynamic = hints.additionalProperties !== false || record(hints.patternProperties);
|
|
205
|
+
field = React.createElement("div", null,
|
|
206
|
+
React.createElement(ObjectLayout, { ui: uiHints, knownFields: [...new Set([...Object.keys(properties), ...Object.keys(value)])], fields: fields, fallback: fields.map(item => item.content) }),
|
|
207
|
+
commonLayout ? React.createElement(React.Fragment, null,
|
|
208
|
+
(missing.length > 0 || optional.some(name => Object.hasOwn(value, name))) && React.createElement("details", { className: "editor-form-tools" },
|
|
209
|
+
React.createElement("summary", null, "\u9805\u76EE\u3092\u7BA1\u7406"),
|
|
210
|
+
missing.map(name => React.createElement("button", { type: "button", key: name, onClick: () => add(name) },
|
|
211
|
+
propertyLabel(name),
|
|
212
|
+
"\u3092\u8FFD\u52A0")),
|
|
213
|
+
optional.filter(name => Object.hasOwn(value, name)).map(name => React.createElement("label", { key: name },
|
|
214
|
+
propertyLabel(name),
|
|
215
|
+
React.createElement("button", { type: "button", "aria-label": `Remove ${pointer(path, name)}`, onClick: () => remove(name) }, "\u5024\u3092\u89E3\u9664")))),
|
|
216
|
+
dynamic && React.createElement("details", { className: "editor-form-tools" },
|
|
217
|
+
React.createElement("summary", null, "\u8FFD\u52A0\u306E\u30D7\u30ED\u30D1\u30C6\u30A3"),
|
|
218
|
+
React.createElement("label", { htmlFor: `${id}-key` }, "\u9805\u76EE\u540D"),
|
|
219
|
+
React.createElement("input", { id: `${id}-key`, "aria-label": `New property ${path || '/'}`, value: newKey, onChange: e => setNewKey(e.target.value) }),
|
|
220
|
+
React.createElement("button", { type: "button", disabled: !newKey || Object.hasOwn(value, newKey) || propertyHint(hints, newKey) === false, onClick: () => add(newKey) }, "Add property"))) : React.createElement(React.Fragment, null,
|
|
221
|
+
React.createElement("label", null,
|
|
222
|
+
"Property ",
|
|
223
|
+
React.createElement("input", { "aria-label": `New property ${path || '/'}`, value: newKey, onChange: e => setNewKey(e.target.value) })),
|
|
224
|
+
React.createElement("select", { "aria-label": `Known property ${path || '/'}`, value: "", onChange: e => setNewKey(e.target.value) },
|
|
225
|
+
React.createElement("option", { value: "" }, "Choose property"),
|
|
226
|
+
missing.map(name => React.createElement("option", { key: name }, name))),
|
|
227
|
+
React.createElement("button", { type: "button", disabled: !newKey || Object.hasOwn(value, newKey), onClick: () => {
|
|
164
228
|
const copy = cloneJson(value);
|
|
165
|
-
|
|
166
|
-
change(copy, { path: pointer(path,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"Property ",
|
|
170
|
-
React.createElement("input", { "aria-label": `New property ${path || '/'}`, value: newKey, onChange: e => setNewKey(e.target.value) })),
|
|
171
|
-
React.createElement("select", { "aria-label": `Known property ${path || '/'}`, value: "", onChange: e => setNewKey(e.target.value) },
|
|
172
|
-
React.createElement("option", { value: "" }, "Choose property"),
|
|
173
|
-
Object.keys(properties).filter(key => !Object.hasOwn(value, key)).map(key => React.createElement("option", { key: key }, key))),
|
|
174
|
-
React.createElement("button", { type: "button", disabled: !newKey || Object.hasOwn(value, newKey), onClick: () => {
|
|
175
|
-
const copy = cloneJson(value);
|
|
176
|
-
Object.defineProperty(copy, newKey, { value: null, enumerable: true, writable: true, configurable: true });
|
|
177
|
-
change(copy, { path: pointer(path, newKey) });
|
|
178
|
-
setNewKey('');
|
|
179
|
-
} }, "Add property"));
|
|
229
|
+
Object.defineProperty(copy, newKey, { value: null, enumerable: true, writable: true, configurable: true });
|
|
230
|
+
change(copy, { path: pointer(path, newKey) });
|
|
231
|
+
setNewKey('');
|
|
232
|
+
} }, "Add property")));
|
|
180
233
|
}
|
|
181
234
|
else
|
|
182
235
|
field = React.createElement("span", null, "null");
|
|
183
|
-
|
|
236
|
+
const containerProps = { 'data-source-path': path, 'data-source-error': commonLayout && errors.includes(path) };
|
|
237
|
+
if (commonLayout && !Array.isArray(value) && !record(value))
|
|
238
|
+
return React.createElement("div", { ...containerProps, className: "editor-form-field" },
|
|
239
|
+
typeof value === 'boolean' && !choices ? React.createElement("label", { className: "editor-form-toggle", htmlFor: id },
|
|
240
|
+
field,
|
|
241
|
+
React.createElement("span", null, label)) : React.createElement(React.Fragment, null,
|
|
242
|
+
React.createElement("label", { htmlFor: id },
|
|
243
|
+
label,
|
|
244
|
+
required && React.createElement("span", { className: "required" }, " *")),
|
|
245
|
+
field),
|
|
246
|
+
typeControl,
|
|
247
|
+
React.createElement(FormHelp, { description: hints.description, id: `${id}-help` }),
|
|
248
|
+
invalidNumbers[path] !== undefined && React.createElement("p", { className: "editor-form-invalid", role: "alert" }, "\u6709\u52B9\u306A\u6570\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"));
|
|
249
|
+
return React.createElement("fieldset", { ...containerProps, className: commonLayout ? 'editor-form-section' : undefined, disabled: readonly },
|
|
184
250
|
React.createElement("legend", null, label),
|
|
185
|
-
|
|
251
|
+
commonLayout && React.createElement(FormHelp, { description: hints.description }),
|
|
252
|
+
typeControl,
|
|
186
253
|
field);
|
|
187
254
|
}
|
|
188
|
-
function liveIssues(draft, bindings) {
|
|
255
|
+
function liveIssues(draft, bindings, commonLayout = false) {
|
|
256
|
+
var _a;
|
|
189
257
|
const issues = [];
|
|
190
258
|
const sources = draft.sources;
|
|
191
259
|
for (const binding of bindings) {
|
|
192
260
|
const entry = sources[binding.argument];
|
|
193
261
|
try {
|
|
194
|
-
if (entry.invalidForm === true)
|
|
195
|
-
|
|
262
|
+
if (entry.invalidForm === true) {
|
|
263
|
+
if (!commonLayout || !record(entry.invalidNumbers) || !Object.keys(entry.invalidNumbers).length)
|
|
264
|
+
throw new TypeError();
|
|
265
|
+
for (const path of Object.keys(((_a = entry.invalidNumbers) !== null && _a !== void 0 ? _a : {})))
|
|
266
|
+
issues.push({ message: 'Enter a valid JSON number.', path: pointer('', binding.argument) + path });
|
|
267
|
+
}
|
|
196
268
|
const doc = record(entry.creating)
|
|
197
269
|
? { ...entry.creating, data: entry.data }
|
|
198
270
|
: readSourceDocument(binding, entry.source);
|
|
199
271
|
const validator = binding.validators.get(JSON.stringify([doc.format, doc.variantId]));
|
|
200
|
-
if (validator
|
|
201
|
-
|
|
272
|
+
if (validator) {
|
|
273
|
+
const result = validator.validate((entry.mode === 'form' ? entry.data : doc.data));
|
|
274
|
+
if (result.kind !== 'valid') {
|
|
275
|
+
if (commonLayout)
|
|
276
|
+
for (const diagnostic of result.diagnostics)
|
|
277
|
+
issues.push({ message: 'Value does not satisfy the schema.', path: pointer('', binding.argument) + ('dataPath' in diagnostic ? diagnostic.dataPath : '') });
|
|
278
|
+
else
|
|
279
|
+
issues.push({ message: 'Value does not satisfy the schema.', path: `/${binding.argument}` });
|
|
280
|
+
}
|
|
202
281
|
}
|
|
203
282
|
}
|
|
204
|
-
catch (
|
|
283
|
+
catch (_b) {
|
|
205
284
|
issues.push({ message: 'Repair the invalid source or form value.', path: `/${binding.argument}` });
|
|
206
285
|
}
|
|
207
286
|
}
|
|
208
287
|
return issues;
|
|
209
288
|
}
|
|
210
|
-
function SourceView({ context, bindings }) {
|
|
289
|
+
function SourceView({ context, bindings, commonLayout }) {
|
|
211
290
|
const [, redraw] = React.useState(0);
|
|
212
291
|
const [creating, selectCreateVariant] = React.useState({});
|
|
213
|
-
React.
|
|
292
|
+
React.useLayoutEffect(() => {
|
|
214
293
|
const stop = context.subscribe(() => redraw(n => n + 1));
|
|
215
|
-
context.setValidationIssues(liveIssues(context.getDraft(), bindings));
|
|
294
|
+
context.setValidationIssues(liveIssues(context.getDraft(), bindings, commonLayout));
|
|
216
295
|
return stop;
|
|
217
|
-
}, [context, bindings]);
|
|
296
|
+
}, [context, bindings, commonLayout]);
|
|
218
297
|
const state = context.getState();
|
|
219
298
|
if (state.status !== 'editing' && state.status !== 'saving')
|
|
220
299
|
return React.createElement(React.Fragment, null);
|
|
@@ -226,10 +305,10 @@ function SourceView({ context, bindings }) {
|
|
|
226
305
|
const nextSources = next.sources;
|
|
227
306
|
Object.defineProperty(nextSources, argument, { value: entry, enumerable: true, configurable: true, writable: true });
|
|
228
307
|
context.setDraft(next);
|
|
229
|
-
context.setValidationIssues(liveIssues(next, bindings));
|
|
308
|
+
context.setValidationIssues(liveIssues(next, bindings, commonLayout));
|
|
230
309
|
};
|
|
231
310
|
return React.createElement("div", { className: "block-editor-source-forms" }, bindings.map(binding => {
|
|
232
|
-
var _a, _b;
|
|
311
|
+
var _a, _b, _c;
|
|
233
312
|
const entry = sources[binding.argument];
|
|
234
313
|
const selectedCreation = Object.hasOwn(creating, binding.argument) ? creating[binding.argument] : '';
|
|
235
314
|
let document;
|
|
@@ -238,29 +317,38 @@ function SourceView({ context, bindings }) {
|
|
|
238
317
|
document = readSourceDocument(binding, entry.source);
|
|
239
318
|
variant = binding.presentationVariants.find(v => v.format === document.format && v.variantId === document.variantId);
|
|
240
319
|
}
|
|
241
|
-
catch ( /* The Source textarea remains available for repair. */
|
|
320
|
+
catch ( /* The Source textarea remains available for repair. */_d) { /* The Source textarea remains available for repair. */ }
|
|
242
321
|
if (record(entry.creating)) {
|
|
243
322
|
const creation = entry.creating;
|
|
244
323
|
variant = binding.presentationVariants.find(v => v.format === creation.format && v.variantId === creation.variantId);
|
|
245
324
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
325
|
+
const argumentPath = pointer('', binding.argument);
|
|
326
|
+
const errorPaths = [...new Set(state.issues.flatMap(issue => {
|
|
327
|
+
var _a;
|
|
328
|
+
return ((_a = issue.path) === null || _a === void 0 ? void 0 : _a.startsWith(argumentPath + '/'))
|
|
329
|
+
? [issue.path.slice(argumentPath.length)] : [];
|
|
330
|
+
}).concat(Object.keys(((_a = entry.invalidNumbers) !== null && _a !== void 0 ? _a : {}))))];
|
|
331
|
+
const sourceButton = React.createElement("button", { type: "button", "aria-pressed": entry.mode === 'source', onClick: () => {
|
|
332
|
+
try {
|
|
333
|
+
if (entry.invalidForm === true)
|
|
334
|
+
throw new TypeError();
|
|
335
|
+
const source = record(entry.creating)
|
|
336
|
+
? createSourceCandidate(entry.creating.format, entry.data)
|
|
337
|
+
: entry.mode === 'form' && document && Object.hasOwn(entry, 'data')
|
|
338
|
+
? patchDecodedSource(document, entry.data, source => readSourceDocument(binding, source)) : entry.source;
|
|
339
|
+
const next = { ...entry, mode: 'source', source, invalidForm: false, invalidNumbers: {} };
|
|
340
|
+
delete next.creating;
|
|
341
|
+
update(binding.argument, next);
|
|
342
|
+
}
|
|
343
|
+
catch (_a) {
|
|
344
|
+
context.setValidationIssues([{ message: 'This edit cannot preserve the source. Repair the Source explicitly.' }]);
|
|
345
|
+
}
|
|
346
|
+
} }, "Source");
|
|
347
|
+
const formButton = React.createElement("button", { type: "button", disabled: !variant, "aria-pressed": entry.mode === 'form', onClick: () => {
|
|
348
|
+
if (document || entry.creating)
|
|
349
|
+
update(binding.argument, { ...entry, mode: 'form', data: Object.hasOwn(entry, 'data') ? entry.data : document.data });
|
|
350
|
+
} }, "Form");
|
|
351
|
+
const creationTools = React.createElement(React.Fragment, null,
|
|
264
352
|
React.createElement("select", { "aria-label": `${binding.argument} New format and variant`, value: selectedCreation, onChange: e => selectCreateVariant({ ...creating, [binding.argument]: e.target.value }) },
|
|
265
353
|
React.createElement("option", { value: "" }, "Choose new format and variant"),
|
|
266
354
|
binding.presentationVariants.map(v => React.createElement("option", { key: JSON.stringify([v.format, v.variantId]), value: JSON.stringify([v.format, v.variantId]) },
|
|
@@ -273,46 +361,58 @@ function SourceView({ context, bindings }) {
|
|
|
273
361
|
const root = chosen.schemaBundle.root.document;
|
|
274
362
|
const type = record(root) && typeof root.type === 'string' ? root.type : 'null';
|
|
275
363
|
update(binding.argument, { mode: 'form', source: entry.source, sourceEdited: true, creating: { format, variantId }, data: initial(type) });
|
|
276
|
-
} }, "Create document (replace source)")
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
364
|
+
} }, "Create document (replace source)"));
|
|
365
|
+
const discard = entry.mode === 'form' && React.createElement("button", { type: "button", onClick: () => { var _a; return update(binding.argument, { mode: 'source', source: entry.source, sourceEdited: (_a = entry.sourceEdited) !== null && _a !== void 0 ? _a : false }); } }, "Discard form edits");
|
|
366
|
+
return React.createElement("fieldset", { key: binding.argument, disabled: disabled, className: commonLayout ? 'editor-form-binding' : undefined },
|
|
367
|
+
(!commonLayout || binding.label || bindings.length > 1) && React.createElement("legend", null, (_b = binding.label) !== null && _b !== void 0 ? _b : binding.argument),
|
|
368
|
+
commonLayout ? React.createElement("div", { className: "editor-form-toolbar" },
|
|
369
|
+
React.createElement("div", { className: "editor-form-modes" },
|
|
370
|
+
sourceButton,
|
|
371
|
+
formButton),
|
|
372
|
+
React.createElement("details", { className: "editor-form-tools" },
|
|
373
|
+
React.createElement("summary", null, "\u6587\u66F8\u306E\u64CD\u4F5C"),
|
|
374
|
+
creationTools,
|
|
375
|
+
discard)) : React.createElement(React.Fragment, null,
|
|
376
|
+
sourceButton,
|
|
377
|
+
creationTools,
|
|
378
|
+
formButton,
|
|
379
|
+
discard),
|
|
282
380
|
entry.mode === 'form' && variant && Object.hasOwn(entry, 'data')
|
|
283
|
-
? React.createElement(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
index
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
381
|
+
? React.createElement(SourcePaths.Provider, { value: errorPaths },
|
|
382
|
+
React.createElement(Tree, { invalidNumbers: ((_c = entry.invalidNumbers) !== null && _c !== void 0 ? _c : {}), value: entry.data, path: "", schema: variant.schemaBundle.root.document, ui: variant.uiSchema, variant: variant, change: (data, edit = { path: '' }) => {
|
|
383
|
+
var _a;
|
|
384
|
+
const pending = ((_a = entry.invalidNumbers) !== null && _a !== void 0 ? _a : {});
|
|
385
|
+
const invalidNumbers = {};
|
|
386
|
+
const prefix = `${edit.path}/`;
|
|
387
|
+
for (const [path, text] of Object.entries(pending)) {
|
|
388
|
+
let target = path;
|
|
389
|
+
if (edit.removeIndex !== undefined || edit.swap) {
|
|
390
|
+
if (path.startsWith(prefix)) {
|
|
391
|
+
const [head, ...tail] = path.slice(prefix.length).split('/');
|
|
392
|
+
let index = Number(head);
|
|
393
|
+
if (index === edit.removeIndex)
|
|
394
|
+
continue;
|
|
395
|
+
if (edit.removeIndex !== undefined && index > edit.removeIndex)
|
|
396
|
+
index--;
|
|
397
|
+
if (edit.swap)
|
|
398
|
+
index = index === edit.swap[0] ? edit.swap[1] : index === edit.swap[1] ? edit.swap[0] : index;
|
|
399
|
+
target = `${prefix}${index}${tail.length ? '/' + tail.join('/') : ''}`;
|
|
400
|
+
}
|
|
301
401
|
}
|
|
402
|
+
else if (path === edit.path || path.startsWith(prefix))
|
|
403
|
+
continue;
|
|
404
|
+
Object.defineProperty(invalidNumbers, target, { value: text, enumerable: true });
|
|
302
405
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
if (edit.invalidText !== undefined)
|
|
308
|
-
Object.defineProperty(invalidNumbers, edit.path, { value: edit.invalidText, enumerable: true });
|
|
309
|
-
update(binding.argument, { ...entry, data, invalidNumbers, invalidForm: Object.keys(invalidNumbers).length > 0 });
|
|
310
|
-
} })
|
|
406
|
+
if (edit.invalidText !== undefined)
|
|
407
|
+
Object.defineProperty(invalidNumbers, edit.path, { value: edit.invalidText, enumerable: true });
|
|
408
|
+
update(binding.argument, { ...entry, data, invalidNumbers, invalidForm: Object.keys(invalidNumbers).length > 0 });
|
|
409
|
+
} }))
|
|
311
410
|
: React.createElement("textarea", { "aria-label": `${binding.argument} Source`, value: entry.source, onChange: e => update(binding.argument, { mode: 'source', source: e.target.value, sourceEdited: true }) }),
|
|
312
411
|
entry.invalidForm === true && React.createElement("p", { role: "alert" }, "Enter a valid JSON number."));
|
|
313
412
|
}));
|
|
314
413
|
}
|
|
315
|
-
export function mountSourceView(container, context, bindings) {
|
|
316
|
-
ReactDOM.render(React.createElement(
|
|
414
|
+
export function mountSourceView(container, context, bindings, commonLayout = false) {
|
|
415
|
+
ReactDOM.render(React.createElement(LayoutHost, { enabled: commonLayout, context: context },
|
|
416
|
+
React.createElement(SourceView, { context: context, bindings: bindings, commonLayout: commonLayout })), container);
|
|
317
417
|
return () => { ReactDOM.unmountComponentAtNode(container); };
|
|
318
418
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export default readBlockEditorAppearance;
|
|
2
|
-
declare function readBlockEditorAppearance(block: any, ScratchBlocks: any): {
|
|
2
|
+
declare function readBlockEditorAppearance(block: any, ScratchBlocks: any, editableArguments: any): {
|
|
3
3
|
name: any;
|
|
4
4
|
colour: string;
|
|
5
5
|
tint: string;
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
// Presentation metadata stays outside snapshots, drafts and saved project data.
|
|
2
|
-
const readBlockEditorAppearance = (block, ScratchBlocks) => {
|
|
2
|
+
const readBlockEditorAppearance = (block, ScratchBlocks, editableArguments) => {
|
|
3
3
|
const appearance = {};
|
|
4
4
|
if (!block)
|
|
5
5
|
return appearance;
|
|
6
6
|
try {
|
|
7
|
-
//
|
|
7
|
+
// Registered editor inputs contain the document; other literal inputs identify the block.
|
|
8
8
|
const name = block.inputList.filter(input => input.type !== ScratchBlocks.NEXT_STATEMENT)
|
|
9
|
-
.map(input =>
|
|
10
|
-
.
|
|
11
|
-
|
|
9
|
+
.map(input => {
|
|
10
|
+
const label = input.fieldRow.filter(field => !(field instanceof ScratchBlocks.FieldImage))
|
|
11
|
+
.map(field => field.getText()).join(' ');
|
|
12
|
+
if (!editableArguments || editableArguments.includes(input.name) ||
|
|
13
|
+
input.type !== ScratchBlocks.INPUT_VALUE)
|
|
14
|
+
return label;
|
|
15
|
+
try {
|
|
16
|
+
const child = input.connection && input.connection.targetBlock();
|
|
17
|
+
// Read only literal shadows, never evaluate or traverse reporter expressions.
|
|
18
|
+
if (!child || !child.isShadow() || child.inputList.some(part => part.connection))
|
|
19
|
+
return label;
|
|
20
|
+
const value = child.inputList.flatMap(part => part.fieldRow)
|
|
21
|
+
.filter(field => !(field instanceof ScratchBlocks.FieldImage))
|
|
22
|
+
.map(field => field.getText()).join(' ').replace(/\s+/g, ' ').trim();
|
|
23
|
+
return value ? `${label} (${value})` : label;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// An unavailable identifier must not discard the block's own labels.
|
|
27
|
+
return label;
|
|
28
|
+
}
|
|
29
|
+
})
|
|
12
30
|
.join(' ')
|
|
13
31
|
.replace(/\s+/g, ' ')
|
|
14
32
|
.trim();
|
package/dist/turbowarp/bridge.js
CHANGED
|
@@ -80,6 +80,7 @@ const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label,
|
|
|
80
80
|
return snapshot && Object.values(snapshot.arguments).every(slot => slot.kind === 'literal');
|
|
81
81
|
};
|
|
82
82
|
const open = block => {
|
|
83
|
+
var _a, _b;
|
|
83
84
|
if (activeSession && ['editing', 'saving'].includes(activeSession.getState().status))
|
|
84
85
|
return;
|
|
85
86
|
const reference = { targetId: vm.editingTarget.id, blockId: block.id };
|
|
@@ -95,13 +96,13 @@ const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label,
|
|
|
95
96
|
openingFixedDeclaration = fixedDeclaration(service.registry, openingSnapshot) || null;
|
|
96
97
|
const labels = {};
|
|
97
98
|
const appearances = Object.create(null);
|
|
98
|
-
const rootAppearance = readBlockEditorAppearance(block, ScratchBlocks);
|
|
99
|
+
const rootAppearance = readBlockEditorAppearance(block, ScratchBlocks, (_a = service.registry.resolve(block.type)) === null || _a === void 0 ? void 0 : _a.editableArguments);
|
|
99
100
|
appearances[reference.blockId] = rootAppearance;
|
|
100
101
|
labels[reference.blockId] = rootAppearance.name || block.type;
|
|
101
102
|
if (typeof session.getNodes === 'function') {
|
|
102
103
|
for (const node of session.getNodes()) {
|
|
103
104
|
const source = workspace.getBlockById(node.reference.blockId);
|
|
104
|
-
const appearance = readBlockEditorAppearance(source, ScratchBlocks);
|
|
105
|
+
const appearance = readBlockEditorAppearance(source, ScratchBlocks, (_b = service.registry.resolve(node.opcode)) === null || _b === void 0 ? void 0 : _b.editableArguments);
|
|
105
106
|
appearances[node.reference.blockId] = appearance;
|
|
106
107
|
labels[node.reference.blockId] = appearance.name || node.opcode;
|
|
107
108
|
}
|
package/dist/turbowarp.d.ts
CHANGED
|
@@ -60,7 +60,8 @@ export declare function bindBlockEditorIcon(options: {
|
|
|
60
60
|
readonly canOpen: () => boolean;
|
|
61
61
|
readonly onOpen: () => void;
|
|
62
62
|
}): () => void;
|
|
63
|
-
|
|
63
|
+
/** Supply editor arguments to include other literal inputs as parenthesized identifiers. */
|
|
64
|
+
export declare function readBlockEditorAppearance(block: object | null, ScratchBlocks: object, editableArguments?: readonly string[]): TurboWarpNodeAppearance;
|
|
64
65
|
export declare function createBlockEditorErrorSession(reference: BlockReference, error: unknown): BlockEditorFailureSession;
|
|
65
66
|
export declare function openBlockEditorSession(options: {
|
|
66
67
|
readonly registry: EditorRegistry;
|
package/dist/turbowarp.js
CHANGED
|
@@ -29,8 +29,9 @@ export function createTurboWarpBlockEditorHost(options) {
|
|
|
29
29
|
export function bindBlockEditorIcon(options) {
|
|
30
30
|
return bindIcon(options);
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/** Supply editor arguments to include other literal inputs as parenthesized identifiers. */
|
|
33
|
+
export function readBlockEditorAppearance(block, ScratchBlocks, editableArguments) {
|
|
34
|
+
return readAppearance(block, ScratchBlocks, editableArguments);
|
|
34
35
|
}
|
|
35
36
|
export function createBlockEditorErrorSession(reference, error) {
|
|
36
37
|
return createErrorSession(reference, error);
|