@craftile/core 0.1.6 → 0.2.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/insert-block-from-preset.ts +218 -0
- package/src/engine.ts +59 -0
- package/src/vue/useBlocksEngine.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craftile/core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.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": "0.1
|
|
30
|
-
"@craftile/types": "0.1
|
|
29
|
+
"@craftile/event-bus": "0.2.1",
|
|
30
|
+
"@craftile/types": "0.2.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"vitest": "^3.2.4",
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { Block, BlockSchema, Page, PresetChild } from '@craftile/types';
|
|
2
|
+
import type { Command, EngineEmitFn } from '../types';
|
|
3
|
+
import { generateId } from '../utils';
|
|
4
|
+
import type { BlocksManager } from '../blocks-manager';
|
|
5
|
+
|
|
6
|
+
export interface InsertBlockFromPresetOptions {
|
|
7
|
+
blockType: string;
|
|
8
|
+
presetIndex: number;
|
|
9
|
+
parentId?: string;
|
|
10
|
+
regionName?: string;
|
|
11
|
+
index?: number;
|
|
12
|
+
blocksManager: BlocksManager;
|
|
13
|
+
emit: EngineEmitFn;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class InsertBlockFromPresetCommand implements Command {
|
|
17
|
+
private page: Page;
|
|
18
|
+
private blockType: string;
|
|
19
|
+
private presetIndex: number;
|
|
20
|
+
private parentId?: string;
|
|
21
|
+
private regionName?: string;
|
|
22
|
+
private index?: number;
|
|
23
|
+
private blockId: string;
|
|
24
|
+
private properties: Record<string, any>;
|
|
25
|
+
private insertedBlock?: Block;
|
|
26
|
+
private actualIndex?: number;
|
|
27
|
+
private blocksManager: BlocksManager;
|
|
28
|
+
private emit: EngineEmitFn;
|
|
29
|
+
private createdBlockIds: string[] = [];
|
|
30
|
+
|
|
31
|
+
constructor(page: Page, options: InsertBlockFromPresetOptions) {
|
|
32
|
+
this.page = page;
|
|
33
|
+
this.blockType = options.blockType;
|
|
34
|
+
this.presetIndex = options.presetIndex;
|
|
35
|
+
this.parentId = options.parentId;
|
|
36
|
+
this.regionName = options.regionName;
|
|
37
|
+
this.index = options.index;
|
|
38
|
+
this.blockId = generateId();
|
|
39
|
+
this.blocksManager = options.blocksManager;
|
|
40
|
+
this.emit = options.emit;
|
|
41
|
+
|
|
42
|
+
const blockSchema = this.blocksManager.get(this.blockType);
|
|
43
|
+
if (!blockSchema) {
|
|
44
|
+
throw new Error(`Block type '${this.blockType}' not found`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const preset = blockSchema.presets?.[this.presetIndex];
|
|
48
|
+
if (!preset) {
|
|
49
|
+
throw new Error(`Preset at index ${this.presetIndex} not found for block type '${this.blockType}'`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.properties = this.buildProperties(blockSchema, preset.properties);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
apply(): void {
|
|
56
|
+
const blockSchema = this.blocksManager.get(this.blockType);
|
|
57
|
+
const preset = blockSchema?.presets?.[this.presetIndex];
|
|
58
|
+
|
|
59
|
+
if (!preset) {
|
|
60
|
+
throw new Error(`Preset at index ${this.presetIndex} not found`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.insertedBlock = {
|
|
64
|
+
type: this.blockType,
|
|
65
|
+
id: this.blockId,
|
|
66
|
+
properties: this.properties,
|
|
67
|
+
children: [],
|
|
68
|
+
parentId: undefined,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
this.page.blocks[this.blockId] = this.insertedBlock;
|
|
72
|
+
this.createdBlockIds.push(this.blockId);
|
|
73
|
+
|
|
74
|
+
if (preset.children && preset.children.length > 0) {
|
|
75
|
+
this.insertedBlock.children = this.createChildrenFromPreset(preset.children, this.blockId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Insert the root block in the appropriate location
|
|
79
|
+
if (this.parentId) {
|
|
80
|
+
const parent = this.page.blocks[this.parentId];
|
|
81
|
+
|
|
82
|
+
if (!parent) {
|
|
83
|
+
throw new Error(`Parent block not found: ${this.parentId}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.insertedBlock.parentId = parent.id;
|
|
87
|
+
|
|
88
|
+
if (this.index !== undefined && this.index >= 0 && this.index <= parent.children.length) {
|
|
89
|
+
parent.children.splice(this.index, 0, this.blockId);
|
|
90
|
+
this.actualIndex = this.index;
|
|
91
|
+
} else {
|
|
92
|
+
parent.children.push(this.blockId);
|
|
93
|
+
this.actualIndex = parent.children.length - 1;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Insert as top-level block
|
|
97
|
+
const targetRegionName = this.regionName || this.page.regions[0].name;
|
|
98
|
+
let targetRegion = this.page.regions.find((r) => r.name === targetRegionName);
|
|
99
|
+
|
|
100
|
+
if (!targetRegion) {
|
|
101
|
+
targetRegion = { name: targetRegionName, blocks: [] };
|
|
102
|
+
this.page.regions.push(targetRegion);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.index !== undefined && this.index >= 0 && this.index <= targetRegion.blocks.length) {
|
|
106
|
+
targetRegion.blocks.splice(this.index, 0, this.blockId);
|
|
107
|
+
this.actualIndex = this.index;
|
|
108
|
+
} else {
|
|
109
|
+
targetRegion.blocks.push(this.blockId);
|
|
110
|
+
this.actualIndex = targetRegion.blocks.length - 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.emit('block:insert', {
|
|
115
|
+
blockId: this.blockId,
|
|
116
|
+
block: this.insertedBlock,
|
|
117
|
+
parentId: this.parentId,
|
|
118
|
+
index: this.actualIndex,
|
|
119
|
+
regionName: this.regionName || this.page.regions[0].name,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
revert(): void {
|
|
124
|
+
if (!this.insertedBlock) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Remove all created blocks
|
|
129
|
+
for (const blockId of this.createdBlockIds) {
|
|
130
|
+
delete this.page.blocks[blockId];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.parentId) {
|
|
134
|
+
const parent = this.page.blocks[this.parentId];
|
|
135
|
+
if (parent && this.actualIndex !== undefined) {
|
|
136
|
+
parent.children.splice(this.actualIndex, 1);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
const targetRegionName = this.regionName || this.page.regions[0].name;
|
|
140
|
+
const targetRegion = this.page.regions.find((r) => r.name === targetRegionName);
|
|
141
|
+
|
|
142
|
+
if (targetRegion && this.actualIndex !== undefined) {
|
|
143
|
+
targetRegion.blocks.splice(this.actualIndex, 1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.emit('block:remove', {
|
|
148
|
+
blockId: this.blockId,
|
|
149
|
+
block: this.insertedBlock,
|
|
150
|
+
parentId: this.parentId,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getBlockId(): string {
|
|
155
|
+
return this.blockId;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getInsertedBlock(): Block | undefined {
|
|
159
|
+
return this.insertedBlock;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private createChildrenFromPreset(presetChildren: PresetChild[], parentId: string): string[] {
|
|
163
|
+
const childIds: string[] = [];
|
|
164
|
+
|
|
165
|
+
for (const presetChild of presetChildren) {
|
|
166
|
+
const childId = generateId();
|
|
167
|
+
const childSchema = this.blocksManager.get(presetChild.type);
|
|
168
|
+
|
|
169
|
+
if (!childSchema) {
|
|
170
|
+
console.warn(`Block type '${presetChild.type}' not found, skipping child`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const childProperties = this.buildProperties(childSchema, presetChild.properties);
|
|
175
|
+
|
|
176
|
+
const childBlock: Block = {
|
|
177
|
+
type: presetChild.type,
|
|
178
|
+
id: childId,
|
|
179
|
+
semanticId: presetChild.id, // Preserve semantic ID from preset
|
|
180
|
+
properties: childProperties,
|
|
181
|
+
children: [],
|
|
182
|
+
parentId,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
this.page.blocks[childId] = childBlock;
|
|
186
|
+
this.createdBlockIds.push(childId);
|
|
187
|
+
|
|
188
|
+
// Recursively create nested children
|
|
189
|
+
if (presetChild.children && presetChild.children.length > 0) {
|
|
190
|
+
childBlock.children = this.createChildrenFromPreset(presetChild.children, childId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
childIds.push(childId);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return childIds;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private buildProperties(blockSchema?: BlockSchema, presetProperties?: Record<string, any>): Record<string, any> {
|
|
200
|
+
const properties: Record<string, any> = {};
|
|
201
|
+
|
|
202
|
+
// Start with schema defaults
|
|
203
|
+
if (blockSchema?.properties) {
|
|
204
|
+
for (const field of blockSchema.properties) {
|
|
205
|
+
if (field.default !== undefined) {
|
|
206
|
+
properties[field.id] = field.default;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Merge preset properties (overrides defaults)
|
|
212
|
+
if (presetProperties) {
|
|
213
|
+
Object.assign(properties, presetProperties);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return properties;
|
|
217
|
+
}
|
|
218
|
+
}
|
package/src/engine.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Block, BlockSchema, Page } from '@craftile/types';
|
|
|
4
4
|
import { BlocksManager } from './blocks-manager';
|
|
5
5
|
import { HistoryManager } from './history-manager';
|
|
6
6
|
import { InsertBlockCommand } from './commands/insert-block';
|
|
7
|
+
import { InsertBlockFromPresetCommand } from './commands/insert-block-from-preset';
|
|
7
8
|
import { RemoveBlockCommand } from './commands/remove-block';
|
|
8
9
|
import { MoveBlockCommand } from './commands/move-block';
|
|
9
10
|
import { ToggleBlockCommand } from './commands/toggle-block';
|
|
@@ -80,6 +81,64 @@ export class Engine extends EventBus<EngineEvents> {
|
|
|
80
81
|
return command.getBlockId();
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Insert a new block from a preset into the page
|
|
86
|
+
*
|
|
87
|
+
* @param blockType - The type of block to insert (must be registered)
|
|
88
|
+
* @param presetIndex - Index of the preset in the block schema's presets array
|
|
89
|
+
* @param options - Optional configuration for block insertion
|
|
90
|
+
* @param options.parentId - ID of parent block (for nested blocks)
|
|
91
|
+
* @param options.regionName - Target region name (defaults to 'main')
|
|
92
|
+
* @param options.index - Position to insert at (defaults to end)
|
|
93
|
+
* @returns The ID of the newly inserted block
|
|
94
|
+
* @throws {Error} When block type is not registered, preset not found, or parent-child relationship is invalid
|
|
95
|
+
* @emits block:insert - When the block is successfully inserted
|
|
96
|
+
*/
|
|
97
|
+
insertBlockFromPreset(
|
|
98
|
+
blockType: string,
|
|
99
|
+
presetIndex: number,
|
|
100
|
+
options?: {
|
|
101
|
+
parentId?: string;
|
|
102
|
+
regionName?: string;
|
|
103
|
+
index?: number;
|
|
104
|
+
}
|
|
105
|
+
): string {
|
|
106
|
+
const blockSchema = this.blocksManager.get(blockType);
|
|
107
|
+
if (!blockSchema) {
|
|
108
|
+
throw new Error(`Block type '${blockType}' is not registered`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!blockSchema.presets || !blockSchema.presets[presetIndex]) {
|
|
112
|
+
throw new Error(`Preset at index ${presetIndex} not found for block type '${blockType}'`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (options?.parentId) {
|
|
116
|
+
const parentBlock = this.page.blocks[options.parentId];
|
|
117
|
+
if (!parentBlock) {
|
|
118
|
+
throw new Error(`Parent block not found: ${options.parentId}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!this.blocksManager.canBeChild(blockType, parentBlock.type)) {
|
|
122
|
+
throw new Error(`Block type '${blockType}' cannot be a child of '${parentBlock.type}'`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const command = new InsertBlockFromPresetCommand(this.page, {
|
|
127
|
+
blockType,
|
|
128
|
+
presetIndex,
|
|
129
|
+
parentId: options?.parentId,
|
|
130
|
+
regionName: options?.regionName,
|
|
131
|
+
index: options?.index,
|
|
132
|
+
blocksManager: this.blocksManager,
|
|
133
|
+
emit: this.emit.bind(this),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
command.apply();
|
|
137
|
+
this.historyManager.addCommand(command);
|
|
138
|
+
|
|
139
|
+
return command.getBlockId();
|
|
140
|
+
}
|
|
141
|
+
|
|
83
142
|
/**
|
|
84
143
|
* Remove a block from the page
|
|
85
144
|
*
|
|
@@ -32,6 +32,15 @@ export interface UseBlocksEngineReturn {
|
|
|
32
32
|
index?: number;
|
|
33
33
|
}
|
|
34
34
|
) => string;
|
|
35
|
+
insertBlockFromPreset: (
|
|
36
|
+
blockType: string,
|
|
37
|
+
presetIndex: number,
|
|
38
|
+
options?: {
|
|
39
|
+
parentId?: string;
|
|
40
|
+
regionName?: string;
|
|
41
|
+
index?: number;
|
|
42
|
+
}
|
|
43
|
+
) => string;
|
|
35
44
|
removeBlock: (blockId: string) => void;
|
|
36
45
|
moveBlock: (
|
|
37
46
|
blockId: string,
|
|
@@ -189,6 +198,24 @@ export function useBlocksEngine(
|
|
|
189
198
|
return blockId;
|
|
190
199
|
};
|
|
191
200
|
|
|
201
|
+
const insertBlockFromPreset = (
|
|
202
|
+
blockType: string,
|
|
203
|
+
presetIndex: number,
|
|
204
|
+
options?: {
|
|
205
|
+
parentId?: string;
|
|
206
|
+
regionName?: string;
|
|
207
|
+
index?: number;
|
|
208
|
+
}
|
|
209
|
+
): string => {
|
|
210
|
+
const blockId = engine.insertBlockFromPreset(blockType, presetIndex, options);
|
|
211
|
+
|
|
212
|
+
if (!autoSync) {
|
|
213
|
+
syncStateFromEngine();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return blockId;
|
|
217
|
+
};
|
|
218
|
+
|
|
192
219
|
const removeBlock = (blockId: string): void => {
|
|
193
220
|
engine.removeBlock(blockId);
|
|
194
221
|
|
|
@@ -288,6 +315,7 @@ export function useBlocksEngine(
|
|
|
288
315
|
|
|
289
316
|
// Command methods
|
|
290
317
|
insertBlock,
|
|
318
|
+
insertBlockFromPreset,
|
|
291
319
|
removeBlock,
|
|
292
320
|
moveBlock,
|
|
293
321
|
setBlockProperty,
|