@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 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
+ }