@craftile/core 0.1.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/LICENSE.md +21 -0
- package/package.json +49 -0
- package/src/blocks-manager.ts +115 -0
- package/src/commands/duplicate-block.ts +203 -0
- package/src/commands/insert-block.ts +143 -0
- package/src/commands/move-block.ts +182 -0
- package/src/commands/remove-block.ts +94 -0
- package/src/commands/set-block-property.ts +84 -0
- package/src/commands/toggle-block.ts +76 -0
- package/src/engine.ts +329 -0
- package/src/history-manager.ts +78 -0
- package/src/index.ts +3 -0
- package/src/types.ts +46 -0
- package/src/utils.ts +7 -0
- package/src/vue/index.ts +6 -0
- package/src/vue/useBlocksEngine.ts +307 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Craftile Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@craftile/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic block editor engine for Craftile",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"craftile",
|
|
7
|
+
"block-editor",
|
|
8
|
+
"cms",
|
|
9
|
+
"editor",
|
|
10
|
+
"blocks"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"authors": [
|
|
14
|
+
{
|
|
15
|
+
"name": "Eldo Magan",
|
|
16
|
+
"email": "magan.eldo@gmail.com"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"types": "src/index.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./src/index.ts",
|
|
23
|
+
"./vue": "./src/vue/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@craftile/event-bus": "0.1.0",
|
|
30
|
+
"@craftile/types": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"vitest": "^3.2.4",
|
|
34
|
+
"vue": "^3.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"vue": "^3.0.0"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/craftile/editor.git"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "vitest"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { BlockSchema } from '@craftile/types';
|
|
2
|
+
|
|
3
|
+
export class BlocksManager {
|
|
4
|
+
protected schemas: Map<string, BlockSchema> = new Map();
|
|
5
|
+
private patternCache = new Map<string, RegExp>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register a new block schema
|
|
9
|
+
*/
|
|
10
|
+
register(type: string, schema: BlockSchema): void {
|
|
11
|
+
if (this.schemas.has(type)) {
|
|
12
|
+
throw new Error(`Block type '${type}' is already registered`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.schemas.set(type, schema);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register multiple block schemas at once
|
|
20
|
+
*/
|
|
21
|
+
registerMany(schemas: Record<string, BlockSchema>): void {
|
|
22
|
+
for (const [type, schema] of Object.entries(schemas)) {
|
|
23
|
+
this.register(type, schema);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Unregister a block schema
|
|
29
|
+
*/
|
|
30
|
+
unregister(type: string): boolean {
|
|
31
|
+
return this.schemas.delete(type);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a block schema by type
|
|
36
|
+
*/
|
|
37
|
+
get(type: string): BlockSchema | undefined {
|
|
38
|
+
return this.schemas.get(type);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if a block type is registered
|
|
43
|
+
*/
|
|
44
|
+
has(type: string): boolean {
|
|
45
|
+
return this.schemas.has(type);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get all registered schemas as a record
|
|
50
|
+
*/
|
|
51
|
+
getAll(): Record<string, BlockSchema> {
|
|
52
|
+
const result: Record<string, BlockSchema> = {};
|
|
53
|
+
|
|
54
|
+
for (const [type, schema] of this.schemas.entries()) {
|
|
55
|
+
result[type] = schema;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find block types that match a predicate
|
|
63
|
+
*/
|
|
64
|
+
find(predicate: (type: string, schema: BlockSchema) => boolean): string[] {
|
|
65
|
+
const result: string[] = [];
|
|
66
|
+
|
|
67
|
+
for (const [type, schema] of this.schemas.entries()) {
|
|
68
|
+
if (predicate(type, schema)) {
|
|
69
|
+
result.push(type);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate if a block type can be a child of another type
|
|
78
|
+
*/
|
|
79
|
+
canBeChild(childType: string, parentType: string): boolean {
|
|
80
|
+
const parentSchema = this.get(parentType);
|
|
81
|
+
|
|
82
|
+
if (!parentSchema || !parentSchema.accepts) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return parentSchema.accepts.some((pattern) => this.matchesPattern(childType, pattern));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get all registered block types
|
|
91
|
+
*/
|
|
92
|
+
getTypes(): string[] {
|
|
93
|
+
return Array.from(this.schemas.keys());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a child type matches a pattern from the accepts list
|
|
98
|
+
* Supports glob-style wildcards: '*', '@theme/*', '*-button', '*type*', etc.
|
|
99
|
+
*/
|
|
100
|
+
private matchesPattern(childType: string, pattern: string): boolean {
|
|
101
|
+
if (pattern === '*') {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!this.patternCache.has(pattern)) {
|
|
106
|
+
const withPlaceholders = pattern.replace(/\*/g, '__WILDCARD__');
|
|
107
|
+
const escapedPattern = withPlaceholders.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
108
|
+
const regexPattern = '^' + escapedPattern.replace(/__WILDCARD__/g, '.*') + '$';
|
|
109
|
+
|
|
110
|
+
this.patternCache.set(pattern, new RegExp(regexPattern));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return this.patternCache.get(pattern)!.test(childType);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Command, EngineEmitFn } from '../types';
|
|
2
|
+
import type { Block, Page } from '@craftile/types';
|
|
3
|
+
import { generateId } from '../utils';
|
|
4
|
+
|
|
5
|
+
export interface DuplicateBlockOptions {
|
|
6
|
+
blockId: string;
|
|
7
|
+
emit: EngineEmitFn;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class DuplicateBlockCommand implements Command {
|
|
11
|
+
private page: Page;
|
|
12
|
+
private blockId: string;
|
|
13
|
+
|
|
14
|
+
// State for reverting
|
|
15
|
+
private duplicatedBlockId!: string;
|
|
16
|
+
private duplicatedBlock?: Block;
|
|
17
|
+
private parentId?: string | null;
|
|
18
|
+
private insertIndex!: number;
|
|
19
|
+
private regionName?: string | null;
|
|
20
|
+
private emit: EngineEmitFn;
|
|
21
|
+
|
|
22
|
+
constructor(page: Page, options: DuplicateBlockOptions) {
|
|
23
|
+
this.page = page;
|
|
24
|
+
this.blockId = options.blockId;
|
|
25
|
+
this.emit = options.emit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
apply(): void {
|
|
29
|
+
const originalBlock = this.page.blocks[this.blockId];
|
|
30
|
+
if (!originalBlock) {
|
|
31
|
+
throw new Error(`Block not found: ${this.blockId}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.duplicatedBlockId = generateId();
|
|
35
|
+
|
|
36
|
+
this.duplicatedBlock = this.cloneBlockWithNewIds(originalBlock, this.duplicatedBlockId, originalBlock.parentId);
|
|
37
|
+
|
|
38
|
+
this.determineInsertLocation();
|
|
39
|
+
|
|
40
|
+
this.insertDuplicatedBlock();
|
|
41
|
+
|
|
42
|
+
this.emit('block:duplicate', {
|
|
43
|
+
originalBlockId: this.blockId,
|
|
44
|
+
newBlockId: this.duplicatedBlockId,
|
|
45
|
+
newBlock: this.duplicatedBlock,
|
|
46
|
+
parentId: this.parentId || undefined,
|
|
47
|
+
index: this.insertIndex,
|
|
48
|
+
regionName: this.regionName || undefined,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
revert(): void {
|
|
53
|
+
if (!this.duplicatedBlock || !this.duplicatedBlockId) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.removeDuplicatedBlock();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getBlockId(): string {
|
|
61
|
+
return this.blockId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getDuplicatedBlockId(): string {
|
|
65
|
+
return this.duplicatedBlockId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getDuplicatedBlock(): Block | undefined {
|
|
69
|
+
return this.duplicatedBlock;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getParentId(): string | null | undefined {
|
|
73
|
+
return this.parentId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getInsertIndex(): number {
|
|
77
|
+
return this.insertIndex;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getRegionName(): string | null | undefined {
|
|
81
|
+
return this.regionName;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private cloneBlockWithNewIds(block: Block, newId: string, newParentId?: string): Block {
|
|
85
|
+
const clonedBlock: Block = {
|
|
86
|
+
...structuredClone(block),
|
|
87
|
+
id: newId,
|
|
88
|
+
parentId: newParentId,
|
|
89
|
+
children: [],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (block.children && block.children.length > 0) {
|
|
93
|
+
for (const childId of block.children) {
|
|
94
|
+
const childBlock = this.page.blocks[childId];
|
|
95
|
+
if (childBlock) {
|
|
96
|
+
const newChildId = generateId();
|
|
97
|
+
this.cloneBlockWithNewIds(childBlock, newChildId, newId);
|
|
98
|
+
clonedBlock.children.push(newChildId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.page.blocks[newId] = clonedBlock;
|
|
104
|
+
|
|
105
|
+
return clonedBlock;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private determineInsertLocation(): void {
|
|
109
|
+
const originalBlock = this.page.blocks[this.blockId];
|
|
110
|
+
if (!originalBlock) {
|
|
111
|
+
throw new Error(`Block not found: ${this.blockId}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.parentId = originalBlock.parentId || null;
|
|
115
|
+
|
|
116
|
+
if (this.parentId) {
|
|
117
|
+
const parent = this.page.blocks[this.parentId];
|
|
118
|
+
if (parent) {
|
|
119
|
+
const index = parent.children.indexOf(this.blockId);
|
|
120
|
+
this.insertIndex = index !== -1 ? index + 1 : parent.children.length;
|
|
121
|
+
} else {
|
|
122
|
+
this.insertIndex = 0;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
const region = this.page.regions.find((r) => r.blocks.includes(this.blockId));
|
|
126
|
+
if (region) {
|
|
127
|
+
const index = region.blocks.indexOf(this.blockId);
|
|
128
|
+
this.regionName = region.name;
|
|
129
|
+
this.insertIndex = index + 1;
|
|
130
|
+
} else {
|
|
131
|
+
this.regionName = this.page.regions[0].name || 'main';
|
|
132
|
+
this.insertIndex = this.page.regions[0].blocks.length || 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private insertDuplicatedBlock(): void {
|
|
138
|
+
if (!this.duplicatedBlock) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (this.parentId) {
|
|
143
|
+
const targetParent = this.page.blocks[this.parentId];
|
|
144
|
+
if (!targetParent) {
|
|
145
|
+
throw new Error(`Parent block not found: ${this.parentId}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
targetParent.children.splice(this.insertIndex, 0, this.duplicatedBlockId);
|
|
149
|
+
} else {
|
|
150
|
+
let targetRegion = this.page.regions.find((r) => r.name === this.regionName);
|
|
151
|
+
|
|
152
|
+
if (!targetRegion) {
|
|
153
|
+
targetRegion = { name: this.regionName || 'main', blocks: [] };
|
|
154
|
+
this.page.regions.push(targetRegion);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
targetRegion.blocks.splice(this.insertIndex, 0, this.duplicatedBlockId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private removeDuplicatedBlock(): void {
|
|
162
|
+
if (!this.duplicatedBlockId) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.removeBlockAndChildren(this.duplicatedBlockId);
|
|
167
|
+
|
|
168
|
+
if (this.parentId) {
|
|
169
|
+
// Remove from parent's children array
|
|
170
|
+
const targetParent = this.page.blocks[this.parentId];
|
|
171
|
+
if (targetParent) {
|
|
172
|
+
const index = targetParent.children.indexOf(this.duplicatedBlockId);
|
|
173
|
+
if (index !== -1) {
|
|
174
|
+
targetParent.children.splice(index, 1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// Remove from region
|
|
179
|
+
if (this.page.regions) {
|
|
180
|
+
const targetRegion = this.page.regions.find((r) => r.name === this.regionName);
|
|
181
|
+
if (targetRegion) {
|
|
182
|
+
const index = targetRegion.blocks.indexOf(this.duplicatedBlockId);
|
|
183
|
+
if (index !== -1) {
|
|
184
|
+
targetRegion.blocks.splice(index, 1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private removeBlockAndChildren(blockId: string): void {
|
|
192
|
+
const block = this.page.blocks[blockId];
|
|
193
|
+
if (!block) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const childId of block.children) {
|
|
198
|
+
this.removeBlockAndChildren(childId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
delete this.page.blocks[blockId];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Block, BlockSchema, Page } from '@craftile/types';
|
|
2
|
+
import type { Command, EngineEmitFn } from '../types';
|
|
3
|
+
import { generateId } from '../utils';
|
|
4
|
+
|
|
5
|
+
export interface InsertBlockOptions {
|
|
6
|
+
blockType: string;
|
|
7
|
+
parentId?: string;
|
|
8
|
+
regionName?: string;
|
|
9
|
+
index?: number;
|
|
10
|
+
properties?: Record<string, any>;
|
|
11
|
+
blockSchema?: BlockSchema;
|
|
12
|
+
emit: EngineEmitFn;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class InsertBlockCommand implements Command {
|
|
16
|
+
private page: Page;
|
|
17
|
+
private blockType: string;
|
|
18
|
+
private parentId?: string;
|
|
19
|
+
private regionName?: string;
|
|
20
|
+
private index?: number;
|
|
21
|
+
private blockId: string;
|
|
22
|
+
private properties: Record<string, any>;
|
|
23
|
+
private insertedBlock?: Block;
|
|
24
|
+
private actualIndex?: number;
|
|
25
|
+
private emit: EngineEmitFn;
|
|
26
|
+
|
|
27
|
+
constructor(page: Page, options: InsertBlockOptions) {
|
|
28
|
+
this.page = page;
|
|
29
|
+
this.blockType = options.blockType;
|
|
30
|
+
this.parentId = options.parentId;
|
|
31
|
+
this.regionName = options.regionName;
|
|
32
|
+
this.index = options.index;
|
|
33
|
+
this.blockId = generateId();
|
|
34
|
+
this.emit = options.emit;
|
|
35
|
+
|
|
36
|
+
this.properties = this.buildProperties(options.blockSchema);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
apply(): void {
|
|
40
|
+
this.insertedBlock = {
|
|
41
|
+
type: this.blockType,
|
|
42
|
+
id: this.blockId,
|
|
43
|
+
properties: this.properties,
|
|
44
|
+
children: [],
|
|
45
|
+
parentId: undefined, // Will be set if inserting as child
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.page.blocks[this.blockId] = this.insertedBlock;
|
|
49
|
+
|
|
50
|
+
if (this.parentId) {
|
|
51
|
+
const parent = this.page.blocks[this.parentId];
|
|
52
|
+
|
|
53
|
+
if (!parent) {
|
|
54
|
+
throw new Error(`Parent block not found: ${this.parentId}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.insertedBlock.parentId = parent.id;
|
|
58
|
+
|
|
59
|
+
if (this.index !== undefined && this.index >= 0 && this.index <= parent.children.length) {
|
|
60
|
+
parent.children.splice(this.index, 0, this.blockId);
|
|
61
|
+
this.actualIndex = this.index;
|
|
62
|
+
} else {
|
|
63
|
+
parent.children.push(this.blockId);
|
|
64
|
+
this.actualIndex = parent.children.length - 1;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Insert as top-level block (to the specified region)
|
|
68
|
+
const targetRegionName = this.regionName || this.page.regions[0].name;
|
|
69
|
+
let targetRegion = this.page.regions.find((r) => r.name === targetRegionName);
|
|
70
|
+
|
|
71
|
+
if (!targetRegion) {
|
|
72
|
+
targetRegion = { name: targetRegionName, blocks: [] };
|
|
73
|
+
this.page.regions.push(targetRegion);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.index !== undefined && this.index >= 0 && this.index <= targetRegion.blocks.length) {
|
|
77
|
+
targetRegion.blocks.splice(this.index, 0, this.blockId);
|
|
78
|
+
this.actualIndex = this.index;
|
|
79
|
+
} else {
|
|
80
|
+
targetRegion.blocks.push(this.blockId);
|
|
81
|
+
this.actualIndex = targetRegion.blocks.length - 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.emit('block:insert', {
|
|
86
|
+
blockId: this.blockId,
|
|
87
|
+
block: this.insertedBlock,
|
|
88
|
+
parentId: this.parentId,
|
|
89
|
+
index: this.actualIndex,
|
|
90
|
+
regionName: this.regionName || this.page.regions[0].name,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
revert(): void {
|
|
95
|
+
if (!this.insertedBlock) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
delete this.page.blocks[this.blockId];
|
|
100
|
+
|
|
101
|
+
if (this.parentId) {
|
|
102
|
+
const parent = this.page.blocks[this.parentId];
|
|
103
|
+
if (parent && this.actualIndex !== undefined) {
|
|
104
|
+
parent.children.splice(this.actualIndex, 1);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
const targetRegionName = this.regionName || this.page.regions[0].name;
|
|
108
|
+
const targetRegion = this.page.regions.find((r) => r.name === targetRegionName);
|
|
109
|
+
|
|
110
|
+
if (targetRegion && this.actualIndex !== undefined) {
|
|
111
|
+
targetRegion.blocks.splice(this.actualIndex, 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.emit('block:remove', {
|
|
116
|
+
blockId: this.blockId,
|
|
117
|
+
block: this.insertedBlock,
|
|
118
|
+
parentId: this.parentId,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getBlockId(): string {
|
|
123
|
+
return this.blockId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getInsertedBlock(): Block | undefined {
|
|
127
|
+
return this.insertedBlock;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private buildProperties(blockSchema?: BlockSchema): Record<string, any> {
|
|
131
|
+
const properties: Record<string, any> = {};
|
|
132
|
+
|
|
133
|
+
if (blockSchema?.properties) {
|
|
134
|
+
for (const field of blockSchema.properties) {
|
|
135
|
+
if (field.default !== undefined) {
|
|
136
|
+
properties[field.id] = field.default;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return properties;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Block, Page } from '@craftile/types';
|
|
2
|
+
import type { Command, EngineEmitFn } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface MoveBlockOptions {
|
|
5
|
+
blockId: string;
|
|
6
|
+
targetParentId?: string;
|
|
7
|
+
targetIndex?: number;
|
|
8
|
+
targetRegionName?: string;
|
|
9
|
+
emit: EngineEmitFn;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class MoveBlockCommand implements Command {
|
|
13
|
+
private page: Page;
|
|
14
|
+
private blockId: string;
|
|
15
|
+
private targetParentId?: string;
|
|
16
|
+
private targetIndex?: number;
|
|
17
|
+
private targetRegionName?: string;
|
|
18
|
+
|
|
19
|
+
// State for reverting
|
|
20
|
+
private blockToMove?: Block;
|
|
21
|
+
private originalParentId?: string;
|
|
22
|
+
private originalIndex!: number;
|
|
23
|
+
private originalRegionName?: string;
|
|
24
|
+
|
|
25
|
+
private emit: EngineEmitFn;
|
|
26
|
+
|
|
27
|
+
constructor(page: Page, options: MoveBlockOptions) {
|
|
28
|
+
this.page = page;
|
|
29
|
+
this.blockId = options.blockId;
|
|
30
|
+
this.targetParentId = options.targetParentId;
|
|
31
|
+
this.targetIndex = options.targetIndex;
|
|
32
|
+
this.targetRegionName = options.targetRegionName;
|
|
33
|
+
this.emit = options.emit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
apply(): void {
|
|
37
|
+
this.blockToMove = this.page.blocks[this.blockId];
|
|
38
|
+
|
|
39
|
+
if (!this.blockToMove) {
|
|
40
|
+
throw new Error(`Block not found: ${this.blockId}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.targetParentId && !this.page.blocks[this.targetParentId]) {
|
|
44
|
+
throw new Error(`Target parent not found: ${this.targetParentId}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.originalParentId = this.blockToMove.parentId;
|
|
48
|
+
|
|
49
|
+
// First we remove the block from the current position
|
|
50
|
+
if (this.originalParentId) {
|
|
51
|
+
const parent = this.page.blocks[this.originalParentId];
|
|
52
|
+
if (parent) {
|
|
53
|
+
this.originalIndex = parent.children.indexOf(this.blockId);
|
|
54
|
+
if (this.originalIndex !== -1) {
|
|
55
|
+
parent.children.splice(this.originalIndex, 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// it is a region level block
|
|
60
|
+
const region = this.page.regions.find((r) => r.blocks.includes(this.blockId));
|
|
61
|
+
if (region) {
|
|
62
|
+
this.originalIndex = region.blocks.indexOf(this.blockId);
|
|
63
|
+
this.originalRegionName = region.name;
|
|
64
|
+
region.blocks.splice(this.originalIndex, 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// the we insert the block at the new position
|
|
69
|
+
if (this.targetParentId) {
|
|
70
|
+
const targetParent = this.page.blocks[this.targetParentId];
|
|
71
|
+
this.blockToMove.parentId = this.targetParentId;
|
|
72
|
+
|
|
73
|
+
if (this.targetIndex !== undefined && this.targetIndex >= 0 && this.targetIndex <= targetParent.children.length) {
|
|
74
|
+
targetParent.children.splice(this.targetIndex, 0, this.blockId);
|
|
75
|
+
} else {
|
|
76
|
+
targetParent.children.push(this.blockId);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
this.blockToMove.parentId = undefined;
|
|
80
|
+
|
|
81
|
+
const regionName = this.targetRegionName || this.page.regions[0].name || 'main';
|
|
82
|
+
let targetRegion = this.page.regions.find((r) => r.name === regionName);
|
|
83
|
+
|
|
84
|
+
if (!targetRegion) {
|
|
85
|
+
targetRegion = { name: regionName, blocks: [] };
|
|
86
|
+
this.page.regions.push(targetRegion);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this.targetIndex !== undefined && this.targetIndex >= 0 && this.targetIndex <= targetRegion.blocks.length) {
|
|
90
|
+
targetRegion.blocks.splice(this.targetIndex, 0, this.blockId);
|
|
91
|
+
} else {
|
|
92
|
+
targetRegion.blocks.push(this.blockId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.emit('block:move', {
|
|
97
|
+
blockId: this.blockId,
|
|
98
|
+
targetParentId: this.targetParentId,
|
|
99
|
+
targetIndex: this.targetIndex,
|
|
100
|
+
targetRegionName: this.targetRegionName,
|
|
101
|
+
sourceParentId: this.originalParentId || null,
|
|
102
|
+
sourceRegionName: this.originalRegionName || null,
|
|
103
|
+
sourceIndex: this.originalIndex,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
revert(): void {
|
|
108
|
+
if (!this.blockToMove || this.originalIndex === undefined) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (this.blockToMove.parentId) {
|
|
113
|
+
const currentParent = this.page.blocks[this.blockToMove.parentId];
|
|
114
|
+
if (currentParent) {
|
|
115
|
+
const index = currentParent.children.indexOf(this.blockId);
|
|
116
|
+
if (index !== -1) {
|
|
117
|
+
currentParent.children.splice(index, 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const region = this.page.regions.find((r) => r.blocks.includes(this.blockId));
|
|
122
|
+
if (region) {
|
|
123
|
+
const index = region.blocks.indexOf(this.blockId);
|
|
124
|
+
if (index !== -1) {
|
|
125
|
+
region.blocks.splice(index, 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.blockToMove.parentId = this.originalParentId;
|
|
131
|
+
|
|
132
|
+
if (this.originalParentId) {
|
|
133
|
+
const originalParent = this.page.blocks[this.originalParentId];
|
|
134
|
+
if (originalParent) {
|
|
135
|
+
originalParent.children.splice(this.originalIndex, 0, this.blockId);
|
|
136
|
+
}
|
|
137
|
+
} else if (this.originalRegionName) {
|
|
138
|
+
const region = this.page.regions.find((r) => r.name === this.originalRegionName);
|
|
139
|
+
if (region) {
|
|
140
|
+
region.blocks.splice(this.originalIndex, 0, this.blockId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.emit('block:move', {
|
|
145
|
+
blockId: this.blockId,
|
|
146
|
+
targetParentId: this.originalParentId,
|
|
147
|
+
targetIndex: this.originalIndex,
|
|
148
|
+
targetRegionName: this.originalRegionName,
|
|
149
|
+
sourceParentId: this.targetParentId || null,
|
|
150
|
+
sourceRegionName: this.targetRegionName || null,
|
|
151
|
+
sourceIndex: this.targetIndex || 0,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getBlockId(): string {
|
|
156
|
+
return this.blockId;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getTargetParentId(): string | undefined {
|
|
160
|
+
return this.targetParentId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getTargetIndex(): number | undefined {
|
|
164
|
+
return this.targetIndex;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getTargetRegionName(): string | undefined {
|
|
168
|
+
return this.targetRegionName;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getSourceParentId(): string | null {
|
|
172
|
+
return this.originalParentId || null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getSourceRegionName(): string | null {
|
|
176
|
+
return this.originalRegionName || null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
getSourceIndex(): number {
|
|
180
|
+
return this.originalIndex;
|
|
181
|
+
}
|
|
182
|
+
}
|