@pcg/dynamic-components 1.0.0-alpha.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/CHANGELOG.md +7 -0
- package/dist/index.d.ts +1816 -0
- package/dist/index.js +1564 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.cjs +14 -0
- package/package.json +30 -0
- package/src/assertions/basic.ts +58 -0
- package/src/assertions/containers.ts +76 -0
- package/src/assertions/index.ts +6 -0
- package/src/assertions/paths.ts +12 -0
- package/src/assertions/rich-text.ts +16 -0
- package/src/assertions/yjs.ts +25 -0
- package/src/data-objects/data-object.ts +34 -0
- package/src/data-objects/index.ts +3 -0
- package/src/data-objects/rich-text.ts +38 -0
- package/src/dynamic-components/fractional-indexing.ts +321 -0
- package/src/dynamic-components/index.ts +6 -0
- package/src/dynamic-components/paths.ts +194 -0
- package/src/dynamic-components/registry/chats.ts +24 -0
- package/src/dynamic-components/registry/content.ts +118 -0
- package/src/dynamic-components/registry/forms.ts +525 -0
- package/src/dynamic-components/registry/index.ts +6 -0
- package/src/dynamic-components/registry/layout.ts +86 -0
- package/src/dynamic-components/registry/uikit-dynamic-component.ts +84 -0
- package/src/dynamic-components/tools.ts +195 -0
- package/src/dynamic-components/types.ts +237 -0
- package/src/index.ts +7 -0
- package/src/paths/array-keys.ts +164 -0
- package/src/paths/array-ops.ts +124 -0
- package/src/paths/basic-ops.ts +181 -0
- package/src/paths/constants.ts +1 -0
- package/src/paths/index.ts +7 -0
- package/src/paths/tools.ts +42 -0
- package/src/paths/types.ts +133 -0
- package/src/y-components/index.ts +3 -0
- package/src/y-components/tools.ts +234 -0
- package/src/y-components/types.ts +19 -0
- package/src/y-tools/array-path-ops.ts +240 -0
- package/src/y-tools/basic-path-ops.ts +189 -0
- package/src/y-tools/index.ts +6 -0
- package/src/y-tools/tools.ts +122 -0
- package/src/y-tools/types.ts +32 -0
- package/src/y-tools/y-array-keys.ts +47 -0
- package/tests/assertions/basic-types.test.ts +78 -0
- package/tests/assertions/containers.test.ts +72 -0
- package/tests/assertions/paths.test.ts +23 -0
- package/tests/assertions/yjs.test.ts +33 -0
- package/tests/dynamic-components/paths.test.ts +171 -0
- package/tests/dynamic-components/tools.test.ts +121 -0
- package/tests/paths/array-keys.test.ts +182 -0
- package/tests/paths/array-ops.test.ts +164 -0
- package/tests/paths/basic-ops.test.ts +263 -0
- package/tests/paths/tools.test.ts +55 -0
- package/tests/y-components/tools.test.ts +198 -0
- package/tests/y-tools/array-base-ops.test.ts +55 -0
- package/tests/y-tools/array-path-ops.test.ts +95 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +13 -0
- package/tsdown.config.ts +18 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { JSONContent } from '@tiptap/core';
|
|
2
|
+
import { yXmlFragmentToProsemirrorJSON } from 'y-prosemirror';
|
|
3
|
+
import * as Y from 'yjs';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
isObject, isString, isYArray, isYMap,
|
|
7
|
+
} from '@/assertions/index.js';
|
|
8
|
+
import { RichText } from '@/data-objects/index.js';
|
|
9
|
+
import {
|
|
10
|
+
DynamicComponent, DynamicComponentWithPosition,
|
|
11
|
+
sortDynamicComponents,
|
|
12
|
+
} from '@/dynamic-components/index.js';
|
|
13
|
+
import {
|
|
14
|
+
arrayToYArray,
|
|
15
|
+
objectToYMap,
|
|
16
|
+
} from '@/y-tools/index.js';
|
|
17
|
+
|
|
18
|
+
import { YComponent } from './types.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This function converts dynamic component properties to Y.Map representation.
|
|
22
|
+
*/
|
|
23
|
+
export const dynamicComponentPropsToYComponentProps = (props: Record<string, unknown>) => {
|
|
24
|
+
const entries: [string, unknown][] = Object.entries(props).map(([key, value]) => {
|
|
25
|
+
if (key === 'content' && isObject(value) && (value.content as JSONContent)?.type === 'doc') {
|
|
26
|
+
// sae value content as is
|
|
27
|
+
return [key, value.content];
|
|
28
|
+
} else if (Array.isArray(value)) {
|
|
29
|
+
return [key, arrayToYArray(value)];
|
|
30
|
+
} else if (isObject(value)) {
|
|
31
|
+
return [key, objectToYMap(value)];
|
|
32
|
+
} else {
|
|
33
|
+
return [key, value];
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return new Y.Map(entries);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* This function converts a dynamic component to Y.Map representation.
|
|
42
|
+
*/
|
|
43
|
+
export const dynamicComponentToYMap = (component: DynamicComponent, yMap = new Y.Map()): YComponent => {
|
|
44
|
+
const {
|
|
45
|
+
props: propsWithComponents,
|
|
46
|
+
...fields
|
|
47
|
+
} = component;
|
|
48
|
+
|
|
49
|
+
const {
|
|
50
|
+
components,
|
|
51
|
+
...props
|
|
52
|
+
} = propsWithComponents ?? {
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
56
|
+
yMap.set(key, value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const yProps = props ? objectToYMap(props) : new Y.Map();
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(components)) {
|
|
62
|
+
yProps.set('components', dynamicComponentsToYArray(components));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
yMap.set('props', yProps);
|
|
66
|
+
|
|
67
|
+
return yMap;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* This function converts an array of dynamic components to Y.Array representation.
|
|
72
|
+
*/
|
|
73
|
+
export const dynamicComponentsToYArray = (components: DynamicComponent[], yArray = new Y.Array()) => {
|
|
74
|
+
yArray.push(sortDynamicComponents(components).map((cmp) => dynamicComponentToYMap(cmp)));
|
|
75
|
+
|
|
76
|
+
return yArray;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* This function checks if a given Y.Map object is a YComponent.
|
|
81
|
+
*/
|
|
82
|
+
export const isYComponent = (yComponent: unknown): yComponent is YComponent => {
|
|
83
|
+
return isYMap(yComponent) && isString(yComponent.get('component')) && isString(yComponent.get('id'));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* This function converts a YComponent to a dynamic component.
|
|
88
|
+
*/
|
|
89
|
+
export const yComponentToDynamicComponent = (yComponent: YComponent, extractContent = false): DynamicComponentWithPosition => {
|
|
90
|
+
const component: Record<string, unknown> = {
|
|
91
|
+
props: {
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (const [key, value] of yComponent.entries()) {
|
|
96
|
+
if (key === 'props') {
|
|
97
|
+
if (!isYMap(value)) {
|
|
98
|
+
throw new Error('Component props must be YMap');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const yComponentProps = value;
|
|
102
|
+
|
|
103
|
+
const props: Record<string, unknown> = {
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
for (const [prop, value] of yComponentProps.entries()) {
|
|
107
|
+
if (prop === 'components') {
|
|
108
|
+
if (!isYArray<YComponent>(value)) {
|
|
109
|
+
throw new Error('Component props.components array must be YArray');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
props.components = value.toArray().map((it) => yComponentToDynamicComponent(it));
|
|
113
|
+
} else if (
|
|
114
|
+
extractContent &&
|
|
115
|
+
yComponent.doc &&
|
|
116
|
+
isYMap(value) &&
|
|
117
|
+
value.get('type') === 'RichText' &&
|
|
118
|
+
typeof value.get('id') === 'string'
|
|
119
|
+
) {
|
|
120
|
+
const id = value.get('id') as string;
|
|
121
|
+
const rawRichText = value.toJSON() as RichText;
|
|
122
|
+
|
|
123
|
+
const richText: RichText = {
|
|
124
|
+
...rawRichText,
|
|
125
|
+
tiptap: yXmlFragmentToProsemirrorJSON(yComponent.doc.getXmlFragment(`RichText:${id}`)),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
props[prop] = richText;
|
|
129
|
+
} else if (value instanceof Y.AbstractType) {
|
|
130
|
+
props[prop] = value.toJSON();
|
|
131
|
+
} else {
|
|
132
|
+
props[prop] = value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
component.props = props;
|
|
137
|
+
} else if (isYArray(value)) {
|
|
138
|
+
component[key] = value.toArray();
|
|
139
|
+
} else if (isYMap(value)) {
|
|
140
|
+
component[key] = value.toJSON();
|
|
141
|
+
} else {
|
|
142
|
+
component[key] = value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return component as unknown as DynamicComponentWithPosition;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* This function converts a dynamic component to YComponent.
|
|
151
|
+
*/
|
|
152
|
+
export const dynamicComponentToYComponent = (component: DynamicComponent): YComponent => {
|
|
153
|
+
// return yComponent.toJSON() as DynamicComponentWithPosition;
|
|
154
|
+
return dynamicComponentToYMap(component) as YComponent;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* This function retrieves the properties of a YComponent.
|
|
159
|
+
*/
|
|
160
|
+
export const getYComponentProps = (yComponent: YComponent): Y.Map<unknown> => {
|
|
161
|
+
const props = yComponent.get('props');
|
|
162
|
+
if (!isYMap(props)) {
|
|
163
|
+
throw new Error(`Component hasn't "props" property. It must have to be defined as Y.Map (component: ${yComponent.get('id')})`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return props;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* This function retrieves the array of components of a YComponent.
|
|
171
|
+
*/
|
|
172
|
+
export const getYComponentComponentsArray = (yComponent: YComponent): Y.Array<YComponent> => {
|
|
173
|
+
const props = getYComponentProps(yComponent);
|
|
174
|
+
|
|
175
|
+
const yComponents = props.get('components');
|
|
176
|
+
|
|
177
|
+
if (!isYArray<YComponent>(yComponents)) {
|
|
178
|
+
throw new Error(`Component hasn't "props.components" property. It must be defined as Y.Array (component: ${yComponent.get('id')})`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return yComponents;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* This function checks if a YComponent has a components array.
|
|
186
|
+
* @example
|
|
187
|
+
* // component
|
|
188
|
+
* {
|
|
189
|
+
* id: "column",
|
|
190
|
+
* component: "column",
|
|
191
|
+
* props: {
|
|
192
|
+
* components: [...] <= has components array
|
|
193
|
+
* }
|
|
194
|
+
* }
|
|
195
|
+
*/
|
|
196
|
+
export const yComponentHasComponentsYArray = (yComponent: YComponent): boolean => {
|
|
197
|
+
const props = getYComponentProps(yComponent);
|
|
198
|
+
|
|
199
|
+
const yComponents = props.get('components');
|
|
200
|
+
|
|
201
|
+
return isYArray<YComponent>(yComponents);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* This function retrieves the parent component of a YArray of YComponents.
|
|
206
|
+
* @example
|
|
207
|
+
* // parent component
|
|
208
|
+
* {
|
|
209
|
+
* id: "column",
|
|
210
|
+
* component: "column",
|
|
211
|
+
* props: {
|
|
212
|
+
* components: [...] <= components array
|
|
213
|
+
* }
|
|
214
|
+
* }
|
|
215
|
+
*/
|
|
216
|
+
export const getYComponentsArrayParentComponent = (yArray: Y.Array<YComponent>): YComponent | null => {
|
|
217
|
+
// yarray => parent (props) => parent (YIndexedDynamicComponents)
|
|
218
|
+
|
|
219
|
+
return yArray.parent?.parent as YComponent ?? null;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* This function finds a Y.Component in a Y.Array by its ID.
|
|
224
|
+
*/
|
|
225
|
+
export const findYComponentInArray = (yArray: Y.Array<YComponent>, componentId: string) => {
|
|
226
|
+
const yComponent = yArray.toArray().find((it) => it.get('id') === componentId);
|
|
227
|
+
|
|
228
|
+
if (!yComponent) {
|
|
229
|
+
throw new Error(`Component with id ${componentId} not found in yArray`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return yComponent;
|
|
233
|
+
};
|
|
234
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a dynamic component in Y.js.
|
|
5
|
+
* Each component is represented as a Y.Map object where each key-value pair corresponds to a property of the component.
|
|
6
|
+
*/
|
|
7
|
+
export type YComponent = Y.Map<unknown>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents an array of dynamic components in Y.js.
|
|
11
|
+
* Each component in the array is represented as a YComponent.
|
|
12
|
+
*/
|
|
13
|
+
export type YComponentsArray = Y.Array<YComponent>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Represents the properties of a dynamic component in Y.js.
|
|
17
|
+
* Each property is represented as a key-value pair in a Y.Map object.
|
|
18
|
+
*/
|
|
19
|
+
export type YComponentProps = Y.Map<unknown>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isIndex, isYArray, isYMap,
|
|
5
|
+
} from '@/assertions/index.js';
|
|
6
|
+
import { generateKeyBetween } from '@/dynamic-components/index.js';
|
|
7
|
+
|
|
8
|
+
import { getFromYPath, setInYPath } from './basic-path-ops.js';
|
|
9
|
+
import { getSortedYArray, getYArrayRefsByPosition } from './tools.js';
|
|
10
|
+
import { getYArrayIndexByArrayKey } from './y-array-keys.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pushes a value to the end of the Y.Array at the specified path.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* let yMap = new Y.Map();
|
|
17
|
+
* yMap.set('names', new Y.Array());
|
|
18
|
+
* pushInYPath(yMap, ['names'], 'John');
|
|
19
|
+
* // yMap now looks like: { names: ['John'] }
|
|
20
|
+
*/
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
export const pushInYPath = (entry: Y.Array<any> | Y.Map<any>, path: (string | number)[], value: any) => {
|
|
23
|
+
const yArray = getFromYPath(entry, path);
|
|
24
|
+
if (!isYArray(yArray)) {
|
|
25
|
+
setInYPath(entry, path, Y.Array.from([value]));
|
|
26
|
+
} else {
|
|
27
|
+
yArray.push([value]);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const printPosition = (array: unknown[]) => array.map((it) => isYMap(it) ? it.get('position') ?? 'no-position' : 'not-ymap');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Moves an item to a new position relative to another item in the Y.Array or Y.Map at the specified path.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* moveInYPath(yMap, ['components'], 'a0', 'b1', 'after');
|
|
38
|
+
*/
|
|
39
|
+
export const moveInYPath = (
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
entry: Y.Array<any> | Y.Map<any>,
|
|
42
|
+
path: (string | number)[],
|
|
43
|
+
oldPosition: string,
|
|
44
|
+
relativePosition: string,
|
|
45
|
+
insert: 'before' | 'after',
|
|
46
|
+
) => {
|
|
47
|
+
const yArray = getFromYPath(entry, path);
|
|
48
|
+
if (!isYArray(yArray)) {
|
|
49
|
+
throw new Error(`Can't move. Target item is not yArray in path ${JSON.stringify(path)}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const array = yArray.toArray();
|
|
53
|
+
|
|
54
|
+
const item = array.find((it) => isYMap(it) && it.get('position') === oldPosition);
|
|
55
|
+
const relativeItemIndex = array.findIndex((it) => isYMap(it) && it.get('position') === relativePosition);
|
|
56
|
+
|
|
57
|
+
if (!isYMap(item)) {
|
|
58
|
+
throw new Error(`Can't move. Item in position ${oldPosition} not found. ${printPosition(array)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (relativeItemIndex === -1) {
|
|
62
|
+
throw new Error(`Can't move. Relative item in position ${relativePosition} not found. ${printPosition(array)}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const relativeItem = array[relativeItemIndex];
|
|
66
|
+
if (!isYMap(relativeItem)) {
|
|
67
|
+
throw new Error(`Can't move. Relative item in position ${relativePosition} must be YMap. ${printPosition(array)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (insert === 'before') {
|
|
71
|
+
const left = array[relativeItemIndex - 1];
|
|
72
|
+
const leftPosition = isYMap(left) ? left.get('position') as string ?? null : null;
|
|
73
|
+
|
|
74
|
+
const newPosition = generateKeyBetween(leftPosition, relativeItem.get('position') as string);
|
|
75
|
+
item.set('position', newPosition);
|
|
76
|
+
|
|
77
|
+
return newPosition;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const right = array[relativeItemIndex + 1];
|
|
81
|
+
const rightPosition = isYMap(right) ? right.get('position') as string ?? null : null;
|
|
82
|
+
|
|
83
|
+
const newPosition = generateKeyBetween(relativeItem.get('position') as string, rightPosition);
|
|
84
|
+
item.set('position', newPosition);
|
|
85
|
+
|
|
86
|
+
return newPosition;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Removes an item from the Y.Array or Y.Map at the specified path.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* let yArray = new Y.Array();
|
|
94
|
+
* yArray.push(['item1', 'item2', 'item3']);
|
|
95
|
+
* pullFromYPath(yArray, [1]);
|
|
96
|
+
* // yArray now looks like: ['item1', 'item3']
|
|
97
|
+
*/
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
export const pullFromYPath = (entry: Y.Array<any> | Y.Map<any>, path: (string | number)[], key: string | number) => {
|
|
100
|
+
const yArray = getFromYPath(entry, path) as unknown[];
|
|
101
|
+
if (isYArray(yArray)) {
|
|
102
|
+
if (isIndex(key)) {
|
|
103
|
+
yArray.delete(key);
|
|
104
|
+
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const index = getYArrayIndexByArrayKey(yArray, key);
|
|
109
|
+
yArray.delete(index);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export interface MoveYMapToStartOptions {
|
|
114
|
+
yArray: Y.Array<Y.Map<unknown>>;
|
|
115
|
+
yMap: Y.Map<unknown>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Moves a Y.Map to the start of a Y.Array.
|
|
120
|
+
* */
|
|
121
|
+
export const moveYMapToStart = ({ yArray, yMap }: MoveYMapToEndOptions) => {
|
|
122
|
+
const sortedArray = getSortedYArray(yArray);
|
|
123
|
+
const first = sortedArray[0];
|
|
124
|
+
|
|
125
|
+
const rightIndex = first ? (first.get('position') as string) : null;
|
|
126
|
+
|
|
127
|
+
yMap.set('position', generateKeyBetween(null, rightIndex));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export interface MoveYMapToEndOptions {
|
|
131
|
+
yArray: Y.Array<Y.Map<unknown>>;
|
|
132
|
+
yMap: Y.Map<unknown>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Moves a Y.Map to the end of a Y.Array.
|
|
137
|
+
* */
|
|
138
|
+
export const moveYMapToEnd = ({ yArray, yMap }: MoveYMapToEndOptions) => {
|
|
139
|
+
const sortedArray = getSortedYArray(yArray);
|
|
140
|
+
const last = sortedArray[sortedArray.length - 1];
|
|
141
|
+
|
|
142
|
+
const leftIndex = last ? (last.get('position') as string) : null;
|
|
143
|
+
|
|
144
|
+
yMap.set('position', generateKeyBetween(leftIndex, null));
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export interface MoveYMapBeforeOptions {
|
|
148
|
+
yArray: Y.Array<Y.Map<unknown>>;
|
|
149
|
+
yMap: Y.Map<unknown>;
|
|
150
|
+
relativePosition: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Moves a Y.Map before another Y.Map in a Y.Array.
|
|
155
|
+
* */
|
|
156
|
+
export const moveYMapBefore = ({
|
|
157
|
+
yArray,
|
|
158
|
+
yMap,
|
|
159
|
+
relativePosition,
|
|
160
|
+
}: MoveYMapBeforeOptions) => {
|
|
161
|
+
const { ref: relative, left } = getYArrayRefsByPosition(yArray, relativePosition);
|
|
162
|
+
|
|
163
|
+
const right = relative; // left - x - relative
|
|
164
|
+
|
|
165
|
+
const rightIndex = right ? (right.get('position') as string) : null;
|
|
166
|
+
const leftIndex = left ? (left.get('position') as string) : null;
|
|
167
|
+
|
|
168
|
+
yMap.set('position', generateKeyBetween(leftIndex, rightIndex));
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export interface MoveYMapAfterOptions {
|
|
172
|
+
yArray: Y.Array<Y.Map<unknown>>;
|
|
173
|
+
yMap: Y.Map<unknown>;
|
|
174
|
+
relativePosition: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Moves a Y.Map after another Y.Map in a Y.Array.
|
|
179
|
+
* */
|
|
180
|
+
export const moveYMapAfter = ({
|
|
181
|
+
yArray,
|
|
182
|
+
yMap,
|
|
183
|
+
relativePosition,
|
|
184
|
+
}: MoveYMapAfterOptions) => {
|
|
185
|
+
const { ref: relative, right } = getYArrayRefsByPosition(yArray, relativePosition);
|
|
186
|
+
|
|
187
|
+
const left = relative; // relative - x - right
|
|
188
|
+
|
|
189
|
+
const rightIndex = right ? (right.get('position') as string) : null;
|
|
190
|
+
const leftIndex = left ? (left.get('position') as string) : null;
|
|
191
|
+
|
|
192
|
+
yMap.set('position', generateKeyBetween(leftIndex, rightIndex));
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export interface InsertYMapToEndOptions {
|
|
196
|
+
yArray: Y.Array<Y.Map<unknown>>;
|
|
197
|
+
yMap: Y.Map<unknown>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Inserts a Y.Map to the end of a Y.Array.
|
|
202
|
+
* */
|
|
203
|
+
export const insertYMapToEnd = (opts: InsertYMapToEndOptions) => {
|
|
204
|
+
moveYMapToEnd(opts);
|
|
205
|
+
|
|
206
|
+
// always appended, since sort order is not reliant on array order
|
|
207
|
+
opts.yArray.push([opts.yMap]);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export interface InsertYMapBeforeOptions {
|
|
211
|
+
yArray: Y.Array<Y.Map<unknown>>;
|
|
212
|
+
yMap: Y.Map<unknown>;
|
|
213
|
+
relativePosition: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Inserts a Y.Map before another Y.Map in a Y.Array.
|
|
218
|
+
* */
|
|
219
|
+
export const insertYMapBefore = (opts: InsertYMapBeforeOptions) => {
|
|
220
|
+
moveYMapBefore(opts);
|
|
221
|
+
|
|
222
|
+
// always appended, since sort order is not reliant on array order
|
|
223
|
+
opts.yArray.push([opts.yMap]);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export interface InsertYMapAfterOptions {
|
|
227
|
+
yArray: Y.Array<Y.Map<unknown>>;
|
|
228
|
+
yMap: Y.Map<unknown>;
|
|
229
|
+
relativePosition: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Inserts a Y.Map before another Y.Map in a Y.Array.
|
|
234
|
+
* */
|
|
235
|
+
export const insertYMapAfter = (opts: InsertYMapAfterOptions) => {
|
|
236
|
+
moveYMapAfter(opts);
|
|
237
|
+
|
|
238
|
+
// always appended, since sort order is not reliant on array order
|
|
239
|
+
opts.yArray.push([opts.yMap]);
|
|
240
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isIndex, isNullOrUndefined, isString, isYArray, isYMap,
|
|
5
|
+
} from '@/assertions/index.js';
|
|
6
|
+
import { isArrayKey } from '@/paths/index.js';
|
|
7
|
+
|
|
8
|
+
import { getYArrayIndexByArrayKey } from './y-array-keys.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sets a value at a given path within a Y.Map or Y.Array.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* let ymap = new Y.Map();
|
|
15
|
+
* setInYPath(ymap, ['key'], 'value');
|
|
16
|
+
* // ymap now looks like: { key: 'value' }
|
|
17
|
+
*/
|
|
18
|
+
export const setInYPath = (ymap: Y.Map<unknown> | Y.Array<unknown>, path: (string | number)[], value: unknown): void => {
|
|
19
|
+
let acc: Y.Map<unknown> | Y.Array<unknown> = ymap;
|
|
20
|
+
|
|
21
|
+
for (const [i, rawKey] of path.entries()) {
|
|
22
|
+
const key = isArrayKey(rawKey) ? getYArrayIndexByArrayKey(acc, rawKey) : rawKey;
|
|
23
|
+
|
|
24
|
+
if (isYArray(acc) && isIndex(key)) {
|
|
25
|
+
const index = key;
|
|
26
|
+
|
|
27
|
+
// Last key, set it
|
|
28
|
+
if (i === path.length - 1) {
|
|
29
|
+
acc.delete(index);
|
|
30
|
+
acc.push([value]);
|
|
31
|
+
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Key does not exist, create a container for it
|
|
36
|
+
if (isNullOrUndefined(acc.get(index))) {
|
|
37
|
+
// container can be either an object or an array depending on the next key if it exists
|
|
38
|
+
const nextRawKey = path[i + 1];
|
|
39
|
+
const nextKey = isArrayKey(nextRawKey) ? getYArrayIndexByArrayKey(acc, nextRawKey) : nextRawKey;
|
|
40
|
+
|
|
41
|
+
// if (isArrayKey(nextKey)) {
|
|
42
|
+
// throw Error(`Can't create empty array in "${key}" with next position key (path: ${JSON.stringify(path)})`);
|
|
43
|
+
// }
|
|
44
|
+
|
|
45
|
+
acc.delete(index);
|
|
46
|
+
acc.insert(index, [isIndex(nextKey) ? new Y.Array() : new Y.Map()]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
acc = acc.get(index) as Y.Map<unknown> | Y.Array<unknown>;
|
|
50
|
+
} else if (isYMap(acc) && isString(key)) {
|
|
51
|
+
// Last key, set it
|
|
52
|
+
if (i === path.length - 1) {
|
|
53
|
+
acc.set(key, value);
|
|
54
|
+
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Key does not exist, create a container for it
|
|
59
|
+
if (!acc.has(key) || isNullOrUndefined(acc.get(key))) {
|
|
60
|
+
// container can be either an object or an array depending on the next key if it exists
|
|
61
|
+
const nextRawKey = path[i + 1];
|
|
62
|
+
const nextKey = isArrayKey(nextRawKey) ? getYArrayIndexByArrayKey(acc, nextRawKey) : nextRawKey;
|
|
63
|
+
|
|
64
|
+
// if (isArrayKey(nextKey)) {
|
|
65
|
+
// throw Error(`Can't create empty array in "${key}" with next position key "${nextKey}" (path: ${JSON.stringify(path)})`);
|
|
66
|
+
// }
|
|
67
|
+
|
|
68
|
+
acc.set(key, isIndex(nextKey) ? new Y.Array() : new Y.Map());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
acc = acc.get(key) as Y.Map<unknown> | Y.Array<unknown>;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const yUnset = (yType: Y.Map<unknown> | Y.Array<unknown>, key: string | number) => {
|
|
77
|
+
if (yType instanceof Y.Array && isIndex(key)) {
|
|
78
|
+
yType.delete(Number(key));
|
|
79
|
+
} else if (yType instanceof Y.Map && isString(key)) {
|
|
80
|
+
yType.delete(key);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Removes a nested property from a Y.Map or Y.Array.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* let ymap = new Y.Map();
|
|
89
|
+
* ymap.set('key', 'value');
|
|
90
|
+
* unsetYPath(ymap, ['key']);
|
|
91
|
+
* // ymap now looks like: {}
|
|
92
|
+
*/
|
|
93
|
+
export const unsetYPath = (yType: Y.Map<unknown> | Y.Array<unknown>, path: (string | number)[]): void => {
|
|
94
|
+
let acc: Y.Map<unknown> | Y.Array<unknown> = yType;
|
|
95
|
+
for (const [i, rawKey] of path.entries()) {
|
|
96
|
+
const key = isArrayKey(rawKey) ? getYArrayIndexByArrayKey(acc, rawKey) : rawKey;
|
|
97
|
+
|
|
98
|
+
// Last key, unset it
|
|
99
|
+
if (i === path.length - 1) {
|
|
100
|
+
yUnset(acc, key);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (acc instanceof Y.Array && isIndex(key)) {
|
|
105
|
+
const index = Number(key);
|
|
106
|
+
|
|
107
|
+
// Key does not exist, exit
|
|
108
|
+
if (isNullOrUndefined(acc.get(index))) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
acc = acc.get(index) as Y.Map<unknown> | Y.Array<unknown>;
|
|
113
|
+
} else if (acc instanceof Y.Map && isString(key)) {
|
|
114
|
+
// Key does not exist, exit
|
|
115
|
+
if (!acc.has(key) || isNullOrUndefined(acc.get(key))) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
acc = acc.get(key) as Y.Map<unknown> | Y.Array<unknown>;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// const pathValues: (unknown | Record<string, unknown>)[] = keys.map((_, idx) => {
|
|
124
|
+
// return getFromPath(object, keys.slice(0, idx).join('.'));
|
|
125
|
+
// });
|
|
126
|
+
|
|
127
|
+
// for (let i = pathValues.length - 1; i >= 0; i--) {
|
|
128
|
+
// if (!isEmptyContainer(pathValues[i])) {
|
|
129
|
+
// continue;
|
|
130
|
+
// }
|
|
131
|
+
|
|
132
|
+
// if (i === 0) {
|
|
133
|
+
// unset(object, keys[0]);
|
|
134
|
+
// continue;
|
|
135
|
+
// }
|
|
136
|
+
|
|
137
|
+
// unset(pathValues[i - 1] as Record<string, unknown>, keys[i - 1]);
|
|
138
|
+
// }
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Retrieves a nested property value from a Y.Map or Y.Array.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* let ymap = new Y.Map();
|
|
146
|
+
* ymap.set('key', 'value');
|
|
147
|
+
* const value = getFromYPath(ymap, ['key']);
|
|
148
|
+
* // value is now: 'value'
|
|
149
|
+
*/
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
export const getFromYPath = (entry: Y.Array<any> | Y.Map<any>, path: (string | number)[]): unknown | null => {
|
|
152
|
+
if (!entry) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (path.length === 0) {
|
|
157
|
+
return entry;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let ref = entry as unknown;
|
|
161
|
+
for (const rawKey of path) {
|
|
162
|
+
const key = isArrayKey(rawKey) ? getYArrayIndexByArrayKey(ref, rawKey) : rawKey;
|
|
163
|
+
|
|
164
|
+
if (isYArray(ref)) {
|
|
165
|
+
if (!isIndex(key)) {
|
|
166
|
+
console.warn(`Can't use non index key in YArray ${key}: ${JSON.stringify(path)}`);
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ref = ref.get(key);
|
|
172
|
+
continue;
|
|
173
|
+
} else if (isYMap(ref)) {
|
|
174
|
+
if (!isString(key)) {
|
|
175
|
+
console.warn(`Can't use non string key in YMap ${key}: ${JSON.stringify(path)}`);
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ref = ref.get(key);
|
|
181
|
+
} else {
|
|
182
|
+
console.warn(`Non supported YType on ${key}: ${JSON.stringify(path)}`);
|
|
183
|
+
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return ref;
|
|
189
|
+
};
|