@craftile/core 0.6.3 → 0.7.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craftile/core",
3
- "version": "0.6.3",
3
+ "version": "0.7.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": "0.6.3",
30
- "@craftile/types": "0.6.3"
29
+ "@craftile/event-bus": "0.7.0",
30
+ "@craftile/types": "0.7.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "vitest": "^3.2.4",
@@ -1,11 +1,12 @@
1
- import type { Block, BlockSchema, Page, PresetChild } from '@craftile/types';
1
+ import type { Block, BlockProperties, BlockSchema, BlockStructure, Page } from '@craftile/types';
2
2
  import type { Command, EngineEmitFn } from '../types';
3
3
  import { generateId } from '../utils';
4
4
  import type { BlocksManager } from '../blocks-manager';
5
5
 
6
6
  export interface InsertBlockFromPresetOptions {
7
7
  blockType: string;
8
- presetIndex: number;
8
+ presetIndex?: number;
9
+ presetData?: BlockStructure;
9
10
  parentId?: string;
10
11
  regionName?: string;
11
12
  index?: number;
@@ -16,7 +17,8 @@ export interface InsertBlockFromPresetOptions {
16
17
  export class InsertBlockFromPresetCommand implements Command {
17
18
  private page: Page;
18
19
  private blockType: string;
19
- private presetIndex: number;
20
+ private presetIndex?: number;
21
+ private presetData?: BlockStructure;
20
22
  private parentId?: string;
21
23
  private regionName?: string;
22
24
  private index?: number;
@@ -32,6 +34,7 @@ export class InsertBlockFromPresetCommand implements Command {
32
34
  this.page = page;
33
35
  this.blockType = options.blockType;
34
36
  this.presetIndex = options.presetIndex;
37
+ this.presetData = options.presetData;
35
38
  this.parentId = options.parentId;
36
39
  this.regionName = options.regionName;
37
40
  this.index = options.index;
@@ -39,41 +42,81 @@ export class InsertBlockFromPresetCommand implements Command {
39
42
  this.blocksManager = options.blocksManager;
40
43
  this.emit = options.emit;
41
44
 
45
+ if (options.presetIndex === undefined && !options.presetData) {
46
+ throw new Error('Either presetIndex or presetData must be provided');
47
+ }
48
+
42
49
  const blockSchema = this.blocksManager.get(this.blockType);
43
50
  if (!blockSchema) {
44
51
  throw new Error(`Block type '${this.blockType}' not found`);
45
52
  }
46
53
 
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}'`);
54
+ let properties: BlockProperties | undefined;
55
+
56
+ if (options.presetData) {
57
+ properties = options.presetData.properties;
58
+ } else if (options.presetIndex !== undefined) {
59
+ const preset = blockSchema.presets?.[options.presetIndex];
60
+ if (!preset) {
61
+ throw new Error(`Preset at index ${options.presetIndex} not found for block type '${this.blockType}'`);
62
+ }
63
+
64
+ properties = preset.properties;
50
65
  }
51
66
 
52
- this.properties = this.buildProperties(blockSchema, preset.properties);
67
+ this.properties = this.buildProperties(blockSchema, properties);
53
68
  }
54
69
 
55
70
  apply(): void {
56
71
  const blockSchema = this.blocksManager.get(this.blockType);
57
- const preset = blockSchema?.presets?.[this.presetIndex];
58
72
 
59
- if (!preset) {
60
- throw new Error(`Preset at index ${this.presetIndex} not found`);
61
- }
73
+ if (this.presetData) {
74
+ // Handle BlockStructure (paste operation)
75
+ this.insertedBlock = {
76
+ type: this.blockType,
77
+ id: this.blockId,
78
+ name: this.presetData.name || blockSchema?.meta?.name || this.blockType,
79
+ semanticId: this.presetData.semanticId || this.presetData.id,
80
+ properties: this.properties,
81
+ children: [],
82
+ parentId: undefined,
83
+ static: this.presetData.static,
84
+ disabled: this.presetData.disabled,
85
+ repeated: this.presetData.repeated,
86
+ };
87
+
88
+ this.page.blocks[this.blockId] = this.insertedBlock;
89
+ this.createdBlockIds.push(this.blockId);
62
90
 
63
- this.insertedBlock = {
64
- type: this.blockType,
65
- id: this.blockId,
66
- name: preset.name,
67
- properties: this.properties,
68
- children: [],
69
- parentId: undefined,
70
- };
91
+ if (this.presetData.children && this.presetData.children.length > 0) {
92
+ this.insertedBlock.children = this.createChildrenFromPreset(this.presetData.children, this.blockId);
93
+ }
94
+ } else if (this.presetIndex !== undefined) {
95
+ // Handle BlockPreset from schema (preset insertion)
96
+ const preset = blockSchema?.presets?.[this.presetIndex];
97
+ if (!preset) {
98
+ throw new Error(`Preset at index ${this.presetIndex} not found`);
99
+ }
100
+
101
+ this.insertedBlock = {
102
+ type: this.blockType,
103
+ id: this.blockId,
104
+ name: preset.name,
105
+ properties: this.properties,
106
+ children: [],
107
+ parentId: undefined,
108
+ };
71
109
 
72
- this.page.blocks[this.blockId] = this.insertedBlock;
73
- this.createdBlockIds.push(this.blockId);
110
+ this.page.blocks[this.blockId] = this.insertedBlock;
111
+ this.createdBlockIds.push(this.blockId);
74
112
 
75
- if (preset.children && preset.children.length > 0) {
76
- this.insertedBlock.children = this.createChildrenFromPreset(preset.children, this.blockId);
113
+ if (preset.children && preset.children.length > 0) {
114
+ this.insertedBlock.children = this.createChildrenFromPreset(preset.children, this.blockId);
115
+ }
116
+ }
117
+
118
+ if (!this.insertedBlock) {
119
+ throw new Error('Failed to create block');
77
120
  }
78
121
 
79
122
  // Insert the root block in the appropriate location
@@ -160,37 +203,39 @@ export class InsertBlockFromPresetCommand implements Command {
160
203
  return this.insertedBlock;
161
204
  }
162
205
 
163
- private createChildrenFromPreset(presetChildren: PresetChild[], parentId: string): string[] {
206
+ private createChildrenFromPreset(structures: BlockStructure[], parentId: string): string[] {
164
207
  const childIds: string[] = [];
165
208
 
166
- for (const presetChild of presetChildren) {
209
+ for (const structure of structures) {
167
210
  const childId = generateId();
168
- const childSchema = this.blocksManager.get(presetChild.type);
211
+ const childSchema = this.blocksManager.get(structure.type);
169
212
 
170
213
  if (!childSchema) {
171
- console.warn(`Block type '${presetChild.type}' not found, skipping child`);
214
+ console.warn(`Block type '${structure.type}' not found, skipping child`);
172
215
  continue;
173
216
  }
174
217
 
175
- const childProperties = this.buildProperties(childSchema, presetChild.properties);
218
+ const childProperties = this.buildProperties(childSchema, structure.properties);
176
219
 
177
220
  const childBlock: Block = {
178
- type: presetChild.type,
221
+ type: structure.type,
179
222
  id: childId,
180
- name: presetChild.name || childSchema.meta?.name || presetChild.type, // Preserve custom name from preset
181
- semanticId: presetChild.id, // Preserve semantic ID from preset
223
+ name: structure.name || childSchema.meta?.name || structure.type,
224
+ semanticId: structure.semanticId || structure.id,
182
225
  properties: childProperties,
183
226
  children: [],
184
227
  parentId,
185
- static: presetChild.static, // Preserve static flag
228
+ static: structure.static,
229
+ disabled: structure.disabled,
230
+ repeated: structure.repeated,
186
231
  };
187
232
 
188
233
  this.page.blocks[childId] = childBlock;
189
234
  this.createdBlockIds.push(childId);
190
235
 
191
236
  // Recursively create nested children
192
- if (presetChild.children && presetChild.children.length > 0) {
193
- childBlock.children = this.createChildrenFromPreset(presetChild.children, childId);
237
+ if (structure.children && structure.children.length > 0) {
238
+ childBlock.children = this.createChildrenFromPreset(structure.children, childId);
194
239
  }
195
240
 
196
241
  childIds.push(childId);
package/src/engine.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EventBus } from '@craftile/event-bus';
2
2
  import type { EngineConfig, EngineEvents } from './types';
3
- import type { Block, BlockSchema, Page } from '@craftile/types';
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
6
  import { InsertBlockCommand } from './commands/insert-block';
@@ -272,6 +272,87 @@ export class Engine extends EventBus<EngineEvents> {
272
272
  return this.page.blocks[blockId];
273
273
  }
274
274
 
275
+ /**
276
+ * Export a block and its children as a nested structure
277
+ * Useful for copying blocks or creating presets from existing blocks
278
+ */
279
+ exportBlockAsNestedStructure(blockId: string): BlockStructure {
280
+ const block = this.page.blocks[blockId];
281
+ if (!block) {
282
+ throw new Error(`Block not found: ${blockId}`);
283
+ }
284
+
285
+ const structure: BlockStructure = {
286
+ type: block.type,
287
+ id: block.id,
288
+ semanticId: block.semanticId,
289
+ properties: structuredClone(block.properties),
290
+ name: block.name,
291
+ static: block.static,
292
+ disabled: block.disabled,
293
+ repeated: block.repeated,
294
+ children: [],
295
+ };
296
+
297
+ if (block.children && block.children.length > 0) {
298
+ structure.children = block.children.map((childId) => this.exportBlockAsNestedStructure(childId));
299
+ }
300
+
301
+ return structure;
302
+ }
303
+
304
+ /**
305
+ * Paste a block from a nested structure
306
+ *
307
+ * @param structure - The block structure to paste
308
+ * @param options - Optional configuration for block insertion
309
+ * @param options.parentId - ID of parent block (for nested blocks)
310
+ * @param options.regionName - Target region name (defaults to 'main')
311
+ * @param options.index - Position to insert at (defaults to end)
312
+ * @returns The ID of the newly inserted block
313
+ * @throws {Error} When block type is not registered or parent-child relationship is invalid
314
+ * @emits block:insert - When the block is successfully inserted
315
+ */
316
+ pasteBlock(
317
+ structure: BlockStructure,
318
+ options?: {
319
+ parentId?: string;
320
+ regionName?: string;
321
+ index?: number;
322
+ }
323
+ ): string {
324
+ const blockSchema = this.blocksManager.get(structure.type);
325
+ if (!blockSchema) {
326
+ throw new Error(`Block type '${structure.type}' is not registered`);
327
+ }
328
+
329
+ if (options?.parentId) {
330
+ const parentBlock = this.page.blocks[options.parentId];
331
+ if (!parentBlock) {
332
+ throw new Error(`Parent block not found: ${options.parentId}`);
333
+ }
334
+
335
+ if (!this.blocksManager.canBeChild(structure.type, parentBlock.type)) {
336
+ throw new Error(`Block type '${structure.type}' cannot be a child of '${parentBlock.type}'`);
337
+ }
338
+ }
339
+
340
+ const command = new InsertBlockFromPresetCommand(this.page, {
341
+ blockType: structure.type,
342
+ presetData: structure,
343
+ parentId: options?.parentId,
344
+ regionName: options?.regionName,
345
+ index: options?.index,
346
+ blocksManager: this.blocksManager,
347
+ emit: this.emit.bind(this),
348
+ });
349
+
350
+ command.apply();
351
+ this.historyManager.addCommand(command);
352
+
353
+ return command.getBlockId();
354
+ }
355
+
275
356
  /**
276
357
  * Get current page state
277
358
  */
@@ -1,5 +1,5 @@
1
1
  import { ref, onUnmounted, computed, toRaw, nextTick, type Ref } from 'vue';
2
- import type { Block, Page, Region } from '@craftile/types';
2
+ import type { Block, BlockStructure, Page, Region } from '@craftile/types';
3
3
  import type { EngineConfig } from '../types';
4
4
  import { Engine } from '../engine';
5
5
 
@@ -54,6 +54,14 @@ export interface UseBlocksEngineReturn {
54
54
  setBlockName: (blockId: string, name: string) => void;
55
55
  toggleBlock: (blockId: string, disabled?: boolean) => void;
56
56
  duplicateBlock: (blockId: string) => string;
57
+ pasteBlock: (
58
+ structure: BlockStructure,
59
+ options?: {
60
+ parentId?: string;
61
+ regionName?: string;
62
+ index?: number;
63
+ }
64
+ ) => string;
57
65
 
58
66
  undo: () => boolean;
59
67
  redo: () => boolean;
@@ -275,6 +283,23 @@ export function useBlocksEngine(
275
283
  return newBlockId;
276
284
  };
277
285
 
286
+ const pasteBlock = (
287
+ structure: BlockStructure,
288
+ options?: {
289
+ parentId?: string;
290
+ regionName?: string;
291
+ index?: number;
292
+ }
293
+ ): string => {
294
+ const blockId = engine.pasteBlock(structure, options);
295
+
296
+ if (!autoSync) {
297
+ syncStateFromEngine();
298
+ }
299
+
300
+ return blockId;
301
+ };
302
+
278
303
  const undo = (): boolean => {
279
304
  const result = engine.undo();
280
305
 
@@ -332,6 +357,7 @@ export function useBlocksEngine(
332
357
  setBlockName,
333
358
  toggleBlock,
334
359
  duplicateBlock,
360
+ pasteBlock,
335
361
 
336
362
  // History methods
337
363
  undo,