@capsuletech/web-editor-state 0.1.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/dist/ids.d.ts +7 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +211 -0
- package/dist/index.mjs.map +1 -0
- package/dist/operations.d.ts +41 -0
- package/dist/package.json +20 -0
- package/dist/schema.d.ts +35 -0
- package/dist/types.d.ts +61 -0
- package/package.json +31 -0
- package/src/ids.ts +16 -0
- package/src/index.ts +23 -0
- package/src/operations.ts +228 -0
- package/src/schema.ts +133 -0
- package/src/types.ts +69 -0
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Короткий случайный id для нод в дереве. По умолчанию 10 символов —
|
|
3
|
+
* collision-rate приемлемая для редактора. Не зависит от crypto — работает
|
|
4
|
+
* в любой среде, включая SSR-сборку.
|
|
5
|
+
*/
|
|
6
|
+
export declare const generateId: (length?: number) => string;
|
|
7
|
+
export declare const ROOT_ID = "root";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { EditorOpError, addNode, createEmptyTree, moveNode, removeNode, reorderChildren, updateNode, } from './operations';
|
|
2
|
+
export { ROOT_ID, generateId } from './ids';
|
|
3
|
+
export { createEditorSchema } from './schema';
|
|
4
|
+
export type { ICreateEditorSchemaOptions } from './schema';
|
|
5
|
+
export type { IAddNodePayload, IEditorContext, IEditorNode, IEditorTree, IMoveNodePayload, IRemoveNodePayload, IReorderChildrenPayload, IUpdateNodePayload, NodeId, } from './types';
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { canAcceptChild as w, getManifest as I } from "@capsuletech/manifests";
|
|
2
|
+
const u = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", y = (n = 10) => {
|
|
3
|
+
let o = "";
|
|
4
|
+
for (let t = 0; t < n; t++)
|
|
5
|
+
o += u[Math.floor(Math.random() * u.length)];
|
|
6
|
+
return o;
|
|
7
|
+
}, f = "root";
|
|
8
|
+
class c extends Error {
|
|
9
|
+
constructor(o) {
|
|
10
|
+
super(o), this.name = "EditorOpError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const x = (n = "ui.Card") => ({
|
|
14
|
+
root: f,
|
|
15
|
+
nodes: {
|
|
16
|
+
[f]: {
|
|
17
|
+
id: f,
|
|
18
|
+
type: n,
|
|
19
|
+
parentId: null,
|
|
20
|
+
children: [],
|
|
21
|
+
props: {},
|
|
22
|
+
meta: {},
|
|
23
|
+
styles: {}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}), p = (n) => ({
|
|
27
|
+
...n,
|
|
28
|
+
children: [...n.children],
|
|
29
|
+
props: { ...n.props },
|
|
30
|
+
meta: { ...n.meta },
|
|
31
|
+
styles: { ...n.styles }
|
|
32
|
+
}), a = (n, o) => {
|
|
33
|
+
const t = n.nodes[o];
|
|
34
|
+
if (!t) throw new c(`node "${o}" not found`);
|
|
35
|
+
return t;
|
|
36
|
+
}, E = (n, o, t) => {
|
|
37
|
+
let e = t;
|
|
38
|
+
for (; e; ) {
|
|
39
|
+
if (e === o) return !0;
|
|
40
|
+
e = n.nodes[e]?.parentId ?? null;
|
|
41
|
+
}
|
|
42
|
+
return !1;
|
|
43
|
+
}, m = (n, o, t) => t === void 0 || t < 0 || t >= n.length ? [...n, o] : [...n.slice(0, t), o, ...n.slice(t)], N = (n, o) => {
|
|
44
|
+
const t = a(n, o.parentId);
|
|
45
|
+
if (!w(t.type, o.type))
|
|
46
|
+
throw new c(
|
|
47
|
+
`node type "${t.type}" не принимает "${o.type}" как ребёнка`
|
|
48
|
+
);
|
|
49
|
+
const e = I(o.type), s = y(), r = {
|
|
50
|
+
id: s,
|
|
51
|
+
type: o.type,
|
|
52
|
+
parentId: t.id,
|
|
53
|
+
children: [],
|
|
54
|
+
props: { ...e?.defaultProps ?? {}, ...o.props ?? {} },
|
|
55
|
+
meta: { ...o.meta ?? {} },
|
|
56
|
+
styles: {}
|
|
57
|
+
}, d = p(t);
|
|
58
|
+
return d.children = m(d.children, s, o.index), {
|
|
59
|
+
nodeId: s,
|
|
60
|
+
tree: {
|
|
61
|
+
...n,
|
|
62
|
+
nodes: {
|
|
63
|
+
...n.nodes,
|
|
64
|
+
[s]: r,
|
|
65
|
+
[t.id]: d
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}, P = (n, o) => {
|
|
70
|
+
if (o.nodeId === n.root)
|
|
71
|
+
throw new c("root cannot be moved");
|
|
72
|
+
const t = a(n, o.nodeId), e = a(n, o.newParentId);
|
|
73
|
+
if (E(n, o.nodeId, o.newParentId))
|
|
74
|
+
throw new c("cannot move a node into its own subtree");
|
|
75
|
+
if (!w(e.type, t.type))
|
|
76
|
+
throw new c(
|
|
77
|
+
`node type "${e.type}" не принимает "${t.type}" как ребёнка`
|
|
78
|
+
);
|
|
79
|
+
const s = a(n, t.parentId), r = p(s);
|
|
80
|
+
r.children = r.children.filter((h) => h !== t.id);
|
|
81
|
+
const d = s.id === e.id, l = d ? r : p(e);
|
|
82
|
+
l.children = m(l.children, t.id, o.index);
|
|
83
|
+
const i = p(t);
|
|
84
|
+
return i.parentId = e.id, {
|
|
85
|
+
...n,
|
|
86
|
+
nodes: {
|
|
87
|
+
...n.nodes,
|
|
88
|
+
[t.id]: i,
|
|
89
|
+
[s.id]: r,
|
|
90
|
+
...d ? {} : { [e.id]: l }
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}, g = (n, o) => {
|
|
94
|
+
if (o.nodeId === n.root)
|
|
95
|
+
throw new c("root cannot be removed");
|
|
96
|
+
const t = a(n, o.nodeId), e = a(n, t.parentId), s = /* @__PURE__ */ new Set(), r = [t.id];
|
|
97
|
+
for (; r.length; ) {
|
|
98
|
+
const i = r.pop();
|
|
99
|
+
if (s.has(i)) continue;
|
|
100
|
+
s.add(i);
|
|
101
|
+
const h = n.nodes[i];
|
|
102
|
+
h && r.push(...h.children);
|
|
103
|
+
}
|
|
104
|
+
const d = p(e);
|
|
105
|
+
d.children = d.children.filter((i) => i !== t.id);
|
|
106
|
+
const l = {};
|
|
107
|
+
for (const [i, h] of Object.entries(n.nodes))
|
|
108
|
+
s.has(i) || (l[i] = h);
|
|
109
|
+
return l[e.id] = d, { ...n, nodes: l };
|
|
110
|
+
}, O = (n, o) => {
|
|
111
|
+
const t = a(n, o.nodeId), e = p(t);
|
|
112
|
+
return o.patch.props && (e.props = { ...e.props, ...o.patch.props }), o.patch.meta && (e.meta = { ...e.meta, ...o.patch.meta }), o.patch.styles && (e.styles = { ...e.styles, ...o.patch.styles }), {
|
|
113
|
+
...n,
|
|
114
|
+
nodes: { ...n.nodes, [t.id]: e }
|
|
115
|
+
};
|
|
116
|
+
}, v = (n, o) => {
|
|
117
|
+
const t = a(n, o.parentId), e = new Set(t.children), s = new Set(o.newOrder);
|
|
118
|
+
if (e.size !== s.size || [...e].some((d) => !s.has(d)))
|
|
119
|
+
throw new c(
|
|
120
|
+
"reorderChildren: newOrder должен содержать ровно тех же детей что и parent"
|
|
121
|
+
);
|
|
122
|
+
const r = p(t);
|
|
123
|
+
return r.children = [...o.newOrder], { ...n, nodes: { ...n.nodes, [t.id]: r } };
|
|
124
|
+
}, S = (n = {}) => ({
|
|
125
|
+
initial: "idle",
|
|
126
|
+
context: {
|
|
127
|
+
tree: n.initialTree ?? x(n.rootType),
|
|
128
|
+
selectedId: null
|
|
129
|
+
},
|
|
130
|
+
states: {
|
|
131
|
+
idle: {
|
|
132
|
+
addNode: ({ target: t, store: e }) => {
|
|
133
|
+
const s = t.payload;
|
|
134
|
+
try {
|
|
135
|
+
const { tree: r, nodeId: d } = N(e.ctx.tree, s);
|
|
136
|
+
e.update({ tree: r, selectedId: d });
|
|
137
|
+
} catch (r) {
|
|
138
|
+
if (r instanceof c)
|
|
139
|
+
e.setErrors({ editor: r.message });
|
|
140
|
+
else
|
|
141
|
+
throw r;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
moveNode: ({ target: t, store: e }) => {
|
|
145
|
+
const s = t.payload;
|
|
146
|
+
try {
|
|
147
|
+
const r = P(e.ctx.tree, s);
|
|
148
|
+
e.update({ tree: r });
|
|
149
|
+
} catch (r) {
|
|
150
|
+
if (r instanceof c)
|
|
151
|
+
e.setErrors({ editor: r.message });
|
|
152
|
+
else
|
|
153
|
+
throw r;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
removeNode: ({ target: t, store: e }) => {
|
|
157
|
+
const s = t.payload;
|
|
158
|
+
try {
|
|
159
|
+
const r = g(e.ctx.tree, s), d = e.ctx.selectedId === s.nodeId ? null : e.ctx.selectedId;
|
|
160
|
+
e.update({ tree: r, selectedId: d });
|
|
161
|
+
} catch (r) {
|
|
162
|
+
if (r instanceof c)
|
|
163
|
+
e.setErrors({ editor: r.message });
|
|
164
|
+
else
|
|
165
|
+
throw r;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
updateNode: ({ target: t, store: e }) => {
|
|
169
|
+
const s = t.payload;
|
|
170
|
+
try {
|
|
171
|
+
const r = O(e.ctx.tree, s);
|
|
172
|
+
e.update({ tree: r });
|
|
173
|
+
} catch (r) {
|
|
174
|
+
if (r instanceof c)
|
|
175
|
+
e.setErrors({ editor: r.message });
|
|
176
|
+
else
|
|
177
|
+
throw r;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
reorderChildren: ({ target: t, store: e }) => {
|
|
181
|
+
const s = t.payload;
|
|
182
|
+
try {
|
|
183
|
+
const r = v(e.ctx.tree, s);
|
|
184
|
+
e.update({ tree: r });
|
|
185
|
+
} catch (r) {
|
|
186
|
+
if (r instanceof c)
|
|
187
|
+
e.setErrors({ editor: r.message });
|
|
188
|
+
else
|
|
189
|
+
throw r;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
selectNode: ({ target: t, store: e }) => {
|
|
193
|
+
const s = t.payload;
|
|
194
|
+
e.update({ selectedId: s });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
export {
|
|
200
|
+
c as EditorOpError,
|
|
201
|
+
f as ROOT_ID,
|
|
202
|
+
N as addNode,
|
|
203
|
+
S as createEditorSchema,
|
|
204
|
+
x as createEmptyTree,
|
|
205
|
+
y as generateId,
|
|
206
|
+
P as moveNode,
|
|
207
|
+
g as removeNode,
|
|
208
|
+
v as reorderChildren,
|
|
209
|
+
O as updateNode
|
|
210
|
+
};
|
|
211
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/ids.ts","../src/operations.ts","../src/schema.ts"],"sourcesContent":["const ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';\n\n/**\n * Короткий случайный id для нод в дереве. По умолчанию 10 символов —\n * collision-rate приемлемая для редактора. Не зависит от crypto — работает\n * в любой среде, включая SSR-сборку.\n */\nexport const generateId = (length = 10): string => {\n let id = '';\n for (let i = 0; i < length; i++) {\n id += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];\n }\n return id;\n};\n\nexport const ROOT_ID = 'root';\n","import { canAcceptChild, getManifest } from '@capsuletech/manifests';\nimport { ROOT_ID, generateId } from './ids';\nimport type {\n IAddNodePayload,\n IEditorNode,\n IEditorTree,\n IMoveNodePayload,\n IRemoveNodePayload,\n IReorderChildrenPayload,\n IUpdateNodePayload,\n NodeId,\n} from './types';\n\n/** Ошибка операции с понятным сообщением — Feature/Controller сможет показать юзеру. */\nexport class EditorOpError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'EditorOpError';\n }\n}\n\n/** Пустое дерево с одним корнем (`Wrapper` или указанный тип). */\nexport const createEmptyTree = (rootType = 'ui.Card'): IEditorTree => ({\n root: ROOT_ID,\n nodes: {\n [ROOT_ID]: {\n id: ROOT_ID,\n type: rootType,\n parentId: null,\n children: [],\n props: {},\n meta: {},\n styles: {},\n },\n },\n});\n\nconst cloneNode = (n: IEditorNode): IEditorNode => ({\n ...n,\n children: [...n.children],\n props: { ...n.props },\n meta: { ...n.meta },\n styles: { ...n.styles },\n});\n\nconst requireNode = (tree: IEditorTree, id: NodeId): IEditorNode => {\n const n = tree.nodes[id];\n if (!n) throw new EditorOpError(`node \"${id}\" not found`);\n return n;\n};\n\n/** Глубже ли `descendantId` под `ancestorId`. Включая равенство. */\nconst isDescendantOrSelf = (\n tree: IEditorTree,\n ancestorId: NodeId,\n descendantId: NodeId,\n): boolean => {\n let cur: NodeId | null = descendantId;\n while (cur) {\n if (cur === ancestorId) return true;\n cur = tree.nodes[cur]?.parentId ?? null;\n }\n return false;\n};\n\nconst insertAt = <T>(arr: T[], item: T, index?: number): T[] => {\n if (index === undefined || index < 0 || index >= arr.length) return [...arr, item];\n return [...arr.slice(0, index), item, ...arr.slice(index)];\n};\n\n/**\n * Добавить новую ноду к `parentId`. Валидирует:\n * - parent существует;\n * - parent не leaf (через манифест);\n * - parent.accepts(type) (через манифест).\n *\n * Возвращает новое дерево + id созданной ноды.\n */\nexport const addNode = (\n tree: IEditorTree,\n payload: IAddNodePayload,\n): { tree: IEditorTree; nodeId: NodeId } => {\n const parent = requireNode(tree, payload.parentId);\n if (!canAcceptChild(parent.type, payload.type)) {\n throw new EditorOpError(\n `node type \"${parent.type}\" не принимает \"${payload.type}\" как ребёнка`,\n );\n }\n const manifest = getManifest(payload.type);\n const id = generateId();\n const node: IEditorNode = {\n id,\n type: payload.type,\n parentId: parent.id,\n children: [],\n props: { ...(manifest?.defaultProps ?? {}), ...(payload.props ?? {}) },\n meta: { ...(payload.meta ?? {}) },\n styles: {},\n };\n const nextParent = cloneNode(parent);\n nextParent.children = insertAt(nextParent.children, id, payload.index);\n return {\n nodeId: id,\n tree: {\n ...tree,\n nodes: {\n ...tree.nodes,\n [id]: node,\n [parent.id]: nextParent,\n },\n },\n };\n};\n\n/**\n * Переместить ноду. Запрещает:\n * - перемещать root;\n * - перемещать ноду внутрь себя/своих потомков;\n * - перемещать в leaf или в parent, который не принимает этот тип.\n */\nexport const moveNode = (tree: IEditorTree, payload: IMoveNodePayload): IEditorTree => {\n if (payload.nodeId === tree.root) {\n throw new EditorOpError('root cannot be moved');\n }\n const node = requireNode(tree, payload.nodeId);\n const newParent = requireNode(tree, payload.newParentId);\n if (isDescendantOrSelf(tree, payload.nodeId, payload.newParentId)) {\n throw new EditorOpError('cannot move a node into its own subtree');\n }\n if (!canAcceptChild(newParent.type, node.type)) {\n throw new EditorOpError(\n `node type \"${newParent.type}\" не принимает \"${node.type}\" как ребёнка`,\n );\n }\n const oldParent = requireNode(tree, node.parentId!);\n const nextOldParent = cloneNode(oldParent);\n nextOldParent.children = nextOldParent.children.filter((c) => c !== node.id);\n\n // Если parent тот же — сначала удаляем из старого, потом вставляем в новый\n // (правильный индекс может «съехать» при том же родителе — calculate accordingly).\n const isSameParent = oldParent.id === newParent.id;\n const targetParent = isSameParent ? nextOldParent : cloneNode(newParent);\n targetParent.children = insertAt(targetParent.children, node.id, payload.index);\n\n const nextNode = cloneNode(node);\n nextNode.parentId = newParent.id;\n\n return {\n ...tree,\n nodes: {\n ...tree.nodes,\n [node.id]: nextNode,\n [oldParent.id]: nextOldParent,\n ...(isSameParent ? {} : { [newParent.id]: targetParent }),\n },\n };\n};\n\n/**\n * Удалить ноду и весь её subtree. Root удалить нельзя.\n */\nexport const removeNode = (tree: IEditorTree, payload: IRemoveNodePayload): IEditorTree => {\n if (payload.nodeId === tree.root) {\n throw new EditorOpError('root cannot be removed');\n }\n const node = requireNode(tree, payload.nodeId);\n const parent = requireNode(tree, node.parentId!);\n\n // Собираем все ноды subtree для удаления\n const toDelete = new Set<NodeId>();\n const stack: NodeId[] = [node.id];\n while (stack.length) {\n const id = stack.pop()!;\n if (toDelete.has(id)) continue;\n toDelete.add(id);\n const n = tree.nodes[id];\n if (n) stack.push(...n.children);\n }\n\n const nextParent = cloneNode(parent);\n nextParent.children = nextParent.children.filter((c) => c !== node.id);\n\n const nextNodes: Record<NodeId, IEditorNode> = {};\n for (const [id, n] of Object.entries(tree.nodes)) {\n if (!toDelete.has(id)) nextNodes[id] = n;\n }\n nextNodes[parent.id] = nextParent;\n\n return { ...tree, nodes: nextNodes };\n};\n\n/**\n * Patch props/meta/styles конкретной ноды. Не валидирует props против\n * `propsSchema` — это работа инспектора при вводе. Тут только мердж.\n */\nexport const updateNode = (tree: IEditorTree, payload: IUpdateNodePayload): IEditorTree => {\n const node = requireNode(tree, payload.nodeId);\n const next = cloneNode(node);\n if (payload.patch.props) next.props = { ...next.props, ...payload.patch.props };\n if (payload.patch.meta) next.meta = { ...next.meta, ...payload.patch.meta };\n if (payload.patch.styles) next.styles = { ...next.styles, ...payload.patch.styles };\n return {\n ...tree,\n nodes: { ...tree.nodes, [node.id]: next },\n };\n};\n\n/**\n * Переупорядочить детей. `newOrder` должен содержать **те же id**, что и\n * текущие children parent'а — иначе ошибка (это симптом несогласованного\n * обновления и проще упасть, чем тихо потерять/добавить ноды).\n */\nexport const reorderChildren = (\n tree: IEditorTree,\n payload: IReorderChildrenPayload,\n): IEditorTree => {\n const parent = requireNode(tree, payload.parentId);\n const current = new Set(parent.children);\n const next = new Set(payload.newOrder);\n if (current.size !== next.size || [...current].some((id) => !next.has(id))) {\n throw new EditorOpError(\n 'reorderChildren: newOrder должен содержать ровно тех же детей что и parent',\n );\n }\n const nextParent = cloneNode(parent);\n nextParent.children = [...payload.newOrder];\n return { ...tree, nodes: { ...tree.nodes, [parent.id]: nextParent } };\n};\n","import {\n EditorOpError,\n addNode,\n createEmptyTree,\n moveNode,\n removeNode,\n reorderChildren,\n updateNode,\n} from './operations';\nimport type {\n IAddNodePayload,\n IEditorContext,\n IEditorTree,\n IMoveNodePayload,\n IRemoveNodePayload,\n IReorderChildrenPayload,\n IUpdateNodePayload,\n NodeId,\n} from './types';\n\nexport interface ICreateEditorSchemaOptions {\n /** Стартовое дерево. По умолчанию пустое с корнем `ui.Card`. */\n initialTree?: IEditorTree;\n rootType?: string;\n}\n\n/**\n * Возвращает HCA-схему (для `Feature(...)`) с одним стейтом `idle` и набором\n * методов-обработчиков. Каждый метод — pure-операция + commit через\n * `store.update({ tree, selectedId })`.\n *\n * Обёртка над пакетом-операциями — нужна чтобы внутри editor-app можно было:\n * ```ts\n * import { createEditorSchema } from '@capsuletech/editor-state';\n * const State = Feature(() => createEditorSchema());\n * export default State;\n * ```\n *\n * Методы вызываются через `next('addNode')` из дочернего Controller'а с\n * payload в `target.payload`.\n */\nexport const createEditorSchema = (options: ICreateEditorSchemaOptions = {}) => {\n const initialContext: IEditorContext = {\n tree: options.initialTree ?? createEmptyTree(options.rootType),\n selectedId: null,\n };\n\n return {\n initial: 'idle',\n context: initialContext,\n states: {\n idle: {\n addNode: ({ target, store }: any) => {\n const payload = target.payload as IAddNodePayload;\n try {\n const { tree, nodeId } = addNode(store.ctx.tree, payload);\n store.update({ tree, selectedId: nodeId });\n } catch (err) {\n if (err instanceof EditorOpError) {\n store.setErrors({ editor: err.message });\n } else {\n throw err;\n }\n }\n },\n\n moveNode: ({ target, store }: any) => {\n const payload = target.payload as IMoveNodePayload;\n try {\n const tree = moveNode(store.ctx.tree, payload);\n store.update({ tree });\n } catch (err) {\n if (err instanceof EditorOpError) {\n store.setErrors({ editor: err.message });\n } else {\n throw err;\n }\n }\n },\n\n removeNode: ({ target, store }: any) => {\n const payload = target.payload as IRemoveNodePayload;\n try {\n const tree = removeNode(store.ctx.tree, payload);\n // Если удаляли selected — сбрасываем selection\n const selectedId =\n store.ctx.selectedId === payload.nodeId ? null : store.ctx.selectedId;\n store.update({ tree, selectedId });\n } catch (err) {\n if (err instanceof EditorOpError) {\n store.setErrors({ editor: err.message });\n } else {\n throw err;\n }\n }\n },\n\n updateNode: ({ target, store }: any) => {\n const payload = target.payload as IUpdateNodePayload;\n try {\n const tree = updateNode(store.ctx.tree, payload);\n store.update({ tree });\n } catch (err) {\n if (err instanceof EditorOpError) {\n store.setErrors({ editor: err.message });\n } else {\n throw err;\n }\n }\n },\n\n reorderChildren: ({ target, store }: any) => {\n const payload = target.payload as IReorderChildrenPayload;\n try {\n const tree = reorderChildren(store.ctx.tree, payload);\n store.update({ tree });\n } catch (err) {\n if (err instanceof EditorOpError) {\n store.setErrors({ editor: err.message });\n } else {\n throw err;\n }\n }\n },\n\n selectNode: ({ target, store }: any) => {\n const id = target.payload as NodeId | null;\n store.update({ selectedId: id });\n },\n },\n },\n };\n};\n"],"names":["ALPHABET","generateId","length","id","i","ROOT_ID","EditorOpError","message","createEmptyTree","rootType","cloneNode","requireNode","tree","n","isDescendantOrSelf","ancestorId","descendantId","cur","insertAt","arr","item","index","addNode","payload","parent","canAcceptChild","manifest","getManifest","node","nextParent","moveNode","newParent","oldParent","nextOldParent","c","isSameParent","targetParent","nextNode","removeNode","toDelete","stack","nextNodes","updateNode","next","reorderChildren","current","createEditorSchema","options","target","store","nodeId","err","selectedId"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IAddNodePayload, IEditorTree, IMoveNodePayload, IRemoveNodePayload, IReorderChildrenPayload, IUpdateNodePayload, NodeId } from './types';
|
|
2
|
+
/** Ошибка операции с понятным сообщением — Feature/Controller сможет показать юзеру. */
|
|
3
|
+
export declare class EditorOpError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
/** Пустое дерево с одним корнем (`Wrapper` или указанный тип). */
|
|
7
|
+
export declare const createEmptyTree: (rootType?: string) => IEditorTree;
|
|
8
|
+
/**
|
|
9
|
+
* Добавить новую ноду к `parentId`. Валидирует:
|
|
10
|
+
* - parent существует;
|
|
11
|
+
* - parent не leaf (через манифест);
|
|
12
|
+
* - parent.accepts(type) (через манифест).
|
|
13
|
+
*
|
|
14
|
+
* Возвращает новое дерево + id созданной ноды.
|
|
15
|
+
*/
|
|
16
|
+
export declare const addNode: (tree: IEditorTree, payload: IAddNodePayload) => {
|
|
17
|
+
tree: IEditorTree;
|
|
18
|
+
nodeId: NodeId;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Переместить ноду. Запрещает:
|
|
22
|
+
* - перемещать root;
|
|
23
|
+
* - перемещать ноду внутрь себя/своих потомков;
|
|
24
|
+
* - перемещать в leaf или в parent, который не принимает этот тип.
|
|
25
|
+
*/
|
|
26
|
+
export declare const moveNode: (tree: IEditorTree, payload: IMoveNodePayload) => IEditorTree;
|
|
27
|
+
/**
|
|
28
|
+
* Удалить ноду и весь её subtree. Root удалить нельзя.
|
|
29
|
+
*/
|
|
30
|
+
export declare const removeNode: (tree: IEditorTree, payload: IRemoveNodePayload) => IEditorTree;
|
|
31
|
+
/**
|
|
32
|
+
* Patch props/meta/styles конкретной ноды. Не валидирует props против
|
|
33
|
+
* `propsSchema` — это работа инспектора при вводе. Тут только мердж.
|
|
34
|
+
*/
|
|
35
|
+
export declare const updateNode: (tree: IEditorTree, payload: IUpdateNodePayload) => IEditorTree;
|
|
36
|
+
/**
|
|
37
|
+
* Переупорядочить детей. `newOrder` должен содержать **те же id**, что и
|
|
38
|
+
* текущие children parent'а — иначе ошибка (это симптом несогласованного
|
|
39
|
+
* обновления и проще упасть, чем тихо потерять/добавить ноды).
|
|
40
|
+
*/
|
|
41
|
+
export declare const reorderChildren: (tree: IEditorTree, payload: IReorderChildrenPayload) => IEditorTree;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capsuletech/web-editor-state",
|
|
3
|
+
"version": "0.0.17",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.mjs",
|
|
6
|
+
"types": "./index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"import": "./index.mjs",
|
|
11
|
+
"default": "./index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@capsuletech/web-manifests": "workspace:*"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"solid-js": "^1.9.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { IEditorContext, IEditorTree } from './types';
|
|
2
|
+
export interface ICreateEditorSchemaOptions {
|
|
3
|
+
/** Стартовое дерево. По умолчанию пустое с корнем `ui.Card`. */
|
|
4
|
+
initialTree?: IEditorTree;
|
|
5
|
+
rootType?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Возвращает HCA-схему (для `Feature(...)`) с одним стейтом `idle` и набором
|
|
9
|
+
* методов-обработчиков. Каждый метод — pure-операция + commit через
|
|
10
|
+
* `store.update({ tree, selectedId })`.
|
|
11
|
+
*
|
|
12
|
+
* Обёртка над пакетом-операциями — нужна чтобы внутри editor-app можно было:
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { createEditorSchema } from '@capsuletech/editor-state';
|
|
15
|
+
* const State = Feature(() => createEditorSchema());
|
|
16
|
+
* export default State;
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Методы вызываются через `next('addNode')` из дочернего Controller'а с
|
|
20
|
+
* payload в `target.payload`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const createEditorSchema: (options?: ICreateEditorSchemaOptions) => {
|
|
23
|
+
initial: string;
|
|
24
|
+
context: IEditorContext;
|
|
25
|
+
states: {
|
|
26
|
+
idle: {
|
|
27
|
+
addNode: ({ target, store }: any) => void;
|
|
28
|
+
moveNode: ({ target, store }: any) => void;
|
|
29
|
+
removeNode: ({ target, store }: any) => void;
|
|
30
|
+
updateNode: ({ target, store }: any) => void;
|
|
31
|
+
reorderChildren: ({ target, store }: any) => void;
|
|
32
|
+
selectNode: ({ target, store }: any) => void;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type NodeId = string;
|
|
2
|
+
/**
|
|
3
|
+
* Узел дерева редактора. Полностью описывает один JSX-узел в формате,
|
|
4
|
+
* совместимом с `@capsuletech/renderer` (`ISchema.components.nodes[id]`).
|
|
5
|
+
*
|
|
6
|
+
* Никакого UI-state'а (selected/expanded/etc.) — это отдельный концерн
|
|
7
|
+
* редактора и хранится вне дерева.
|
|
8
|
+
*/
|
|
9
|
+
export interface IEditorNode {
|
|
10
|
+
id: NodeId;
|
|
11
|
+
/** Dot-path в registry, напр. `'ui.Button'`. */
|
|
12
|
+
type: string;
|
|
13
|
+
parentId: NodeId | null;
|
|
14
|
+
/** Порядок имеет значение — массив, а не Set. */
|
|
15
|
+
children: NodeId[];
|
|
16
|
+
props: Record<string, unknown>;
|
|
17
|
+
meta: Record<string, unknown>;
|
|
18
|
+
styles: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export interface IEditorTree {
|
|
21
|
+
root: NodeId;
|
|
22
|
+
nodes: Record<NodeId, IEditorNode>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Полный контекст редактора. Кроме дерева — UI-state (выбранная нода).
|
|
26
|
+
* Лежит в XState `context.data` через бридж.
|
|
27
|
+
*/
|
|
28
|
+
export interface IEditorContext {
|
|
29
|
+
tree: IEditorTree;
|
|
30
|
+
selectedId: NodeId | null;
|
|
31
|
+
}
|
|
32
|
+
/** Payload'ы для операций — типизированы отдельно для использования в Feature handlers. */
|
|
33
|
+
export interface IAddNodePayload {
|
|
34
|
+
type: string;
|
|
35
|
+
parentId: NodeId;
|
|
36
|
+
/** Куда вставить среди детей родителя. По умолчанию — в конец. */
|
|
37
|
+
index?: number;
|
|
38
|
+
/** Опциональный override дефолтных пропсов из манифеста. */
|
|
39
|
+
props?: Record<string, unknown>;
|
|
40
|
+
meta?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface IMoveNodePayload {
|
|
43
|
+
nodeId: NodeId;
|
|
44
|
+
newParentId: NodeId;
|
|
45
|
+
index?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface IRemoveNodePayload {
|
|
48
|
+
nodeId: NodeId;
|
|
49
|
+
}
|
|
50
|
+
export interface IUpdateNodePayload {
|
|
51
|
+
nodeId: NodeId;
|
|
52
|
+
patch: {
|
|
53
|
+
props?: Record<string, unknown>;
|
|
54
|
+
meta?: Record<string, unknown>;
|
|
55
|
+
styles?: Record<string, string>;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export interface IReorderChildrenPayload {
|
|
59
|
+
parentId: NodeId;
|
|
60
|
+
newOrder: NodeId[];
|
|
61
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capsuletech/web-editor-state",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"!**/*.tsbuildinfo"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@capsuletech/web-manifests": "0.1.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"solid-js": "^1.9.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@capsuletech/shared-vite": "0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "vite build"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/ids.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Короткий случайный id для нод в дереве. По умолчанию 10 символов —
|
|
5
|
+
* collision-rate приемлемая для редактора. Не зависит от crypto — работает
|
|
6
|
+
* в любой среде, включая SSR-сборку.
|
|
7
|
+
*/
|
|
8
|
+
export const generateId = (length = 10): string => {
|
|
9
|
+
let id = '';
|
|
10
|
+
for (let i = 0; i < length; i++) {
|
|
11
|
+
id += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
|
|
12
|
+
}
|
|
13
|
+
return id;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ROOT_ID = 'root';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
EditorOpError,
|
|
3
|
+
addNode,
|
|
4
|
+
createEmptyTree,
|
|
5
|
+
moveNode,
|
|
6
|
+
removeNode,
|
|
7
|
+
reorderChildren,
|
|
8
|
+
updateNode,
|
|
9
|
+
} from './operations';
|
|
10
|
+
export { ROOT_ID, generateId } from './ids';
|
|
11
|
+
export { createEditorSchema } from './schema';
|
|
12
|
+
export type { ICreateEditorSchemaOptions } from './schema';
|
|
13
|
+
export type {
|
|
14
|
+
IAddNodePayload,
|
|
15
|
+
IEditorContext,
|
|
16
|
+
IEditorNode,
|
|
17
|
+
IEditorTree,
|
|
18
|
+
IMoveNodePayload,
|
|
19
|
+
IRemoveNodePayload,
|
|
20
|
+
IReorderChildrenPayload,
|
|
21
|
+
IUpdateNodePayload,
|
|
22
|
+
NodeId,
|
|
23
|
+
} from './types';
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { canAcceptChild, getManifest } from '@capsuletech/manifests';
|
|
2
|
+
import { ROOT_ID, generateId } from './ids';
|
|
3
|
+
import type {
|
|
4
|
+
IAddNodePayload,
|
|
5
|
+
IEditorNode,
|
|
6
|
+
IEditorTree,
|
|
7
|
+
IMoveNodePayload,
|
|
8
|
+
IRemoveNodePayload,
|
|
9
|
+
IReorderChildrenPayload,
|
|
10
|
+
IUpdateNodePayload,
|
|
11
|
+
NodeId,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
/** Ошибка операции с понятным сообщением — Feature/Controller сможет показать юзеру. */
|
|
15
|
+
export class EditorOpError extends Error {
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'EditorOpError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Пустое дерево с одним корнем (`Wrapper` или указанный тип). */
|
|
23
|
+
export const createEmptyTree = (rootType = 'ui.Card'): IEditorTree => ({
|
|
24
|
+
root: ROOT_ID,
|
|
25
|
+
nodes: {
|
|
26
|
+
[ROOT_ID]: {
|
|
27
|
+
id: ROOT_ID,
|
|
28
|
+
type: rootType,
|
|
29
|
+
parentId: null,
|
|
30
|
+
children: [],
|
|
31
|
+
props: {},
|
|
32
|
+
meta: {},
|
|
33
|
+
styles: {},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const cloneNode = (n: IEditorNode): IEditorNode => ({
|
|
39
|
+
...n,
|
|
40
|
+
children: [...n.children],
|
|
41
|
+
props: { ...n.props },
|
|
42
|
+
meta: { ...n.meta },
|
|
43
|
+
styles: { ...n.styles },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const requireNode = (tree: IEditorTree, id: NodeId): IEditorNode => {
|
|
47
|
+
const n = tree.nodes[id];
|
|
48
|
+
if (!n) throw new EditorOpError(`node "${id}" not found`);
|
|
49
|
+
return n;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Глубже ли `descendantId` под `ancestorId`. Включая равенство. */
|
|
53
|
+
const isDescendantOrSelf = (
|
|
54
|
+
tree: IEditorTree,
|
|
55
|
+
ancestorId: NodeId,
|
|
56
|
+
descendantId: NodeId,
|
|
57
|
+
): boolean => {
|
|
58
|
+
let cur: NodeId | null = descendantId;
|
|
59
|
+
while (cur) {
|
|
60
|
+
if (cur === ancestorId) return true;
|
|
61
|
+
cur = tree.nodes[cur]?.parentId ?? null;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const insertAt = <T>(arr: T[], item: T, index?: number): T[] => {
|
|
67
|
+
if (index === undefined || index < 0 || index >= arr.length) return [...arr, item];
|
|
68
|
+
return [...arr.slice(0, index), item, ...arr.slice(index)];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Добавить новую ноду к `parentId`. Валидирует:
|
|
73
|
+
* - parent существует;
|
|
74
|
+
* - parent не leaf (через манифест);
|
|
75
|
+
* - parent.accepts(type) (через манифест).
|
|
76
|
+
*
|
|
77
|
+
* Возвращает новое дерево + id созданной ноды.
|
|
78
|
+
*/
|
|
79
|
+
export const addNode = (
|
|
80
|
+
tree: IEditorTree,
|
|
81
|
+
payload: IAddNodePayload,
|
|
82
|
+
): { tree: IEditorTree; nodeId: NodeId } => {
|
|
83
|
+
const parent = requireNode(tree, payload.parentId);
|
|
84
|
+
if (!canAcceptChild(parent.type, payload.type)) {
|
|
85
|
+
throw new EditorOpError(
|
|
86
|
+
`node type "${parent.type}" не принимает "${payload.type}" как ребёнка`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const manifest = getManifest(payload.type);
|
|
90
|
+
const id = generateId();
|
|
91
|
+
const node: IEditorNode = {
|
|
92
|
+
id,
|
|
93
|
+
type: payload.type,
|
|
94
|
+
parentId: parent.id,
|
|
95
|
+
children: [],
|
|
96
|
+
props: { ...(manifest?.defaultProps ?? {}), ...(payload.props ?? {}) },
|
|
97
|
+
meta: { ...(payload.meta ?? {}) },
|
|
98
|
+
styles: {},
|
|
99
|
+
};
|
|
100
|
+
const nextParent = cloneNode(parent);
|
|
101
|
+
nextParent.children = insertAt(nextParent.children, id, payload.index);
|
|
102
|
+
return {
|
|
103
|
+
nodeId: id,
|
|
104
|
+
tree: {
|
|
105
|
+
...tree,
|
|
106
|
+
nodes: {
|
|
107
|
+
...tree.nodes,
|
|
108
|
+
[id]: node,
|
|
109
|
+
[parent.id]: nextParent,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Переместить ноду. Запрещает:
|
|
117
|
+
* - перемещать root;
|
|
118
|
+
* - перемещать ноду внутрь себя/своих потомков;
|
|
119
|
+
* - перемещать в leaf или в parent, который не принимает этот тип.
|
|
120
|
+
*/
|
|
121
|
+
export const moveNode = (tree: IEditorTree, payload: IMoveNodePayload): IEditorTree => {
|
|
122
|
+
if (payload.nodeId === tree.root) {
|
|
123
|
+
throw new EditorOpError('root cannot be moved');
|
|
124
|
+
}
|
|
125
|
+
const node = requireNode(tree, payload.nodeId);
|
|
126
|
+
const newParent = requireNode(tree, payload.newParentId);
|
|
127
|
+
if (isDescendantOrSelf(tree, payload.nodeId, payload.newParentId)) {
|
|
128
|
+
throw new EditorOpError('cannot move a node into its own subtree');
|
|
129
|
+
}
|
|
130
|
+
if (!canAcceptChild(newParent.type, node.type)) {
|
|
131
|
+
throw new EditorOpError(
|
|
132
|
+
`node type "${newParent.type}" не принимает "${node.type}" как ребёнка`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const oldParent = requireNode(tree, node.parentId!);
|
|
136
|
+
const nextOldParent = cloneNode(oldParent);
|
|
137
|
+
nextOldParent.children = nextOldParent.children.filter((c) => c !== node.id);
|
|
138
|
+
|
|
139
|
+
// Если parent тот же — сначала удаляем из старого, потом вставляем в новый
|
|
140
|
+
// (правильный индекс может «съехать» при том же родителе — calculate accordingly).
|
|
141
|
+
const isSameParent = oldParent.id === newParent.id;
|
|
142
|
+
const targetParent = isSameParent ? nextOldParent : cloneNode(newParent);
|
|
143
|
+
targetParent.children = insertAt(targetParent.children, node.id, payload.index);
|
|
144
|
+
|
|
145
|
+
const nextNode = cloneNode(node);
|
|
146
|
+
nextNode.parentId = newParent.id;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
...tree,
|
|
150
|
+
nodes: {
|
|
151
|
+
...tree.nodes,
|
|
152
|
+
[node.id]: nextNode,
|
|
153
|
+
[oldParent.id]: nextOldParent,
|
|
154
|
+
...(isSameParent ? {} : { [newParent.id]: targetParent }),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Удалить ноду и весь её subtree. Root удалить нельзя.
|
|
161
|
+
*/
|
|
162
|
+
export const removeNode = (tree: IEditorTree, payload: IRemoveNodePayload): IEditorTree => {
|
|
163
|
+
if (payload.nodeId === tree.root) {
|
|
164
|
+
throw new EditorOpError('root cannot be removed');
|
|
165
|
+
}
|
|
166
|
+
const node = requireNode(tree, payload.nodeId);
|
|
167
|
+
const parent = requireNode(tree, node.parentId!);
|
|
168
|
+
|
|
169
|
+
// Собираем все ноды subtree для удаления
|
|
170
|
+
const toDelete = new Set<NodeId>();
|
|
171
|
+
const stack: NodeId[] = [node.id];
|
|
172
|
+
while (stack.length) {
|
|
173
|
+
const id = stack.pop()!;
|
|
174
|
+
if (toDelete.has(id)) continue;
|
|
175
|
+
toDelete.add(id);
|
|
176
|
+
const n = tree.nodes[id];
|
|
177
|
+
if (n) stack.push(...n.children);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const nextParent = cloneNode(parent);
|
|
181
|
+
nextParent.children = nextParent.children.filter((c) => c !== node.id);
|
|
182
|
+
|
|
183
|
+
const nextNodes: Record<NodeId, IEditorNode> = {};
|
|
184
|
+
for (const [id, n] of Object.entries(tree.nodes)) {
|
|
185
|
+
if (!toDelete.has(id)) nextNodes[id] = n;
|
|
186
|
+
}
|
|
187
|
+
nextNodes[parent.id] = nextParent;
|
|
188
|
+
|
|
189
|
+
return { ...tree, nodes: nextNodes };
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Patch props/meta/styles конкретной ноды. Не валидирует props против
|
|
194
|
+
* `propsSchema` — это работа инспектора при вводе. Тут только мердж.
|
|
195
|
+
*/
|
|
196
|
+
export const updateNode = (tree: IEditorTree, payload: IUpdateNodePayload): IEditorTree => {
|
|
197
|
+
const node = requireNode(tree, payload.nodeId);
|
|
198
|
+
const next = cloneNode(node);
|
|
199
|
+
if (payload.patch.props) next.props = { ...next.props, ...payload.patch.props };
|
|
200
|
+
if (payload.patch.meta) next.meta = { ...next.meta, ...payload.patch.meta };
|
|
201
|
+
if (payload.patch.styles) next.styles = { ...next.styles, ...payload.patch.styles };
|
|
202
|
+
return {
|
|
203
|
+
...tree,
|
|
204
|
+
nodes: { ...tree.nodes, [node.id]: next },
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Переупорядочить детей. `newOrder` должен содержать **те же id**, что и
|
|
210
|
+
* текущие children parent'а — иначе ошибка (это симптом несогласованного
|
|
211
|
+
* обновления и проще упасть, чем тихо потерять/добавить ноды).
|
|
212
|
+
*/
|
|
213
|
+
export const reorderChildren = (
|
|
214
|
+
tree: IEditorTree,
|
|
215
|
+
payload: IReorderChildrenPayload,
|
|
216
|
+
): IEditorTree => {
|
|
217
|
+
const parent = requireNode(tree, payload.parentId);
|
|
218
|
+
const current = new Set(parent.children);
|
|
219
|
+
const next = new Set(payload.newOrder);
|
|
220
|
+
if (current.size !== next.size || [...current].some((id) => !next.has(id))) {
|
|
221
|
+
throw new EditorOpError(
|
|
222
|
+
'reorderChildren: newOrder должен содержать ровно тех же детей что и parent',
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const nextParent = cloneNode(parent);
|
|
226
|
+
nextParent.children = [...payload.newOrder];
|
|
227
|
+
return { ...tree, nodes: { ...tree.nodes, [parent.id]: nextParent } };
|
|
228
|
+
};
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EditorOpError,
|
|
3
|
+
addNode,
|
|
4
|
+
createEmptyTree,
|
|
5
|
+
moveNode,
|
|
6
|
+
removeNode,
|
|
7
|
+
reorderChildren,
|
|
8
|
+
updateNode,
|
|
9
|
+
} from './operations';
|
|
10
|
+
import type {
|
|
11
|
+
IAddNodePayload,
|
|
12
|
+
IEditorContext,
|
|
13
|
+
IEditorTree,
|
|
14
|
+
IMoveNodePayload,
|
|
15
|
+
IRemoveNodePayload,
|
|
16
|
+
IReorderChildrenPayload,
|
|
17
|
+
IUpdateNodePayload,
|
|
18
|
+
NodeId,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
export interface ICreateEditorSchemaOptions {
|
|
22
|
+
/** Стартовое дерево. По умолчанию пустое с корнем `ui.Card`. */
|
|
23
|
+
initialTree?: IEditorTree;
|
|
24
|
+
rootType?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Возвращает HCA-схему (для `Feature(...)`) с одним стейтом `idle` и набором
|
|
29
|
+
* методов-обработчиков. Каждый метод — pure-операция + commit через
|
|
30
|
+
* `store.update({ tree, selectedId })`.
|
|
31
|
+
*
|
|
32
|
+
* Обёртка над пакетом-операциями — нужна чтобы внутри editor-app можно было:
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { createEditorSchema } from '@capsuletech/editor-state';
|
|
35
|
+
* const State = Feature(() => createEditorSchema());
|
|
36
|
+
* export default State;
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Методы вызываются через `next('addNode')` из дочернего Controller'а с
|
|
40
|
+
* payload в `target.payload`.
|
|
41
|
+
*/
|
|
42
|
+
export const createEditorSchema = (options: ICreateEditorSchemaOptions = {}) => {
|
|
43
|
+
const initialContext: IEditorContext = {
|
|
44
|
+
tree: options.initialTree ?? createEmptyTree(options.rootType),
|
|
45
|
+
selectedId: null,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
initial: 'idle',
|
|
50
|
+
context: initialContext,
|
|
51
|
+
states: {
|
|
52
|
+
idle: {
|
|
53
|
+
addNode: ({ target, store }: any) => {
|
|
54
|
+
const payload = target.payload as IAddNodePayload;
|
|
55
|
+
try {
|
|
56
|
+
const { tree, nodeId } = addNode(store.ctx.tree, payload);
|
|
57
|
+
store.update({ tree, selectedId: nodeId });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err instanceof EditorOpError) {
|
|
60
|
+
store.setErrors({ editor: err.message });
|
|
61
|
+
} else {
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
moveNode: ({ target, store }: any) => {
|
|
68
|
+
const payload = target.payload as IMoveNodePayload;
|
|
69
|
+
try {
|
|
70
|
+
const tree = moveNode(store.ctx.tree, payload);
|
|
71
|
+
store.update({ tree });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err instanceof EditorOpError) {
|
|
74
|
+
store.setErrors({ editor: err.message });
|
|
75
|
+
} else {
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
removeNode: ({ target, store }: any) => {
|
|
82
|
+
const payload = target.payload as IRemoveNodePayload;
|
|
83
|
+
try {
|
|
84
|
+
const tree = removeNode(store.ctx.tree, payload);
|
|
85
|
+
// Если удаляли selected — сбрасываем selection
|
|
86
|
+
const selectedId =
|
|
87
|
+
store.ctx.selectedId === payload.nodeId ? null : store.ctx.selectedId;
|
|
88
|
+
store.update({ tree, selectedId });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err instanceof EditorOpError) {
|
|
91
|
+
store.setErrors({ editor: err.message });
|
|
92
|
+
} else {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
updateNode: ({ target, store }: any) => {
|
|
99
|
+
const payload = target.payload as IUpdateNodePayload;
|
|
100
|
+
try {
|
|
101
|
+
const tree = updateNode(store.ctx.tree, payload);
|
|
102
|
+
store.update({ tree });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof EditorOpError) {
|
|
105
|
+
store.setErrors({ editor: err.message });
|
|
106
|
+
} else {
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
reorderChildren: ({ target, store }: any) => {
|
|
113
|
+
const payload = target.payload as IReorderChildrenPayload;
|
|
114
|
+
try {
|
|
115
|
+
const tree = reorderChildren(store.ctx.tree, payload);
|
|
116
|
+
store.update({ tree });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof EditorOpError) {
|
|
119
|
+
store.setErrors({ editor: err.message });
|
|
120
|
+
} else {
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
selectNode: ({ target, store }: any) => {
|
|
127
|
+
const id = target.payload as NodeId | null;
|
|
128
|
+
store.update({ selectedId: id });
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type NodeId = string;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Узел дерева редактора. Полностью описывает один JSX-узел в формате,
|
|
5
|
+
* совместимом с `@capsuletech/renderer` (`ISchema.components.nodes[id]`).
|
|
6
|
+
*
|
|
7
|
+
* Никакого UI-state'а (selected/expanded/etc.) — это отдельный концерн
|
|
8
|
+
* редактора и хранится вне дерева.
|
|
9
|
+
*/
|
|
10
|
+
export interface IEditorNode {
|
|
11
|
+
id: NodeId;
|
|
12
|
+
/** Dot-path в registry, напр. `'ui.Button'`. */
|
|
13
|
+
type: string;
|
|
14
|
+
parentId: NodeId | null;
|
|
15
|
+
/** Порядок имеет значение — массив, а не Set. */
|
|
16
|
+
children: NodeId[];
|
|
17
|
+
props: Record<string, unknown>;
|
|
18
|
+
meta: Record<string, unknown>;
|
|
19
|
+
styles: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IEditorTree {
|
|
23
|
+
root: NodeId;
|
|
24
|
+
nodes: Record<NodeId, IEditorNode>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Полный контекст редактора. Кроме дерева — UI-state (выбранная нода).
|
|
29
|
+
* Лежит в XState `context.data` через бридж.
|
|
30
|
+
*/
|
|
31
|
+
export interface IEditorContext {
|
|
32
|
+
tree: IEditorTree;
|
|
33
|
+
selectedId: NodeId | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Payload'ы для операций — типизированы отдельно для использования в Feature handlers. */
|
|
37
|
+
export interface IAddNodePayload {
|
|
38
|
+
type: string;
|
|
39
|
+
parentId: NodeId;
|
|
40
|
+
/** Куда вставить среди детей родителя. По умолчанию — в конец. */
|
|
41
|
+
index?: number;
|
|
42
|
+
/** Опциональный override дефолтных пропсов из манифеста. */
|
|
43
|
+
props?: Record<string, unknown>;
|
|
44
|
+
meta?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IMoveNodePayload {
|
|
48
|
+
nodeId: NodeId;
|
|
49
|
+
newParentId: NodeId;
|
|
50
|
+
index?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IRemoveNodePayload {
|
|
54
|
+
nodeId: NodeId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface IUpdateNodePayload {
|
|
58
|
+
nodeId: NodeId;
|
|
59
|
+
patch: {
|
|
60
|
+
props?: Record<string, unknown>;
|
|
61
|
+
meta?: Record<string, unknown>;
|
|
62
|
+
styles?: Record<string, string>;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface IReorderChildrenPayload {
|
|
67
|
+
parentId: NodeId;
|
|
68
|
+
newOrder: NodeId[];
|
|
69
|
+
}
|