@craftile/core 1.2.1 → 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 +25 -21
- 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 +15 -11
- 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 +57 -10
- 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,7 +69,8 @@ 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);
|
|
@@ -86,11 +87,11 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
86
87
|
parentId: undefined,
|
|
87
88
|
};
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
page.blocks[this.blockId] = this.insertedBlock;
|
|
90
91
|
this.createdBlockIds.push(this.blockId);
|
|
91
92
|
|
|
92
93
|
if (this.presetData.children && this.presetData.children.length > 0) {
|
|
93
|
-
this.insertedBlock.children = this.createChildrenFromPreset(this.presetData.children, this.blockId);
|
|
94
|
+
this.insertedBlock.children = this.createChildrenFromPreset(page, this.presetData.children, this.blockId);
|
|
94
95
|
}
|
|
95
96
|
} else if (this.presetIndex !== undefined) {
|
|
96
97
|
const preset = blockSchema?.presets?.[this.presetIndex];
|
|
@@ -107,11 +108,11 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
107
108
|
parentId: undefined,
|
|
108
109
|
};
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
page.blocks[this.blockId] = this.insertedBlock;
|
|
111
112
|
this.createdBlockIds.push(this.blockId);
|
|
112
113
|
|
|
113
114
|
if (preset.children && preset.children.length > 0) {
|
|
114
|
-
this.insertedBlock.children = this.createChildrenFromPreset(preset.children, this.blockId);
|
|
115
|
+
this.insertedBlock.children = this.createChildrenFromPreset(page, preset.children, this.blockId);
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -125,7 +126,7 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
125
126
|
this.insertedBlock.parentId = target.parent.id;
|
|
126
127
|
target.parent.children.splice(target.index, 0, this.blockId);
|
|
127
128
|
} else {
|
|
128
|
-
const region =
|
|
129
|
+
const region = page.regions.find((r) => getRegionId(r) === target.regionId)!;
|
|
129
130
|
region.blocks.splice(target.index, 0, this.blockId);
|
|
130
131
|
}
|
|
131
132
|
|
|
@@ -134,7 +135,7 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
134
135
|
block: this.insertedBlock,
|
|
135
136
|
parentId: this.parentId,
|
|
136
137
|
index: this.actualIndex,
|
|
137
|
-
regionId: this.regionId || getRegionId(
|
|
138
|
+
regionId: this.regionId || getRegionId(page.regions[0]),
|
|
138
139
|
});
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -143,21 +144,24 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
143
144
|
return;
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
const page = this.getPage();
|
|
148
|
+
|
|
146
149
|
// Remove all created blocks
|
|
147
150
|
for (const blockId of this.createdBlockIds) {
|
|
148
|
-
delete
|
|
151
|
+
delete page.blocks[blockId];
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
if (this.parentId) {
|
|
152
|
-
const parent =
|
|
153
|
-
|
|
154
|
-
|
|
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);
|
|
155
159
|
}
|
|
156
160
|
} else if (this.resolvedRegionId) {
|
|
157
|
-
const targetRegion =
|
|
158
|
-
|
|
159
|
-
if (targetRegion &&
|
|
160
|
-
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);
|
|
161
165
|
}
|
|
162
166
|
}
|
|
163
167
|
|
|
@@ -177,7 +181,7 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
177
181
|
return this.insertedBlock;
|
|
178
182
|
}
|
|
179
183
|
|
|
180
|
-
private createChildrenFromPreset(structures: BlockStructure[], parentId: string): string[] {
|
|
184
|
+
private createChildrenFromPreset(page: Page, structures: BlockStructure[], parentId: string): string[] {
|
|
181
185
|
const childIds: string[] = [];
|
|
182
186
|
|
|
183
187
|
for (const structure of structures) {
|
|
@@ -202,12 +206,12 @@ export class InsertBlockFromPresetCommand implements Command {
|
|
|
202
206
|
parentId,
|
|
203
207
|
};
|
|
204
208
|
|
|
205
|
-
|
|
209
|
+
page.blocks[childId] = childBlock;
|
|
206
210
|
this.createdBlockIds.push(childId);
|
|
207
211
|
|
|
208
212
|
// Recursively create nested children
|
|
209
213
|
if (structure.children && structure.children.length > 0) {
|
|
210
|
-
childBlock.children = this.createChildrenFromPreset(structure.children, childId);
|
|
214
|
+
childBlock.children = this.createChildrenFromPreset(page, structure.children, childId);
|
|
211
215
|
}
|
|
212
216
|
|
|
213
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) =>
|
|
@@ -97,11 +99,11 @@ export class ReplaceRegionCommand implements Command {
|
|
|
97
99
|
return builtProperties;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
private collectBlocksForRoots(rootBlockIds: string[]): Record<string, Block> {
|
|
102
|
+
private collectBlocksForRoots(page: Page, rootBlockIds: string[]): Record<string, Block> {
|
|
101
103
|
const blocks: Record<string, Block> = {};
|
|
102
104
|
|
|
103
105
|
const collectBlock = (blockId: string) => {
|
|
104
|
-
const block =
|
|
106
|
+
const block = page.blocks[blockId];
|
|
105
107
|
if (!block || blocks[blockId]) {
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
@@ -121,15 +123,17 @@ export class ReplaceRegionCommand implements Command {
|
|
|
121
123
|
removedBlocks: Record<string, Block>,
|
|
122
124
|
newBlocks: Record<string, Block>
|
|
123
125
|
): void {
|
|
126
|
+
const page = this.getPage();
|
|
127
|
+
|
|
124
128
|
Object.keys(removedBlocks).forEach((blockId) => {
|
|
125
|
-
delete
|
|
129
|
+
delete page.blocks[blockId];
|
|
126
130
|
});
|
|
127
131
|
|
|
128
132
|
Object.entries(newBlocks).forEach(([blockId, block]) => {
|
|
129
|
-
|
|
133
|
+
page.blocks[blockId] = block;
|
|
130
134
|
});
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
page.regions[this.regionIndex] = newRegion;
|
|
133
137
|
|
|
134
138
|
this.emit('region:replace', {
|
|
135
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
|
});
|
|
@@ -327,7 +328,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
327
328
|
}
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
const command = new InsertBlockFromPresetCommand(this.
|
|
331
|
+
const command = new InsertBlockFromPresetCommand(this.getLivePage, {
|
|
331
332
|
blockType: structure.type,
|
|
332
333
|
presetData: structure,
|
|
333
334
|
parentId: options?.parentId,
|
|
@@ -397,7 +398,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
397
398
|
throw new Error('replaceRegion cannot be called inside batch');
|
|
398
399
|
}
|
|
399
400
|
|
|
400
|
-
const command = new ReplaceRegionCommand(this.
|
|
401
|
+
const command = new ReplaceRegionCommand(this.getLivePage, {
|
|
401
402
|
regionId,
|
|
402
403
|
structures,
|
|
403
404
|
blocksManager: this.blocksManager,
|
|
@@ -407,6 +408,48 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
407
408
|
this.applyCommand(command);
|
|
408
409
|
}
|
|
409
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
|
+
|
|
410
453
|
/**
|
|
411
454
|
* Get the blocks manager for this engine instance
|
|
412
455
|
*/
|
|
@@ -538,6 +581,10 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
538
581
|
this.page = page;
|
|
539
582
|
}
|
|
540
583
|
|
|
584
|
+
private getLivePage = (): Page => {
|
|
585
|
+
return this.page;
|
|
586
|
+
};
|
|
587
|
+
|
|
541
588
|
private applyCommand(command: Command): void {
|
|
542
589
|
command.apply();
|
|
543
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,
|