@craftile/core 1.2.0 → 1.3.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/package.json +3 -3
- package/src/commands/duplicate-block.ts +37 -31
- package/src/commands/insert-block-from-preset.ts +27 -29
- package/src/commands/insert-block.ts +19 -15
- package/src/commands/move-block.ts +43 -28
- package/src/commands/remove-block.ts +13 -10
- package/src/commands/replace-region.ts +16 -15
- package/src/commands/set-block-name.ts +18 -14
- package/src/commands/set-block-property.ts +15 -14
- package/src/commands/toggle-block.ts +20 -16
- package/src/engine.ts +59 -18
- package/src/types.ts +1 -0
- package/src/utils.ts +43 -0
- package/src/vue/useBlocksEngine.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craftile/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Framework-agnostic block editor engine for Craftile",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"craftile",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"src"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@craftile/event-bus": "1.
|
|
30
|
-
"@craftile/types": "1.
|
|
29
|
+
"@craftile/event-bus": "1.3.0",
|
|
30
|
+
"@craftile/types": "1.3.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"vitest": "^3.2.4",
|
|
@@ -9,7 +9,7 @@ export interface DuplicateBlockOptions {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export class DuplicateBlockCommand implements Command {
|
|
12
|
-
private
|
|
12
|
+
private getPage: () => Page;
|
|
13
13
|
private blockId: string;
|
|
14
14
|
|
|
15
15
|
// State for reverting
|
|
@@ -20,25 +20,31 @@ export class DuplicateBlockCommand implements Command {
|
|
|
20
20
|
private regionId?: string | null;
|
|
21
21
|
private emit: EngineEmitFn;
|
|
22
22
|
|
|
23
|
-
constructor(
|
|
24
|
-
this.
|
|
23
|
+
constructor(getPage: () => Page, options: DuplicateBlockOptions) {
|
|
24
|
+
this.getPage = getPage;
|
|
25
25
|
this.blockId = options.blockId;
|
|
26
26
|
this.emit = options.emit;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
apply(): void {
|
|
30
|
-
const
|
|
30
|
+
const page = this.getPage();
|
|
31
|
+
const originalBlock = page.blocks[this.blockId];
|
|
31
32
|
if (!originalBlock) {
|
|
32
33
|
throw new Error(`Block not found: ${this.blockId}`);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
this.duplicatedBlockId = generateId();
|
|
36
37
|
|
|
37
|
-
this.duplicatedBlock = this.cloneBlockWithNewIds(
|
|
38
|
+
this.duplicatedBlock = this.cloneBlockWithNewIds(
|
|
39
|
+
page,
|
|
40
|
+
originalBlock,
|
|
41
|
+
this.duplicatedBlockId,
|
|
42
|
+
originalBlock.parentId
|
|
43
|
+
);
|
|
38
44
|
|
|
39
|
-
this.determineInsertLocation();
|
|
45
|
+
this.determineInsertLocation(page);
|
|
40
46
|
|
|
41
|
-
this.insertDuplicatedBlock();
|
|
47
|
+
this.insertDuplicatedBlock(page);
|
|
42
48
|
|
|
43
49
|
this.emit('block:duplicate', {
|
|
44
50
|
originalBlockId: this.blockId,
|
|
@@ -55,7 +61,7 @@ export class DuplicateBlockCommand implements Command {
|
|
|
55
61
|
return;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
this.removeDuplicatedBlock();
|
|
64
|
+
this.removeDuplicatedBlock(this.getPage());
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
getBlockId(): string {
|
|
@@ -82,7 +88,7 @@ export class DuplicateBlockCommand implements Command {
|
|
|
82
88
|
return this.regionId;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
private cloneBlockWithNewIds(block: Block, newId: string, newParentId?: string): Block {
|
|
91
|
+
private cloneBlockWithNewIds(page: Page, block: Block, newId: string, newParentId?: string): Block {
|
|
86
92
|
const clonedBlock: Block = {
|
|
87
93
|
...structuredClone(block),
|
|
88
94
|
id: newId,
|
|
@@ -92,27 +98,27 @@ export class DuplicateBlockCommand implements Command {
|
|
|
92
98
|
|
|
93
99
|
if (block.children && block.children.length > 0) {
|
|
94
100
|
for (const childId of block.children) {
|
|
95
|
-
const childBlock =
|
|
101
|
+
const childBlock = page.blocks[childId];
|
|
96
102
|
if (childBlock) {
|
|
97
103
|
const newChildId = generateId();
|
|
98
|
-
this.cloneBlockWithNewIds(childBlock, newChildId, newId);
|
|
104
|
+
this.cloneBlockWithNewIds(page, childBlock, newChildId, newId);
|
|
99
105
|
clonedBlock.children.push(newChildId);
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
|
|
110
|
+
page.blocks[newId] = clonedBlock;
|
|
105
111
|
|
|
106
112
|
return clonedBlock;
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
private determineInsertLocation(): void {
|
|
110
|
-
const originalBlock =
|
|
115
|
+
private determineInsertLocation(page: Page): void {
|
|
116
|
+
const originalBlock = page.blocks[this.blockId];
|
|
111
117
|
|
|
112
118
|
this.parentId = originalBlock.parentId || null;
|
|
113
119
|
|
|
114
120
|
if (this.parentId) {
|
|
115
|
-
const parent =
|
|
121
|
+
const parent = page.blocks[this.parentId];
|
|
116
122
|
if (parent) {
|
|
117
123
|
const index = parent.children.indexOf(this.blockId);
|
|
118
124
|
this.insertIndex = index !== -1 ? index + 1 : parent.children.length;
|
|
@@ -120,52 +126,52 @@ export class DuplicateBlockCommand implements Command {
|
|
|
120
126
|
this.insertIndex = 0;
|
|
121
127
|
}
|
|
122
128
|
} else {
|
|
123
|
-
const region =
|
|
129
|
+
const region = page.regions.find((r) => r.blocks.includes(this.blockId));
|
|
124
130
|
if (region) {
|
|
125
131
|
const index = region.blocks.indexOf(this.blockId);
|
|
126
132
|
this.regionId = getRegionId(region);
|
|
127
133
|
this.insertIndex = index + 1;
|
|
128
134
|
} else {
|
|
129
|
-
this.regionId = getRegionId(
|
|
130
|
-
this.insertIndex =
|
|
135
|
+
this.regionId = getRegionId(page.regions[0]) || 'main';
|
|
136
|
+
this.insertIndex = page.regions[0].blocks.length || 0;
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
|
|
135
|
-
private insertDuplicatedBlock(): void {
|
|
141
|
+
private insertDuplicatedBlock(page: Page): void {
|
|
136
142
|
if (!this.duplicatedBlock) {
|
|
137
143
|
return;
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
if (this.parentId) {
|
|
141
|
-
const targetParent =
|
|
147
|
+
const targetParent = page.blocks[this.parentId];
|
|
142
148
|
if (!targetParent) {
|
|
143
149
|
throw new Error(`Parent block not found: ${this.parentId}`);
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
targetParent.children.splice(this.insertIndex, 0, this.duplicatedBlockId);
|
|
147
153
|
} else {
|
|
148
|
-
let targetRegion =
|
|
154
|
+
let targetRegion = page.regions.find((r) => getRegionId(r) === this.regionId);
|
|
149
155
|
|
|
150
156
|
if (!targetRegion) {
|
|
151
157
|
targetRegion = { name: this.regionId || 'main', blocks: [] };
|
|
152
|
-
|
|
158
|
+
page.regions.push(targetRegion);
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
targetRegion.blocks.splice(this.insertIndex, 0, this.duplicatedBlockId);
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
|
|
159
|
-
private removeDuplicatedBlock(): void {
|
|
165
|
+
private removeDuplicatedBlock(page: Page): void {
|
|
160
166
|
if (!this.duplicatedBlockId) {
|
|
161
167
|
return;
|
|
162
168
|
}
|
|
163
169
|
|
|
164
|
-
this.removeBlockAndChildren(this.duplicatedBlockId);
|
|
170
|
+
this.removeBlockAndChildren(page, this.duplicatedBlockId);
|
|
165
171
|
|
|
166
172
|
if (this.parentId) {
|
|
167
173
|
// Remove from parent's children array
|
|
168
|
-
const targetParent =
|
|
174
|
+
const targetParent = page.blocks[this.parentId];
|
|
169
175
|
if (targetParent) {
|
|
170
176
|
const index = targetParent.children.indexOf(this.duplicatedBlockId);
|
|
171
177
|
if (index !== -1) {
|
|
@@ -174,8 +180,8 @@ export class DuplicateBlockCommand implements Command {
|
|
|
174
180
|
}
|
|
175
181
|
} else {
|
|
176
182
|
// Remove from region
|
|
177
|
-
if (
|
|
178
|
-
const targetRegion =
|
|
183
|
+
if (page.regions) {
|
|
184
|
+
const targetRegion = page.regions.find((r) => getRegionId(r) === this.regionId);
|
|
179
185
|
if (targetRegion) {
|
|
180
186
|
const index = targetRegion.blocks.indexOf(this.duplicatedBlockId);
|
|
181
187
|
if (index !== -1) {
|
|
@@ -186,16 +192,16 @@ export class DuplicateBlockCommand implements Command {
|
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
|
|
189
|
-
private removeBlockAndChildren(blockId: string): void {
|
|
190
|
-
const block =
|
|
195
|
+
private removeBlockAndChildren(page: Page, blockId: string): void {
|
|
196
|
+
const block = page.blocks[blockId];
|
|
191
197
|
if (!block) {
|
|
192
198
|
return;
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
for (const childId of block.children) {
|
|
196
|
-
this.removeBlockAndChildren(childId);
|
|
202
|
+
this.removeBlockAndChildren(page, childId);
|
|
197
203
|
}
|
|
198
204
|
|
|
199
|
-
delete
|
|
205
|
+
delete page.blocks[blockId];
|
|
200
206
|
}
|
|
201
207
|
}
|
|
@@ -15,7 +15,7 @@ export interface InsertBlockFromPresetOptions {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export class InsertBlockFromPresetCommand implements Command {
|
|
18
|
-
private
|
|
18
|
+
private getPage: () => Page;
|
|
19
19
|
private blockType: string;
|
|
20
20
|
private presetIndex?: number;
|
|
21
21
|
private presetData?: BlockStructure;
|
|
@@ -31,8 +31,8 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
31
31
|
private emit: EngineEmitFn;
|
|
32
32
|
private createdBlockIds: string[] = [];
|
|
33
33
|
|
|
34
|
-
constructor(
|
|
35
|
-
this.
|
|
34
|
+
constructor(getPage: () => Page, options: InsertBlockFromPresetOptions) {
|
|
35
|
+
this.getPage = getPage;
|
|
36
36
|
this.blockType = options.blockType;
|
|
37
37
|
this.presetIndex = options.presetIndex;
|
|
38
38
|
this.presetData = options.presetData;
|
|
@@ -69,13 +69,15 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
apply(): void {
|
|
72
|
-
const
|
|
72
|
+
const page = this.getPage();
|
|
73
|
+
const target = resolveInsertTarget(page, this.parentId, this.regionId, this.index);
|
|
73
74
|
this.resolvedRegionId = target.kind === 'region' ? target.regionId : undefined;
|
|
74
75
|
|
|
75
76
|
const blockSchema = this.blocksManager.get(this.blockType);
|
|
76
77
|
|
|
77
78
|
if (this.presetData) {
|
|
78
79
|
this.insertedBlock = {
|
|
80
|
+
...structuredClone(this.presetData),
|
|
79
81
|
type: this.blockType,
|
|
80
82
|
id: this.blockId,
|
|
81
83
|
name: this.presetData.name || blockSchema?.meta?.name || this.blockType,
|
|
@@ -83,17 +85,13 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
83
85
|
properties: this.properties,
|
|
84
86
|
children: [],
|
|
85
87
|
parentId: undefined,
|
|
86
|
-
static: this.presetData.static,
|
|
87
|
-
disabled: this.presetData.disabled,
|
|
88
|
-
repeated: this.presetData.repeated,
|
|
89
|
-
ghost: this.presetData.ghost,
|
|
90
88
|
};
|
|
91
89
|
|
|
92
|
-
|
|
90
|
+
page.blocks[this.blockId] = this.insertedBlock;
|
|
93
91
|
this.createdBlockIds.push(this.blockId);
|
|
94
92
|
|
|
95
93
|
if (this.presetData.children && this.presetData.children.length > 0) {
|
|
96
|
-
this.insertedBlock.children = this.createChildrenFromPreset(this.presetData.children, this.blockId);
|
|
94
|
+
this.insertedBlock.children = this.createChildrenFromPreset(page, this.presetData.children, this.blockId);
|
|
97
95
|
}
|
|
98
96
|
} else if (this.presetIndex !== undefined) {
|
|
99
97
|
const preset = blockSchema?.presets?.[this.presetIndex];
|
|
@@ -110,11 +108,11 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
110
108
|
parentId: undefined,
|
|
111
109
|
};
|
|
112
110
|
|
|
113
|
-
|
|
111
|
+
page.blocks[this.blockId] = this.insertedBlock;
|
|
114
112
|
this.createdBlockIds.push(this.blockId);
|
|
115
113
|
|
|
116
114
|
if (preset.children && preset.children.length > 0) {
|
|
117
|
-
this.insertedBlock.children = this.createChildrenFromPreset(preset.children, this.blockId);
|
|
115
|
+
this.insertedBlock.children = this.createChildrenFromPreset(page, preset.children, this.blockId);
|
|
118
116
|
}
|
|
119
117
|
}
|
|
120
118
|
|
|
@@ -128,7 +126,7 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
128
126
|
this.insertedBlock.parentId = target.parent.id;
|
|
129
127
|
target.parent.children.splice(target.index, 0, this.blockId);
|
|
130
128
|
} else {
|
|
131
|
-
const region =
|
|
129
|
+
const region = page.regions.find((r) => getRegionId(r) === target.regionId)!;
|
|
132
130
|
region.blocks.splice(target.index, 0, this.blockId);
|
|
133
131
|
}
|
|
134
132
|
|
|
@@ -137,7 +135,7 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
137
135
|
block: this.insertedBlock,
|
|
138
136
|
parentId: this.parentId,
|
|
139
137
|
index: this.actualIndex,
|
|
140
|
-
regionId: this.regionId || getRegionId(
|
|
138
|
+
regionId: this.regionId || getRegionId(page.regions[0]),
|
|
141
139
|
});
|
|
142
140
|
}
|
|
143
141
|
|
|
@@ -146,21 +144,24 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
146
144
|
return;
|
|
147
145
|
}
|
|
148
146
|
|
|
147
|
+
const page = this.getPage();
|
|
148
|
+
|
|
149
149
|
// Remove all created blocks
|
|
150
150
|
for (const blockId of this.createdBlockIds) {
|
|
151
|
-
delete
|
|
151
|
+
delete page.blocks[blockId];
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
if (this.parentId) {
|
|
155
|
-
const parent =
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
const parent = page.blocks[this.parentId];
|
|
156
|
+
const index = parent ? parent.children.indexOf(this.blockId) : -1;
|
|
157
|
+
if (parent && index !== -1) {
|
|
158
|
+
parent.children.splice(index, 1);
|
|
158
159
|
}
|
|
159
160
|
} else if (this.resolvedRegionId) {
|
|
160
|
-
const targetRegion =
|
|
161
|
-
|
|
162
|
-
if (targetRegion &&
|
|
163
|
-
targetRegion.blocks.splice(
|
|
161
|
+
const targetRegion = page.regions.find((r) => getRegionId(r) === this.resolvedRegionId);
|
|
162
|
+
const index = targetRegion ? targetRegion.blocks.indexOf(this.blockId) : -1;
|
|
163
|
+
if (targetRegion && index !== -1) {
|
|
164
|
+
targetRegion.blocks.splice(index, 1);
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
|
|
@@ -180,7 +181,7 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
180
181
|
return this.insertedBlock;
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
private createChildrenFromPreset(structures: BlockStructure[], parentId: string): string[] {
|
|
184
|
+
private createChildrenFromPreset(page: Page, structures: BlockStructure[], parentId: string): string[] {
|
|
184
185
|
const childIds: string[] = [];
|
|
185
186
|
|
|
186
187
|
for (const structure of structures) {
|
|
@@ -195,6 +196,7 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
195
196
|
const childProperties = this.buildProperties(childSchema, structure.properties);
|
|
196
197
|
|
|
197
198
|
const childBlock: Block = {
|
|
199
|
+
...structuredClone(structure),
|
|
198
200
|
type: structure.type,
|
|
199
201
|
id: childId,
|
|
200
202
|
name: structure.name || childSchema.meta?.name || structure.type,
|
|
@@ -202,18 +204,14 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
202
204
|
properties: childProperties,
|
|
203
205
|
children: [],
|
|
204
206
|
parentId,
|
|
205
|
-
static: structure.static,
|
|
206
|
-
disabled: structure.disabled,
|
|
207
|
-
repeated: structure.repeated,
|
|
208
|
-
ghost: structure.ghost,
|
|
209
207
|
};
|
|
210
208
|
|
|
211
|
-
|
|
209
|
+
page.blocks[childId] = childBlock;
|
|
212
210
|
this.createdBlockIds.push(childId);
|
|
213
211
|
|
|
214
212
|
// Recursively create nested children
|
|
215
213
|
if (structure.children && structure.children.length > 0) {
|
|
216
|
-
childBlock.children = this.createChildrenFromPreset(structure.children, childId);
|
|
214
|
+
childBlock.children = this.createChildrenFromPreset(page, structure.children, childId);
|
|
217
215
|
}
|
|
218
216
|
|
|
219
217
|
childIds.push(childId);
|
|
@@ -13,7 +13,7 @@ export interface InsertBlockOptions {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export class InsertBlockCommand implements Command {
|
|
16
|
-
private
|
|
16
|
+
private getPage: () => Page;
|
|
17
17
|
private blockType: string;
|
|
18
18
|
private parentId?: string;
|
|
19
19
|
private regionId?: string;
|
|
@@ -26,8 +26,8 @@ export class InsertBlockCommand implements Command {
|
|
|
26
26
|
private resolvedRegionId?: string;
|
|
27
27
|
private emit: EngineEmitFn;
|
|
28
28
|
|
|
29
|
-
constructor(
|
|
30
|
-
this.
|
|
29
|
+
constructor(getPage: () => Page, options: InsertBlockOptions) {
|
|
30
|
+
this.getPage = getPage;
|
|
31
31
|
this.blockType = options.blockType;
|
|
32
32
|
this.parentId = options.parentId;
|
|
33
33
|
this.regionId = options.regionId;
|
|
@@ -40,7 +40,8 @@ export class InsertBlockCommand implements Command {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
apply(): void {
|
|
43
|
-
const
|
|
43
|
+
const page = this.getPage();
|
|
44
|
+
const target = resolveInsertTarget(page, this.parentId, this.regionId, this.index);
|
|
44
45
|
this.resolvedRegionId = target.kind === 'region' ? target.regionId : undefined;
|
|
45
46
|
|
|
46
47
|
const blockName = this.blockSchema?.meta?.name || this.blockType;
|
|
@@ -54,14 +55,14 @@ export class InsertBlockCommand implements Command {
|
|
|
54
55
|
parentId: undefined,
|
|
55
56
|
};
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
page.blocks[this.blockId] = this.insertedBlock;
|
|
58
59
|
this.actualIndex = target.index;
|
|
59
60
|
|
|
60
61
|
if (target.kind === 'parent') {
|
|
61
62
|
this.insertedBlock.parentId = target.parent.id;
|
|
62
63
|
target.parent.children.splice(target.index, 0, this.blockId);
|
|
63
64
|
} else {
|
|
64
|
-
const region =
|
|
65
|
+
const region = page.regions.find((r) => getRegionId(r) === target.regionId)!;
|
|
65
66
|
region.blocks.splice(target.index, 0, this.blockId);
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -70,7 +71,7 @@ export class InsertBlockCommand implements Command {
|
|
|
70
71
|
block: this.insertedBlock,
|
|
71
72
|
parentId: this.parentId,
|
|
72
73
|
index: this.actualIndex,
|
|
73
|
-
regionId: this.regionId || getRegionId(
|
|
74
|
+
regionId: this.regionId || getRegionId(page.regions[0]),
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -79,18 +80,21 @@ export class InsertBlockCommand implements Command {
|
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
const page = this.getPage();
|
|
84
|
+
|
|
85
|
+
delete page.blocks[this.blockId];
|
|
83
86
|
|
|
84
87
|
if (this.parentId) {
|
|
85
|
-
const parent =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
const parent = page.blocks[this.parentId];
|
|
89
|
+
const index = parent ? parent.children.indexOf(this.blockId) : -1;
|
|
90
|
+
if (parent && index !== -1) {
|
|
91
|
+
parent.children.splice(index, 1);
|
|
88
92
|
}
|
|
89
93
|
} else if (this.resolvedRegionId) {
|
|
90
|
-
const targetRegion =
|
|
91
|
-
|
|
92
|
-
if (targetRegion &&
|
|
93
|
-
targetRegion.blocks.splice(
|
|
94
|
+
const targetRegion = page.regions.find((r) => getRegionId(r) === this.resolvedRegionId);
|
|
95
|
+
const index = targetRegion ? targetRegion.blocks.indexOf(this.blockId) : -1;
|
|
96
|
+
if (targetRegion && index !== -1) {
|
|
97
|
+
targetRegion.blocks.splice(index, 1);
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Page } from '@craftile/types';
|
|
2
2
|
import type { Command, EngineEmitFn } from '../types';
|
|
3
3
|
import type { ResolvedTarget } from '../utils';
|
|
4
4
|
import { canInsertDynamicChildAt, clampIndex, getRegionId, resolveRegionId } from '../utils';
|
|
@@ -12,22 +12,22 @@ export interface MoveBlockOptions {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export class MoveBlockCommand implements Command {
|
|
15
|
-
private
|
|
15
|
+
private getPage: () => Page;
|
|
16
16
|
private blockId: string;
|
|
17
17
|
private targetParentId?: string;
|
|
18
18
|
private targetIndex?: number;
|
|
19
19
|
private targetRegionId?: string;
|
|
20
20
|
|
|
21
21
|
// State for reverting
|
|
22
|
-
private
|
|
22
|
+
private moved = false;
|
|
23
23
|
private originalParentId?: string;
|
|
24
24
|
private originalIndex!: number;
|
|
25
25
|
private originalRegionId?: string;
|
|
26
26
|
|
|
27
27
|
private emit: EngineEmitFn;
|
|
28
28
|
|
|
29
|
-
constructor(
|
|
30
|
-
this.
|
|
29
|
+
constructor(getPage: () => Page, options: MoveBlockOptions) {
|
|
30
|
+
this.getPage = getPage;
|
|
31
31
|
this.blockId = options.blockId;
|
|
32
32
|
this.targetParentId = options.targetParentId;
|
|
33
33
|
this.targetIndex = options.targetIndex;
|
|
@@ -36,29 +36,37 @@ export class MoveBlockCommand implements Command {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
apply(): void {
|
|
39
|
-
const
|
|
39
|
+
const page = this.getPage();
|
|
40
|
+
const blockToMove = page.blocks[this.blockId];
|
|
41
|
+
if (!blockToMove) {
|
|
42
|
+
throw new Error(`Block not found: ${this.blockId}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const target = this.validateAndResolveTarget(page);
|
|
40
46
|
|
|
41
47
|
if (this.originalParentId) {
|
|
42
|
-
const sourceParent =
|
|
48
|
+
const sourceParent = page.blocks[this.originalParentId];
|
|
43
49
|
if (sourceParent && this.originalIndex !== -1) {
|
|
44
50
|
sourceParent.children.splice(this.originalIndex, 1);
|
|
45
51
|
}
|
|
46
52
|
} else if (this.originalRegionId) {
|
|
47
|
-
const sourceRegion =
|
|
53
|
+
const sourceRegion = page.regions.find((r) => getRegionId(r) === this.originalRegionId);
|
|
48
54
|
if (sourceRegion && this.originalIndex !== -1) {
|
|
49
55
|
sourceRegion.blocks.splice(this.originalIndex, 1);
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
if (target.kind === 'parent') {
|
|
54
|
-
|
|
60
|
+
blockToMove.parentId = target.parent.id;
|
|
55
61
|
target.parent.children.splice(target.index, 0, this.blockId);
|
|
56
62
|
} else {
|
|
57
|
-
|
|
58
|
-
const region =
|
|
63
|
+
blockToMove.parentId = undefined;
|
|
64
|
+
const region = page.regions.find((r) => getRegionId(r) === target.regionId)!;
|
|
59
65
|
region.blocks.splice(target.index, 0, this.blockId);
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
this.moved = true;
|
|
69
|
+
|
|
62
70
|
this.emit('block:move', {
|
|
63
71
|
blockId: this.blockId,
|
|
64
72
|
targetParentId: this.targetParentId,
|
|
@@ -76,18 +84,19 @@ export class MoveBlockCommand implements Command {
|
|
|
76
84
|
* Captures the source position too so `revert()` can restore it. Pure read —
|
|
77
85
|
* does not mutate `page`.
|
|
78
86
|
*/
|
|
79
|
-
private validateAndResolveTarget(): ResolvedTarget {
|
|
80
|
-
|
|
81
|
-
if (!
|
|
87
|
+
private validateAndResolveTarget(page: Page): ResolvedTarget {
|
|
88
|
+
const blockToMove = page.blocks[this.blockId];
|
|
89
|
+
if (!blockToMove) {
|
|
82
90
|
throw new Error(`Block not found: ${this.blockId}`);
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
this.originalParentId =
|
|
93
|
+
this.originalParentId = blockToMove.parentId;
|
|
94
|
+
this.originalRegionId = undefined;
|
|
86
95
|
if (this.originalParentId) {
|
|
87
|
-
const sourceParent =
|
|
96
|
+
const sourceParent = page.blocks[this.originalParentId];
|
|
88
97
|
this.originalIndex = sourceParent ? sourceParent.children.indexOf(this.blockId) : -1;
|
|
89
98
|
} else {
|
|
90
|
-
const sourceRegion =
|
|
99
|
+
const sourceRegion = page.regions.find((r) => r.blocks.includes(this.blockId));
|
|
91
100
|
if (sourceRegion) {
|
|
92
101
|
this.originalRegionId = getRegionId(sourceRegion);
|
|
93
102
|
this.originalIndex = sourceRegion.blocks.indexOf(this.blockId);
|
|
@@ -97,7 +106,7 @@ export class MoveBlockCommand implements Command {
|
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
if (this.targetParentId) {
|
|
100
|
-
const parent =
|
|
109
|
+
const parent = page.blocks[this.targetParentId];
|
|
101
110
|
if (!parent) {
|
|
102
111
|
throw new Error(`Target parent not found: ${this.targetParentId}`);
|
|
103
112
|
}
|
|
@@ -106,7 +115,7 @@ export class MoveBlockCommand implements Command {
|
|
|
106
115
|
const prospective = sourceIsTarget ? parent.children.filter((id) => id !== this.blockId) : parent.children;
|
|
107
116
|
const index = clampIndex(prospective.length, this.targetIndex);
|
|
108
117
|
|
|
109
|
-
if (!canInsertDynamicChildAt(prospective,
|
|
118
|
+
if (!canInsertDynamicChildAt(prospective, page.blocks, index)) {
|
|
110
119
|
throw new Error(
|
|
111
120
|
`Cannot place at index ${index} of ${this.targetParentId}: not a valid slot for a dynamic child`
|
|
112
121
|
);
|
|
@@ -115,8 +124,8 @@ export class MoveBlockCommand implements Command {
|
|
|
115
124
|
return { kind: 'parent', parent, index };
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
const regionId = resolveRegionId(
|
|
119
|
-
const existing =
|
|
127
|
+
const regionId = resolveRegionId(page, this.targetRegionId);
|
|
128
|
+
const existing = page.regions.find((r) => getRegionId(r) === regionId);
|
|
120
129
|
if (!existing) {
|
|
121
130
|
throw new Error(`Region not found: ${regionId}`);
|
|
122
131
|
}
|
|
@@ -128,12 +137,18 @@ export class MoveBlockCommand implements Command {
|
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
revert(): void {
|
|
131
|
-
if (!this.
|
|
140
|
+
if (!this.moved || this.originalIndex === undefined) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const page = this.getPage();
|
|
145
|
+
const blockToMove = page.blocks[this.blockId];
|
|
146
|
+
if (!blockToMove) {
|
|
132
147
|
return;
|
|
133
148
|
}
|
|
134
149
|
|
|
135
|
-
if (
|
|
136
|
-
const currentParent =
|
|
150
|
+
if (blockToMove.parentId) {
|
|
151
|
+
const currentParent = page.blocks[blockToMove.parentId];
|
|
137
152
|
if (currentParent) {
|
|
138
153
|
const index = currentParent.children.indexOf(this.blockId);
|
|
139
154
|
if (index !== -1) {
|
|
@@ -141,7 +156,7 @@ export class MoveBlockCommand implements Command {
|
|
|
141
156
|
}
|
|
142
157
|
}
|
|
143
158
|
} else {
|
|
144
|
-
const region =
|
|
159
|
+
const region = page.regions.find((r) => r.blocks.includes(this.blockId));
|
|
145
160
|
if (region) {
|
|
146
161
|
const index = region.blocks.indexOf(this.blockId);
|
|
147
162
|
if (index !== -1) {
|
|
@@ -150,15 +165,15 @@ export class MoveBlockCommand implements Command {
|
|
|
150
165
|
}
|
|
151
166
|
}
|
|
152
167
|
|
|
153
|
-
|
|
168
|
+
blockToMove.parentId = this.originalParentId;
|
|
154
169
|
|
|
155
170
|
if (this.originalParentId) {
|
|
156
|
-
const originalParent =
|
|
171
|
+
const originalParent = page.blocks[this.originalParentId];
|
|
157
172
|
if (originalParent) {
|
|
158
173
|
originalParent.children.splice(this.originalIndex, 0, this.blockId);
|
|
159
174
|
}
|
|
160
175
|
} else if (this.originalRegionId) {
|
|
161
|
-
const region =
|
|
176
|
+
const region = page.regions.find((r) => getRegionId(r) === this.originalRegionId);
|
|
162
177
|
if (region) {
|
|
163
178
|
region.blocks.splice(this.originalIndex, 0, this.blockId);
|
|
164
179
|
}
|
|
@@ -8,7 +8,7 @@ export interface RemoveBlockOptions {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export class RemoveBlockCommand implements Command {
|
|
11
|
-
private
|
|
11
|
+
private getPage: () => Page;
|
|
12
12
|
private blockId: string;
|
|
13
13
|
private removedBlock?: Block;
|
|
14
14
|
private originalIndex?: number;
|
|
@@ -16,14 +16,15 @@ export class RemoveBlockCommand implements Command {
|
|
|
16
16
|
private regionId?: string;
|
|
17
17
|
private emit: EngineEmitFn;
|
|
18
18
|
|
|
19
|
-
constructor(
|
|
20
|
-
this.
|
|
19
|
+
constructor(getPage: () => Page, options: RemoveBlockOptions) {
|
|
20
|
+
this.getPage = getPage;
|
|
21
21
|
this.blockId = options.blockId;
|
|
22
22
|
this.emit = options.emit;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
apply(): void {
|
|
26
|
-
|
|
26
|
+
const page = this.getPage();
|
|
27
|
+
this.removedBlock = page.blocks[this.blockId];
|
|
27
28
|
|
|
28
29
|
if (!this.removedBlock) {
|
|
29
30
|
throw new Error(`Block not found: ${this.blockId}`);
|
|
@@ -32,7 +33,7 @@ export class RemoveBlockCommand implements Command {
|
|
|
32
33
|
this.originalParentId = this.removedBlock.parentId;
|
|
33
34
|
|
|
34
35
|
if (this.originalParentId) {
|
|
35
|
-
const parent =
|
|
36
|
+
const parent = page.blocks[this.originalParentId];
|
|
36
37
|
if (parent) {
|
|
37
38
|
this.originalIndex = parent.children.indexOf(this.blockId);
|
|
38
39
|
if (this.originalIndex !== -1) {
|
|
@@ -40,7 +41,7 @@ export class RemoveBlockCommand implements Command {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
} else {
|
|
43
|
-
const region =
|
|
44
|
+
const region = page.regions.find((r) => r.blocks.includes(this.blockId));
|
|
44
45
|
if (region) {
|
|
45
46
|
this.originalIndex = region.blocks.indexOf(this.blockId);
|
|
46
47
|
this.regionId = getRegionId(region);
|
|
@@ -48,7 +49,7 @@ export class RemoveBlockCommand implements Command {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
delete
|
|
52
|
+
delete page.blocks[this.blockId];
|
|
52
53
|
|
|
53
54
|
this.emit('block:remove', {
|
|
54
55
|
blockId: this.blockId,
|
|
@@ -63,15 +64,17 @@ export class RemoveBlockCommand implements Command {
|
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
const page = this.getPage();
|
|
68
|
+
|
|
69
|
+
page.blocks[this.blockId] = this.removedBlock;
|
|
67
70
|
|
|
68
71
|
if (this.originalParentId) {
|
|
69
|
-
const parent =
|
|
72
|
+
const parent = page.blocks[this.originalParentId];
|
|
70
73
|
if (parent) {
|
|
71
74
|
parent.children.splice(this.originalIndex, 0, this.blockId);
|
|
72
75
|
}
|
|
73
76
|
} else if (this.regionId) {
|
|
74
|
-
const region =
|
|
77
|
+
const region = page.regions.find((r) => getRegionId(r) === this.regionId);
|
|
75
78
|
if (region) {
|
|
76
79
|
region.blocks.splice(this.originalIndex, 0, this.blockId);
|
|
77
80
|
}
|
|
@@ -11,7 +11,7 @@ export interface ReplaceRegionOptions {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export class ReplaceRegionCommand implements Command {
|
|
14
|
-
private
|
|
14
|
+
private getPage: () => Page;
|
|
15
15
|
private regionId: string;
|
|
16
16
|
private regionIndex: number;
|
|
17
17
|
private previousRegion: Page['regions'][number];
|
|
@@ -21,19 +21,21 @@ export class ReplaceRegionCommand implements Command {
|
|
|
21
21
|
private emit: EngineEmitFn;
|
|
22
22
|
private blocksManager: BlocksManager;
|
|
23
23
|
|
|
24
|
-
constructor(
|
|
25
|
-
this.
|
|
24
|
+
constructor(getPage: () => Page, options: ReplaceRegionOptions) {
|
|
25
|
+
this.getPage = getPage;
|
|
26
26
|
this.regionId = options.regionId;
|
|
27
27
|
this.blocksManager = options.blocksManager;
|
|
28
28
|
this.emit = options.emit;
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const page = this.getPage();
|
|
31
|
+
|
|
32
|
+
this.regionIndex = page.regions.findIndex((region) => getRegionId(region) === this.regionId);
|
|
31
33
|
if (this.regionIndex === -1) {
|
|
32
34
|
throw new Error(`Region not found: ${this.regionId}`);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
this.previousRegion = structuredClone(
|
|
36
|
-
this.removedBlocks = this.collectBlocksForRoots(this.previousRegion.blocks);
|
|
37
|
+
this.previousRegion = structuredClone(page.regions[this.regionIndex]);
|
|
38
|
+
this.removedBlocks = this.collectBlocksForRoots(page, this.previousRegion.blocks);
|
|
37
39
|
|
|
38
40
|
this.newBlocks = {};
|
|
39
41
|
const newRootIds = options.structures.map((structure) =>
|
|
@@ -65,6 +67,7 @@ export class ReplaceRegionCommand implements Command {
|
|
|
65
67
|
|
|
66
68
|
const blockId = generateId();
|
|
67
69
|
const block: Block = {
|
|
70
|
+
...structuredClone(structure),
|
|
68
71
|
type: structure.type,
|
|
69
72
|
id: blockId,
|
|
70
73
|
name: structure.name || blockSchema.meta?.name || structure.type,
|
|
@@ -72,10 +75,6 @@ export class ReplaceRegionCommand implements Command {
|
|
|
72
75
|
properties: this.buildProperties(blockSchema, structure.properties),
|
|
73
76
|
children: [],
|
|
74
77
|
parentId,
|
|
75
|
-
static: structure.static,
|
|
76
|
-
disabled: structure.disabled,
|
|
77
|
-
repeated: structure.repeated,
|
|
78
|
-
ghost: structure.ghost,
|
|
79
78
|
};
|
|
80
79
|
|
|
81
80
|
target[blockId] = block;
|
|
@@ -100,11 +99,11 @@ export class ReplaceRegionCommand implements Command {
|
|
|
100
99
|
return builtProperties;
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
private collectBlocksForRoots(rootBlockIds: string[]): Record<string, Block> {
|
|
102
|
+
private collectBlocksForRoots(page: Page, rootBlockIds: string[]): Record<string, Block> {
|
|
104
103
|
const blocks: Record<string, Block> = {};
|
|
105
104
|
|
|
106
105
|
const collectBlock = (blockId: string) => {
|
|
107
|
-
const block =
|
|
106
|
+
const block = page.blocks[blockId];
|
|
108
107
|
if (!block || blocks[blockId]) {
|
|
109
108
|
return;
|
|
110
109
|
}
|
|
@@ -124,15 +123,17 @@ export class ReplaceRegionCommand implements Command {
|
|
|
124
123
|
removedBlocks: Record<string, Block>,
|
|
125
124
|
newBlocks: Record<string, Block>
|
|
126
125
|
): void {
|
|
126
|
+
const page = this.getPage();
|
|
127
|
+
|
|
127
128
|
Object.keys(removedBlocks).forEach((blockId) => {
|
|
128
|
-
delete
|
|
129
|
+
delete page.blocks[blockId];
|
|
129
130
|
});
|
|
130
131
|
|
|
131
132
|
Object.entries(newBlocks).forEach(([blockId, block]) => {
|
|
132
|
-
|
|
133
|
+
page.blocks[blockId] = block;
|
|
133
134
|
});
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
page.regions[this.regionIndex] = newRegion;
|
|
136
137
|
|
|
137
138
|
this.emit('region:replace', {
|
|
138
139
|
regionId: this.regionId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Page } from '@craftile/types';
|
|
2
2
|
import type { Command, EngineEmitFn } from '../types';
|
|
3
3
|
|
|
4
4
|
export interface SetBlockNameOptions {
|
|
@@ -8,37 +8,39 @@ export interface SetBlockNameOptions {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export class SetBlockNameCommand implements Command {
|
|
11
|
-
private
|
|
11
|
+
private getPage: () => Page;
|
|
12
12
|
private blockId: string;
|
|
13
13
|
private newName: string;
|
|
14
14
|
private oldName: string | undefined;
|
|
15
|
-
private block: Block | undefined;
|
|
16
15
|
private emit: EngineEmitFn;
|
|
17
16
|
|
|
18
|
-
constructor(
|
|
19
|
-
this.
|
|
17
|
+
constructor(getPage: () => Page, options: SetBlockNameOptions) {
|
|
18
|
+
this.getPage = getPage;
|
|
20
19
|
this.blockId = options.blockId;
|
|
21
20
|
this.newName = options.name;
|
|
22
21
|
this.emit = options.emit;
|
|
23
|
-
this.block = this.page.blocks[this.blockId];
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
const block = this.getPage().blocks[this.blockId];
|
|
24
|
+
|
|
25
|
+
if (!block) {
|
|
26
26
|
throw new Error(`Block not found: ${this.blockId}`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
this.oldName =
|
|
29
|
+
this.oldName = block.name;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
apply(): void {
|
|
33
|
-
|
|
33
|
+
const block = this.getPage().blocks[this.blockId];
|
|
34
|
+
|
|
35
|
+
if (!block) {
|
|
34
36
|
throw new Error(`Block not found: ${this.blockId}`);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
block.name = this.newName;
|
|
38
40
|
|
|
39
41
|
this.emit('block:update', {
|
|
40
42
|
blockId: this.blockId,
|
|
41
|
-
block
|
|
43
|
+
block,
|
|
42
44
|
property: 'name',
|
|
43
45
|
value: this.newName,
|
|
44
46
|
oldValue: this.oldName,
|
|
@@ -46,15 +48,17 @@ export class SetBlockNameCommand implements Command {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
revert(): void {
|
|
49
|
-
|
|
51
|
+
const block = this.getPage().blocks[this.blockId];
|
|
52
|
+
|
|
53
|
+
if (!block) {
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
block.name = this.oldName;
|
|
54
58
|
|
|
55
59
|
this.emit('block:update', {
|
|
56
60
|
blockId: this.blockId,
|
|
57
|
-
block
|
|
61
|
+
block,
|
|
58
62
|
property: 'name',
|
|
59
63
|
value: this.oldName,
|
|
60
64
|
oldValue: this.newName,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Page } from '@craftile/types';
|
|
2
2
|
import type { Command, EngineEmitFn } from '../types';
|
|
3
3
|
|
|
4
4
|
export interface SetBlockPropertyOptions {
|
|
@@ -9,16 +9,15 @@ export interface SetBlockPropertyOptions {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export class SetBlockPropertyCommand implements Command {
|
|
12
|
-
private
|
|
12
|
+
private getPage: () => Page;
|
|
13
13
|
private blockId: string;
|
|
14
14
|
private propertyKey: string;
|
|
15
15
|
private propertyValue: any;
|
|
16
16
|
private originalValue: any;
|
|
17
|
-
private block?: Block;
|
|
18
17
|
private emit: EngineEmitFn;
|
|
19
18
|
|
|
20
|
-
constructor(
|
|
21
|
-
this.
|
|
19
|
+
constructor(getPage: () => Page, options: SetBlockPropertyOptions) {
|
|
20
|
+
this.getPage = getPage;
|
|
22
21
|
this.blockId = options.blockId;
|
|
23
22
|
this.propertyKey = options.propertyKey;
|
|
24
23
|
this.propertyValue = options.propertyValue;
|
|
@@ -26,18 +25,18 @@ export class SetBlockPropertyCommand implements Command {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
apply(): void {
|
|
29
|
-
|
|
28
|
+
const block = this.getPage().blocks[this.blockId];
|
|
30
29
|
|
|
31
|
-
if (!
|
|
30
|
+
if (!block) {
|
|
32
31
|
throw new Error(`Block not found: ${this.blockId}`);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
if (!
|
|
36
|
-
|
|
34
|
+
if (!block.properties) {
|
|
35
|
+
block.properties = {};
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
this.originalValue =
|
|
40
|
-
|
|
38
|
+
this.originalValue = block.properties[this.propertyKey];
|
|
39
|
+
block.properties[this.propertyKey] = this.propertyValue;
|
|
41
40
|
|
|
42
41
|
this.emit('block:property:set', {
|
|
43
42
|
blockId: this.blockId,
|
|
@@ -48,14 +47,16 @@ export class SetBlockPropertyCommand implements Command {
|
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
revert(): void {
|
|
51
|
-
|
|
50
|
+
const block = this.getPage().blocks[this.blockId];
|
|
51
|
+
|
|
52
|
+
if (!block || !block.properties) {
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
if (this.originalValue !== undefined) {
|
|
56
|
-
|
|
57
|
+
block.properties[this.propertyKey] = this.originalValue;
|
|
57
58
|
} else {
|
|
58
|
-
delete
|
|
59
|
+
delete block.properties[this.propertyKey];
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
this.emit('block:property:set', {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Page } from '@craftile/types';
|
|
2
2
|
import type { Command, EngineEmitFn } from '../types';
|
|
3
3
|
|
|
4
4
|
export interface ToggleBlockOptions {
|
|
@@ -8,56 +8,60 @@ export interface ToggleBlockOptions {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export class ToggleBlockCommand implements Command {
|
|
11
|
-
private
|
|
11
|
+
private getPage: () => Page;
|
|
12
12
|
private blockId: string;
|
|
13
13
|
private targetDisabled?: boolean;
|
|
14
14
|
private originalDisabled?: boolean;
|
|
15
|
-
private
|
|
15
|
+
private newDisabled?: boolean;
|
|
16
16
|
private emit: EngineEmitFn;
|
|
17
17
|
|
|
18
|
-
constructor(
|
|
19
|
-
this.
|
|
18
|
+
constructor(getPage: () => Page, options: ToggleBlockOptions) {
|
|
19
|
+
this.getPage = getPage;
|
|
20
20
|
this.blockId = options.blockId;
|
|
21
21
|
this.targetDisabled = options.disabled;
|
|
22
22
|
this.emit = options.emit;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
apply(): void {
|
|
26
|
-
|
|
26
|
+
const block = this.getPage().blocks[this.blockId];
|
|
27
27
|
|
|
28
|
-
if (!
|
|
28
|
+
if (!block) {
|
|
29
29
|
throw new Error(`Block not found: ${this.blockId}`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
this.originalDisabled =
|
|
32
|
+
this.originalDisabled = block.disabled;
|
|
33
33
|
|
|
34
34
|
if (this.targetDisabled !== undefined) {
|
|
35
|
-
|
|
35
|
+
block.disabled = this.targetDisabled;
|
|
36
36
|
} else {
|
|
37
|
-
|
|
37
|
+
block.disabled = !block.disabled;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
this.newDisabled = block.disabled;
|
|
41
|
+
|
|
40
42
|
this.emit('block:toggle', {
|
|
41
43
|
blockId: this.blockId,
|
|
42
|
-
disabled:
|
|
44
|
+
disabled: block.disabled ?? false,
|
|
43
45
|
oldValue: this.originalDisabled,
|
|
44
46
|
});
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
revert(): void {
|
|
48
|
-
|
|
50
|
+
const block = this.getPage().blocks[this.blockId];
|
|
51
|
+
|
|
52
|
+
if (!block) {
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
if (this.originalDisabled !== undefined) {
|
|
53
|
-
|
|
57
|
+
block.disabled = this.originalDisabled;
|
|
54
58
|
} else {
|
|
55
|
-
|
|
59
|
+
block.disabled = !block.disabled;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
this.emit('block:toggle', {
|
|
59
63
|
blockId: this.blockId,
|
|
60
|
-
disabled:
|
|
64
|
+
disabled: block.disabled ?? false,
|
|
61
65
|
oldValue: this.targetDisabled !== undefined ? this.targetDisabled : !this.originalDisabled,
|
|
62
66
|
});
|
|
63
67
|
}
|
|
@@ -71,6 +75,6 @@ export class ToggleBlockCommand implements Command {
|
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
getNewDisabled(): boolean | undefined {
|
|
74
|
-
return this.
|
|
78
|
+
return this.newDisabled;
|
|
75
79
|
}
|
|
76
80
|
}
|
package/src/engine.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Command, EngineConfig, EngineEvents } from './types';
|
|
|
3
3
|
import type { Block, BlockSchema, BlockStructure, Page } from '@craftile/types';
|
|
4
4
|
import { BlocksManager } from './blocks-manager';
|
|
5
5
|
import { HistoryManager } from './history-manager';
|
|
6
|
+
import { collectVanishedDescendants } from './utils';
|
|
6
7
|
import { BatchCommand } from './commands/batch';
|
|
7
8
|
import { InsertBlockCommand } from './commands/insert-block';
|
|
8
9
|
import { InsertBlockFromPresetCommand } from './commands/insert-block-from-preset';
|
|
@@ -71,7 +72,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
const command = new InsertBlockCommand(this.
|
|
75
|
+
const command = new InsertBlockCommand(this.getLivePage, {
|
|
75
76
|
blockType,
|
|
76
77
|
parentId: options?.parentId,
|
|
77
78
|
regionId: options?.regionId,
|
|
@@ -127,7 +128,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
const command = new InsertBlockFromPresetCommand(this.
|
|
131
|
+
const command = new InsertBlockFromPresetCommand(this.getLivePage, {
|
|
131
132
|
blockType,
|
|
132
133
|
presetIndex,
|
|
133
134
|
parentId: options?.parentId,
|
|
@@ -150,7 +151,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
150
151
|
* @emits block:remove - When the block is successfully removed
|
|
151
152
|
*/
|
|
152
153
|
removeBlock(blockId: string): void {
|
|
153
|
-
const command = new RemoveBlockCommand(this.
|
|
154
|
+
const command = new RemoveBlockCommand(this.getLivePage, {
|
|
154
155
|
blockId,
|
|
155
156
|
emit: this.emit.bind(this),
|
|
156
157
|
});
|
|
@@ -188,7 +189,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
const command = new MoveBlockCommand(this.
|
|
192
|
+
const command = new MoveBlockCommand(this.getLivePage, {
|
|
192
193
|
blockId,
|
|
193
194
|
targetParentId: options?.targetParentId,
|
|
194
195
|
targetIndex: options?.targetIndex,
|
|
@@ -203,7 +204,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
203
204
|
* Toggle a block's disabled state (enable/disable)
|
|
204
205
|
*/
|
|
205
206
|
toggleBlock(blockId: string, disabled?: boolean): void {
|
|
206
|
-
const command = new ToggleBlockCommand(this.
|
|
207
|
+
const command = new ToggleBlockCommand(this.getLivePage, {
|
|
207
208
|
blockId,
|
|
208
209
|
disabled,
|
|
209
210
|
emit: this.emit.bind(this),
|
|
@@ -222,7 +223,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
222
223
|
* @emits block:property:set - When the property is successfully set
|
|
223
224
|
*/
|
|
224
225
|
setBlockProperty(blockId: string, propertyKey: string, propertyValue: any): void {
|
|
225
|
-
const command = new SetBlockPropertyCommand(this.
|
|
226
|
+
const command = new SetBlockPropertyCommand(this.getLivePage, {
|
|
226
227
|
blockId,
|
|
227
228
|
propertyKey,
|
|
228
229
|
propertyValue,
|
|
@@ -241,7 +242,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
241
242
|
* @emits block:update - When the name is successfully set
|
|
242
243
|
*/
|
|
243
244
|
setBlockName(blockId: string, name: string): void {
|
|
244
|
-
const command = new SetBlockNameCommand(this.
|
|
245
|
+
const command = new SetBlockNameCommand(this.getLivePage, {
|
|
245
246
|
blockId,
|
|
246
247
|
name,
|
|
247
248
|
emit: this.emit.bind(this),
|
|
@@ -254,7 +255,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
254
255
|
* Duplicate a block (creates a copy placed right after the original)
|
|
255
256
|
*/
|
|
256
257
|
duplicateBlock(blockId: string): string {
|
|
257
|
-
const command = new DuplicateBlockCommand(this.
|
|
258
|
+
const command = new DuplicateBlockCommand(this.getLivePage, {
|
|
258
259
|
blockId,
|
|
259
260
|
emit: this.emit.bind(this),
|
|
260
261
|
});
|
|
@@ -278,15 +279,9 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
278
279
|
throw new Error(`Block not found: ${blockId}`);
|
|
279
280
|
}
|
|
280
281
|
|
|
282
|
+
const { parentId: _parentId, children: _children, ...rest } = structuredClone(block);
|
|
281
283
|
const structure: BlockStructure = {
|
|
282
|
-
|
|
283
|
-
id: block.id,
|
|
284
|
-
semanticId: block.semanticId,
|
|
285
|
-
properties: structuredClone(block.properties),
|
|
286
|
-
name: block.name,
|
|
287
|
-
static: block.static,
|
|
288
|
-
disabled: block.disabled,
|
|
289
|
-
repeated: block.repeated,
|
|
284
|
+
...rest,
|
|
290
285
|
children: [],
|
|
291
286
|
};
|
|
292
287
|
|
|
@@ -333,7 +328,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
333
328
|
}
|
|
334
329
|
}
|
|
335
330
|
|
|
336
|
-
const command = new InsertBlockFromPresetCommand(this.
|
|
331
|
+
const command = new InsertBlockFromPresetCommand(this.getLivePage, {
|
|
337
332
|
blockType: structure.type,
|
|
338
333
|
presetData: structure,
|
|
339
334
|
parentId: options?.parentId,
|
|
@@ -403,7 +398,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
403
398
|
throw new Error('replaceRegion cannot be called inside batch');
|
|
404
399
|
}
|
|
405
400
|
|
|
406
|
-
const command = new ReplaceRegionCommand(this.
|
|
401
|
+
const command = new ReplaceRegionCommand(this.getLivePage, {
|
|
407
402
|
regionId,
|
|
408
403
|
structures,
|
|
409
404
|
blocksManager: this.blocksManager,
|
|
@@ -413,6 +408,48 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
413
408
|
this.applyCommand(command);
|
|
414
409
|
}
|
|
415
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Merge externally resolved blocks into the live page.
|
|
413
|
+
*
|
|
414
|
+
* Each given block replaces the existing one with the same id wholesale (no deep merge).
|
|
415
|
+
* Blocks are used as given: the caller supplies `parentId` and `children`, no parent
|
|
416
|
+
* back-fill or validation runs. When a patched block drops ids from its previous `children`,
|
|
417
|
+
* those blocks vanish with their whole subtree unless they are present in the patch.
|
|
418
|
+
* Regions are untouched. The page is mutated in place and history is neither cleared nor
|
|
419
|
+
* extended, so this operation is not undoable.
|
|
420
|
+
*
|
|
421
|
+
* @param blocks - Blocks keyed by id
|
|
422
|
+
* @throws {Error} When called inside a batch
|
|
423
|
+
* @emits blocks:patch - When at least one block was given
|
|
424
|
+
*/
|
|
425
|
+
patchBlocks(blocks: Record<string, Block>): void {
|
|
426
|
+
if (this.activeBatch) {
|
|
427
|
+
throw new Error('patchBlocks cannot be called inside batch');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (Object.keys(blocks).length === 0) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const previousPage = structuredClone(this.page);
|
|
435
|
+
const removed = [...collectVanishedDescendants(this.page.blocks, blocks)];
|
|
436
|
+
|
|
437
|
+
for (const blockId of removed) {
|
|
438
|
+
delete this.page.blocks[blockId];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const [blockId, block] of Object.entries(blocks)) {
|
|
442
|
+
this.page.blocks[blockId] = structuredClone(block);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
this.emit('blocks:patch', {
|
|
446
|
+
blocks,
|
|
447
|
+
removed,
|
|
448
|
+
previousPage,
|
|
449
|
+
newPage: structuredClone(this.page),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
416
453
|
/**
|
|
417
454
|
* Get the blocks manager for this engine instance
|
|
418
455
|
*/
|
|
@@ -544,6 +581,10 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
544
581
|
this.page = page;
|
|
545
582
|
}
|
|
546
583
|
|
|
584
|
+
private getLivePage = (): Page => {
|
|
585
|
+
return this.page;
|
|
586
|
+
};
|
|
587
|
+
|
|
547
588
|
private applyCommand(command: Command): void {
|
|
548
589
|
command.apply();
|
|
549
590
|
this.recordCommand(command);
|
package/src/types.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -105,3 +105,46 @@ export const canInsertDynamicChildAt = (
|
|
|
105
105
|
}
|
|
106
106
|
return index >= first && index <= last + 1;
|
|
107
107
|
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Every block in a patch carries an authoritative `children` list. Ids in a patched block's
|
|
111
|
+
* previous `children` that are missing from its new `children` have vanished, together with
|
|
112
|
+
* their whole subtree (walked through `previousBlocks`). Blocks present in the patch are never
|
|
113
|
+
* pruned, even when their previous parent dropped them.
|
|
114
|
+
*/
|
|
115
|
+
export const collectVanishedDescendants = (
|
|
116
|
+
previousBlocks: Record<string, Block>,
|
|
117
|
+
patchBlocks: Record<string, Block>
|
|
118
|
+
): Set<string> => {
|
|
119
|
+
const vanished = new Set<string>();
|
|
120
|
+
|
|
121
|
+
const collectSubtree = (blockId: string): void => {
|
|
122
|
+
if (patchBlocks[blockId] || vanished.has(blockId)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
vanished.add(blockId);
|
|
127
|
+
|
|
128
|
+
for (const childId of previousBlocks[blockId]?.children ?? []) {
|
|
129
|
+
collectSubtree(childId);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const [blockId, patchBlock] of Object.entries(patchBlocks)) {
|
|
134
|
+
const previousBlock = previousBlocks[blockId];
|
|
135
|
+
|
|
136
|
+
if (!previousBlock) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const keptChildren = new Set(patchBlock.children ?? []);
|
|
141
|
+
|
|
142
|
+
for (const childId of previousBlock.children ?? []) {
|
|
143
|
+
if (!keptChildren.has(childId)) {
|
|
144
|
+
collectSubtree(childId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return vanished;
|
|
150
|
+
};
|
|
@@ -64,6 +64,7 @@ export interface UseBlocksEngineReturn {
|
|
|
64
64
|
) => string;
|
|
65
65
|
replacePage: (newPage: Page) => void;
|
|
66
66
|
replaceRegion: (regionId: string, structures: BlockStructure[]) => void;
|
|
67
|
+
patchBlocks: (blocks: Record<string, Block>) => void;
|
|
67
68
|
|
|
68
69
|
undo: () => boolean;
|
|
69
70
|
redo: () => boolean;
|
|
@@ -183,6 +184,7 @@ export function useBlocksEngine(
|
|
|
183
184
|
'block:duplicate',
|
|
184
185
|
'block:property:set',
|
|
185
186
|
'block:update',
|
|
187
|
+
'blocks:patch',
|
|
186
188
|
'undo',
|
|
187
189
|
'redo',
|
|
188
190
|
] as const;
|
|
@@ -321,6 +323,14 @@ export function useBlocksEngine(
|
|
|
321
323
|
}
|
|
322
324
|
};
|
|
323
325
|
|
|
326
|
+
const patchBlocks = (blocks: Record<string, Block>): void => {
|
|
327
|
+
engine.patchBlocks(blocks);
|
|
328
|
+
|
|
329
|
+
if (!autoSync) {
|
|
330
|
+
syncStateFromEngine();
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
324
334
|
const undo = (): boolean => {
|
|
325
335
|
const result = engine.undo();
|
|
326
336
|
|
|
@@ -393,6 +403,7 @@ export function useBlocksEngine(
|
|
|
393
403
|
pasteBlock,
|
|
394
404
|
replacePage,
|
|
395
405
|
replaceRegion,
|
|
406
|
+
patchBlocks,
|
|
396
407
|
|
|
397
408
|
// History methods
|
|
398
409
|
undo,
|