@craftile/core 1.0.9 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/commands/batch.ts +19 -0
- package/src/commands/replace-page.ts +34 -0
- package/src/commands/replace-region.ts +145 -0
- package/src/engine.ts +120 -44
- package/src/types.ts +9 -1
- package/src/vue/useBlocksEngine.ts +40 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craftile/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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.1.1",
|
|
30
|
+
"@craftile/types": "1.1.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"vitest": "^3.2.4",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Command } from '../types';
|
|
2
|
+
|
|
3
|
+
export class BatchCommand implements Command {
|
|
4
|
+
private commands: Command[];
|
|
5
|
+
|
|
6
|
+
constructor(commands: Command[]) {
|
|
7
|
+
this.commands = commands;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
apply(): void {
|
|
11
|
+
this.commands.forEach((command) => command.apply());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
revert(): void {
|
|
15
|
+
for (let i = this.commands.length - 1; i >= 0; i--) {
|
|
16
|
+
this.commands[i].revert();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Page } from '@craftile/types';
|
|
2
|
+
import type { Command, EngineEmitFn } from '../types';
|
|
3
|
+
|
|
4
|
+
export class ReplacePageCommand implements Command {
|
|
5
|
+
private previousPage: Page;
|
|
6
|
+
private newPage: Page;
|
|
7
|
+
private setPageState: (page: Page) => void;
|
|
8
|
+
private emit: EngineEmitFn;
|
|
9
|
+
|
|
10
|
+
constructor(previousPage: Page, newPage: Page, setPageState: (page: Page) => void, emit: EngineEmitFn) {
|
|
11
|
+
this.previousPage = previousPage;
|
|
12
|
+
this.newPage = newPage;
|
|
13
|
+
this.setPageState = setPageState;
|
|
14
|
+
this.emit = emit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
apply(): void {
|
|
18
|
+
this.setPageState(this.newPage);
|
|
19
|
+
|
|
20
|
+
this.emit('page:replace', {
|
|
21
|
+
previousPage: structuredClone(this.previousPage),
|
|
22
|
+
newPage: structuredClone(this.newPage),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
revert(): void {
|
|
27
|
+
this.setPageState(this.previousPage);
|
|
28
|
+
|
|
29
|
+
this.emit('page:replace', {
|
|
30
|
+
previousPage: structuredClone(this.newPage),
|
|
31
|
+
newPage: structuredClone(this.previousPage),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Block, BlockSchema, BlockStructure, Page } from '@craftile/types';
|
|
2
|
+
import type { BlocksManager } from '../blocks-manager';
|
|
3
|
+
import type { Command, EngineEmitFn } from '../types';
|
|
4
|
+
import { generateId, getRegionId } from '../utils';
|
|
5
|
+
|
|
6
|
+
export interface ReplaceRegionOptions {
|
|
7
|
+
regionId: string;
|
|
8
|
+
structures: BlockStructure[];
|
|
9
|
+
blocksManager: BlocksManager;
|
|
10
|
+
emit: EngineEmitFn;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ReplaceRegionCommand implements Command {
|
|
14
|
+
private page: Page;
|
|
15
|
+
private regionId: string;
|
|
16
|
+
private regionIndex: number;
|
|
17
|
+
private previousRegion: Page['regions'][number];
|
|
18
|
+
private newRegion: Page['regions'][number];
|
|
19
|
+
private removedBlocks: Record<string, Block>;
|
|
20
|
+
private newBlocks: Record<string, Block>;
|
|
21
|
+
private emit: EngineEmitFn;
|
|
22
|
+
private blocksManager: BlocksManager;
|
|
23
|
+
|
|
24
|
+
constructor(page: Page, options: ReplaceRegionOptions) {
|
|
25
|
+
this.page = page;
|
|
26
|
+
this.regionId = options.regionId;
|
|
27
|
+
this.blocksManager = options.blocksManager;
|
|
28
|
+
this.emit = options.emit;
|
|
29
|
+
|
|
30
|
+
this.regionIndex = this.page.regions.findIndex((region) => getRegionId(region) === this.regionId);
|
|
31
|
+
if (this.regionIndex === -1) {
|
|
32
|
+
throw new Error(`Region not found: ${this.regionId}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.previousRegion = structuredClone(this.page.regions[this.regionIndex]);
|
|
36
|
+
this.removedBlocks = this.collectBlocksForRoots(this.previousRegion.blocks);
|
|
37
|
+
|
|
38
|
+
this.newBlocks = {};
|
|
39
|
+
const newRootIds = options.structures.map((structure) =>
|
|
40
|
+
this.createBlockFromStructure(structure, undefined, this.newBlocks)
|
|
41
|
+
);
|
|
42
|
+
this.newRegion = {
|
|
43
|
+
...this.previousRegion,
|
|
44
|
+
blocks: newRootIds,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
apply(): void {
|
|
49
|
+
this.replaceRegion(this.previousRegion, this.newRegion, this.removedBlocks, this.newBlocks);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
revert(): void {
|
|
53
|
+
this.replaceRegion(this.newRegion, this.previousRegion, this.newBlocks, this.removedBlocks);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private createBlockFromStructure(
|
|
57
|
+
structure: BlockStructure,
|
|
58
|
+
parentId: string | undefined,
|
|
59
|
+
target: Record<string, Block>
|
|
60
|
+
): string {
|
|
61
|
+
const blockSchema = this.blocksManager.get(structure.type);
|
|
62
|
+
if (!blockSchema) {
|
|
63
|
+
throw new Error(`Block type '${structure.type}' is not registered`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const blockId = generateId();
|
|
67
|
+
const block: Block = {
|
|
68
|
+
type: structure.type,
|
|
69
|
+
id: blockId,
|
|
70
|
+
name: structure.name || blockSchema.meta?.name || structure.type,
|
|
71
|
+
semanticId: structure.semanticId || structure.id,
|
|
72
|
+
properties: this.buildProperties(blockSchema, structure.properties),
|
|
73
|
+
children: [],
|
|
74
|
+
parentId,
|
|
75
|
+
static: structure.static,
|
|
76
|
+
disabled: structure.disabled,
|
|
77
|
+
repeated: structure.repeated,
|
|
78
|
+
ghost: structure.ghost,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
target[blockId] = block;
|
|
82
|
+
block.children = (structure.children ?? []).map((child) => this.createBlockFromStructure(child, blockId, target));
|
|
83
|
+
|
|
84
|
+
return blockId;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private buildProperties(blockSchema: BlockSchema, properties?: Record<string, any>): Record<string, any> {
|
|
88
|
+
const builtProperties: Record<string, any> = {};
|
|
89
|
+
|
|
90
|
+
for (const field of blockSchema.properties ?? []) {
|
|
91
|
+
if (field.default !== undefined) {
|
|
92
|
+
builtProperties[field.id] = field.default;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (properties) {
|
|
97
|
+
Object.assign(builtProperties, properties);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return builtProperties;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private collectBlocksForRoots(rootBlockIds: string[]): Record<string, Block> {
|
|
104
|
+
const blocks: Record<string, Block> = {};
|
|
105
|
+
|
|
106
|
+
const collectBlock = (blockId: string) => {
|
|
107
|
+
const block = this.page.blocks[blockId];
|
|
108
|
+
if (!block || blocks[blockId]) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
blocks[blockId] = block;
|
|
113
|
+
block.children.forEach(collectBlock);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
rootBlockIds.forEach(collectBlock);
|
|
117
|
+
|
|
118
|
+
return blocks;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private replaceRegion(
|
|
122
|
+
previousRegion: Page['regions'][number],
|
|
123
|
+
newRegion: Page['regions'][number],
|
|
124
|
+
removedBlocks: Record<string, Block>,
|
|
125
|
+
newBlocks: Record<string, Block>
|
|
126
|
+
): void {
|
|
127
|
+
Object.keys(removedBlocks).forEach((blockId) => {
|
|
128
|
+
delete this.page.blocks[blockId];
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
Object.entries(newBlocks).forEach(([blockId, block]) => {
|
|
132
|
+
this.page.blocks[blockId] = block;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.page.regions[this.regionIndex] = newRegion;
|
|
136
|
+
|
|
137
|
+
this.emit('region:replace', {
|
|
138
|
+
regionId: this.regionId,
|
|
139
|
+
previousRegion: structuredClone(previousRegion),
|
|
140
|
+
newRegion: structuredClone(newRegion),
|
|
141
|
+
removedBlocks: structuredClone(removedBlocks),
|
|
142
|
+
newBlocks: structuredClone(newBlocks),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
package/src/engine.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { EventBus } from '@craftile/event-bus';
|
|
2
|
-
import type { EngineConfig, EngineEvents } from './types';
|
|
2
|
+
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 { BatchCommand } from './commands/batch';
|
|
6
7
|
import { InsertBlockCommand } from './commands/insert-block';
|
|
7
8
|
import { InsertBlockFromPresetCommand } from './commands/insert-block-from-preset';
|
|
8
9
|
import { RemoveBlockCommand } from './commands/remove-block';
|
|
@@ -11,11 +12,14 @@ import { ToggleBlockCommand } from './commands/toggle-block';
|
|
|
11
12
|
import { SetBlockPropertyCommand } from './commands/set-block-property';
|
|
12
13
|
import { DuplicateBlockCommand } from './commands/duplicate-block';
|
|
13
14
|
import { SetBlockNameCommand } from './commands/set-block-name';
|
|
15
|
+
import { ReplacePageCommand } from './commands/replace-page';
|
|
16
|
+
import { ReplaceRegionCommand } from './commands/replace-region';
|
|
14
17
|
|
|
15
18
|
export class Engine extends EventBus<EngineEvents> {
|
|
16
19
|
protected page!: Page;
|
|
17
20
|
protected blocksManager: BlocksManager;
|
|
18
21
|
protected historyManager: HistoryManager;
|
|
22
|
+
private activeBatch: Command[] | null = null;
|
|
19
23
|
|
|
20
24
|
constructor(config: EngineConfig = {}) {
|
|
21
25
|
super();
|
|
@@ -76,8 +80,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
76
80
|
emit: this.emit.bind(this),
|
|
77
81
|
});
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
this.historyManager.addCommand(command);
|
|
83
|
+
this.applyCommand(command);
|
|
81
84
|
|
|
82
85
|
return command.getBlockId();
|
|
83
86
|
}
|
|
@@ -134,8 +137,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
134
137
|
emit: this.emit.bind(this),
|
|
135
138
|
});
|
|
136
139
|
|
|
137
|
-
|
|
138
|
-
this.historyManager.addCommand(command);
|
|
140
|
+
this.applyCommand(command);
|
|
139
141
|
|
|
140
142
|
return command.getBlockId();
|
|
141
143
|
}
|
|
@@ -153,8 +155,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
153
155
|
emit: this.emit.bind(this),
|
|
154
156
|
});
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
this.historyManager.addCommand(command);
|
|
158
|
+
this.applyCommand(command);
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
/**
|
|
@@ -195,8 +196,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
195
196
|
emit: this.emit.bind(this),
|
|
196
197
|
});
|
|
197
198
|
|
|
198
|
-
|
|
199
|
-
this.historyManager.addCommand(command);
|
|
199
|
+
this.applyCommand(command);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
@@ -209,8 +209,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
209
209
|
emit: this.emit.bind(this),
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
this.historyManager.addCommand(command);
|
|
212
|
+
this.applyCommand(command);
|
|
214
213
|
}
|
|
215
214
|
|
|
216
215
|
/**
|
|
@@ -230,8 +229,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
230
229
|
emit: this.emit.bind(this),
|
|
231
230
|
});
|
|
232
231
|
|
|
233
|
-
|
|
234
|
-
this.historyManager.addCommand(command);
|
|
232
|
+
this.applyCommand(command);
|
|
235
233
|
}
|
|
236
234
|
|
|
237
235
|
/**
|
|
@@ -249,8 +247,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
249
247
|
emit: this.emit.bind(this),
|
|
250
248
|
});
|
|
251
249
|
|
|
252
|
-
|
|
253
|
-
this.historyManager.addCommand(command);
|
|
250
|
+
this.applyCommand(command);
|
|
254
251
|
}
|
|
255
252
|
|
|
256
253
|
/**
|
|
@@ -262,8 +259,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
262
259
|
emit: this.emit.bind(this),
|
|
263
260
|
});
|
|
264
261
|
|
|
265
|
-
|
|
266
|
-
this.historyManager.addCommand(command);
|
|
262
|
+
this.applyCommand(command);
|
|
267
263
|
|
|
268
264
|
return command.getDuplicatedBlockId();
|
|
269
265
|
}
|
|
@@ -347,8 +343,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
347
343
|
emit: this.emit.bind(this),
|
|
348
344
|
});
|
|
349
345
|
|
|
350
|
-
|
|
351
|
-
this.historyManager.addCommand(command);
|
|
346
|
+
this.applyCommand(command);
|
|
352
347
|
|
|
353
348
|
return command.getBlockId();
|
|
354
349
|
}
|
|
@@ -370,26 +365,7 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
370
365
|
setPage(newPage: Page): void {
|
|
371
366
|
const beforePage = structuredClone(this.page);
|
|
372
367
|
|
|
373
|
-
this.page =
|
|
374
|
-
|
|
375
|
-
if (this.page.regions.length === 0) {
|
|
376
|
-
this.page.regions = [{ id: 'main', name: 'main', blocks: Object.keys(this.page.blocks) }];
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Initialize regions if not present
|
|
380
|
-
if (this.page.regions.length === 0) {
|
|
381
|
-
this.page.regions = [
|
|
382
|
-
{
|
|
383
|
-
id: 'main',
|
|
384
|
-
name: 'main',
|
|
385
|
-
blocks: Object.values(this.page.blocks)
|
|
386
|
-
.filter((block) => !block.parentId)
|
|
387
|
-
.map((block) => block.id),
|
|
388
|
-
},
|
|
389
|
-
];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
this.initializeParentChildRelationships();
|
|
368
|
+
this.page = this.normalizePage(newPage);
|
|
393
369
|
|
|
394
370
|
// Clear history since we're switching to a completely new page
|
|
395
371
|
// maybe we should keep a separate history per page
|
|
@@ -401,6 +377,42 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
401
377
|
});
|
|
402
378
|
}
|
|
403
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Replace the current page as a single undoable operation.
|
|
382
|
+
*/
|
|
383
|
+
replacePage(newPage: Page): void {
|
|
384
|
+
if (this.activeBatch) {
|
|
385
|
+
throw new Error('replacePage cannot be called inside batch');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const command = new ReplacePageCommand(
|
|
389
|
+
this.page,
|
|
390
|
+
this.normalizePage(newPage),
|
|
391
|
+
this.replacePageState.bind(this),
|
|
392
|
+
this.emit.bind(this)
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
this.applyCommand(command);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Replace an existing region's root blocks as a single undoable operation.
|
|
400
|
+
*/
|
|
401
|
+
replaceRegion(regionId: string, structures: BlockStructure[]): void {
|
|
402
|
+
if (this.activeBatch) {
|
|
403
|
+
throw new Error('replaceRegion cannot be called inside batch');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const command = new ReplaceRegionCommand(this.page, {
|
|
407
|
+
regionId,
|
|
408
|
+
structures,
|
|
409
|
+
blocksManager: this.blocksManager,
|
|
410
|
+
emit: this.emit.bind(this),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
this.applyCommand(command);
|
|
414
|
+
}
|
|
415
|
+
|
|
404
416
|
/**
|
|
405
417
|
* Get the blocks manager for this engine instance
|
|
406
418
|
*/
|
|
@@ -466,14 +478,54 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
466
478
|
}
|
|
467
479
|
|
|
468
480
|
/**
|
|
469
|
-
*
|
|
470
|
-
* Handles deep nesting recursively
|
|
481
|
+
* Group multiple operations into a single undo/redo history entry.
|
|
471
482
|
*/
|
|
472
|
-
|
|
483
|
+
batch<T>(callback: () => T): T {
|
|
484
|
+
const isOuterBatch = this.activeBatch === null;
|
|
485
|
+
|
|
486
|
+
if (isOuterBatch) {
|
|
487
|
+
this.activeBatch = [];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const batch = this.activeBatch!;
|
|
491
|
+
const startIndex = batch.length;
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const result = callback();
|
|
495
|
+
|
|
496
|
+
if (isOuterBatch && batch.length > 0) {
|
|
497
|
+
this.historyManager.addCommand(new BatchCommand(batch.slice()));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return result;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
this.rollbackCommands(batch.slice(startIndex));
|
|
503
|
+
batch.splice(startIndex);
|
|
504
|
+
throw error;
|
|
505
|
+
} finally {
|
|
506
|
+
if (isOuterBatch) {
|
|
507
|
+
this.activeBatch = null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private normalizePage(page: Page): Page {
|
|
513
|
+
const normalizedPage = structuredClone(page);
|
|
514
|
+
|
|
515
|
+
if (normalizedPage.regions.length === 0) {
|
|
516
|
+
normalizedPage.regions = [{ id: 'main', name: 'main', blocks: Object.keys(normalizedPage.blocks) }];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
this.initializeParentChildRelationshipsForPage(normalizedPage);
|
|
520
|
+
|
|
521
|
+
return normalizedPage;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private initializeParentChildRelationshipsForPage(page: Page): void {
|
|
473
525
|
const processBlock = (block: Block): void => {
|
|
474
526
|
if (block.children && block.children.length > 0) {
|
|
475
527
|
block.children.forEach((childId: string) => {
|
|
476
|
-
const childBlock =
|
|
528
|
+
const childBlock = page.blocks[childId];
|
|
477
529
|
if (childBlock) {
|
|
478
530
|
if (!childBlock.parentId) {
|
|
479
531
|
childBlock.parentId = block.id;
|
|
@@ -485,6 +537,30 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
485
537
|
}
|
|
486
538
|
};
|
|
487
539
|
|
|
488
|
-
Object.values(
|
|
540
|
+
Object.values(page.blocks).forEach(processBlock);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private replacePageState(page: Page): void {
|
|
544
|
+
this.page = page;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private applyCommand(command: Command): void {
|
|
548
|
+
command.apply();
|
|
549
|
+
this.recordCommand(command);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private recordCommand(command: Command): void {
|
|
553
|
+
if (this.activeBatch) {
|
|
554
|
+
this.activeBatch.push(command);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
this.historyManager.addCommand(command);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private rollbackCommands(commands: Command[]): void {
|
|
562
|
+
for (let i = commands.length - 1; i >= 0; i--) {
|
|
563
|
+
commands[i].revert();
|
|
564
|
+
}
|
|
489
565
|
}
|
|
490
566
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Block, BlockSchema, Page } from '@craftile/types';
|
|
1
|
+
import type { Block, BlockSchema, Page, Region } from '@craftile/types';
|
|
2
2
|
import type { BlocksManager } from './blocks-manager';
|
|
3
3
|
|
|
4
4
|
export interface EngineConfig {
|
|
@@ -15,6 +15,14 @@ export interface Command {
|
|
|
15
15
|
// Event system types
|
|
16
16
|
export interface EngineEvents {
|
|
17
17
|
'page:set': { previousPage?: Page; newPage: Page };
|
|
18
|
+
'page:replace': { previousPage: Page; newPage: Page };
|
|
19
|
+
'region:replace': {
|
|
20
|
+
regionId: string;
|
|
21
|
+
previousRegion: Region;
|
|
22
|
+
newRegion: Region;
|
|
23
|
+
removedBlocks: Record<string, Block>;
|
|
24
|
+
newBlocks: Record<string, Block>;
|
|
25
|
+
};
|
|
18
26
|
'block:insert': { blockId: string; block: Block; parentId?: string; index?: number; regionId?: string };
|
|
19
27
|
'block:remove': { blockId: string; block: Block; parentId?: string; regionId?: string };
|
|
20
28
|
'block:toggle': { blockId: string; disabled: boolean; oldValue?: boolean };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, onUnmounted, computed, toRaw, nextTick, type Ref } from 'vue';
|
|
1
|
+
import { ref, onUnmounted, computed, toRaw, nextTick, getCurrentInstance, type Ref } from 'vue';
|
|
2
2
|
import type { Block, BlockStructure, Page, Region } from '@craftile/types';
|
|
3
3
|
import type { EngineConfig } from '../types';
|
|
4
4
|
import { Engine } from '../engine';
|
|
@@ -62,9 +62,12 @@ export interface UseBlocksEngineReturn {
|
|
|
62
62
|
index?: number;
|
|
63
63
|
}
|
|
64
64
|
) => string;
|
|
65
|
+
replacePage: (newPage: Page) => void;
|
|
66
|
+
replaceRegion: (regionId: string, structures: BlockStructure[]) => void;
|
|
65
67
|
|
|
66
68
|
undo: () => boolean;
|
|
67
69
|
redo: () => boolean;
|
|
70
|
+
batch: <T>(callback: () => T) => T;
|
|
68
71
|
canUndo: Ref<boolean>;
|
|
69
72
|
canRedo: Ref<boolean>;
|
|
70
73
|
|
|
@@ -171,6 +174,8 @@ export function useBlocksEngine(
|
|
|
171
174
|
if (autoSync) {
|
|
172
175
|
const eventTypes = [
|
|
173
176
|
'page:set',
|
|
177
|
+
'page:replace',
|
|
178
|
+
'region:replace',
|
|
174
179
|
'block:insert',
|
|
175
180
|
'block:remove',
|
|
176
181
|
'block:move',
|
|
@@ -300,6 +305,22 @@ export function useBlocksEngine(
|
|
|
300
305
|
return blockId;
|
|
301
306
|
};
|
|
302
307
|
|
|
308
|
+
const replacePage = (newPage: Page): void => {
|
|
309
|
+
engine.replacePage(newPage);
|
|
310
|
+
|
|
311
|
+
if (!autoSync) {
|
|
312
|
+
syncStateFromEngine();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const replaceRegion = (regionId: string, structures: BlockStructure[]): void => {
|
|
317
|
+
engine.replaceRegion(regionId, structures);
|
|
318
|
+
|
|
319
|
+
if (!autoSync) {
|
|
320
|
+
syncStateFromEngine();
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
303
324
|
const undo = (): boolean => {
|
|
304
325
|
const result = engine.undo();
|
|
305
326
|
|
|
@@ -320,6 +341,16 @@ export function useBlocksEngine(
|
|
|
320
341
|
return result;
|
|
321
342
|
};
|
|
322
343
|
|
|
344
|
+
const batch = <T>(callback: () => T): T => {
|
|
345
|
+
const result = engine.batch(callback);
|
|
346
|
+
|
|
347
|
+
if (!autoSync) {
|
|
348
|
+
syncStateFromEngine();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return result;
|
|
352
|
+
};
|
|
353
|
+
|
|
323
354
|
const refresh = () => {
|
|
324
355
|
syncStateFromEngine();
|
|
325
356
|
};
|
|
@@ -329,9 +360,11 @@ export function useBlocksEngine(
|
|
|
329
360
|
eventCleanups.length = 0;
|
|
330
361
|
};
|
|
331
362
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
363
|
+
if (getCurrentInstance()) {
|
|
364
|
+
onUnmounted(() => {
|
|
365
|
+
destroy();
|
|
366
|
+
});
|
|
367
|
+
}
|
|
335
368
|
|
|
336
369
|
return {
|
|
337
370
|
// Reactive state
|
|
@@ -358,10 +391,13 @@ export function useBlocksEngine(
|
|
|
358
391
|
toggleBlock,
|
|
359
392
|
duplicateBlock,
|
|
360
393
|
pasteBlock,
|
|
394
|
+
replacePage,
|
|
395
|
+
replaceRegion,
|
|
361
396
|
|
|
362
397
|
// History methods
|
|
363
398
|
undo,
|
|
364
399
|
redo,
|
|
400
|
+
batch,
|
|
365
401
|
canUndo,
|
|
366
402
|
canRedo,
|
|
367
403
|
|