@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craftile/core",
3
- "version": "0.1.6",
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.6",
30
- "@craftile/types": "0.1.6"
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,