@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/index.d.ts +1816 -0
  3. package/dist/index.js +1564 -0
  4. package/dist/index.js.map +1 -0
  5. package/eslint.config.cjs +14 -0
  6. package/package.json +30 -0
  7. package/src/assertions/basic.ts +58 -0
  8. package/src/assertions/containers.ts +76 -0
  9. package/src/assertions/index.ts +6 -0
  10. package/src/assertions/paths.ts +12 -0
  11. package/src/assertions/rich-text.ts +16 -0
  12. package/src/assertions/yjs.ts +25 -0
  13. package/src/data-objects/data-object.ts +34 -0
  14. package/src/data-objects/index.ts +3 -0
  15. package/src/data-objects/rich-text.ts +38 -0
  16. package/src/dynamic-components/fractional-indexing.ts +321 -0
  17. package/src/dynamic-components/index.ts +6 -0
  18. package/src/dynamic-components/paths.ts +194 -0
  19. package/src/dynamic-components/registry/chats.ts +24 -0
  20. package/src/dynamic-components/registry/content.ts +118 -0
  21. package/src/dynamic-components/registry/forms.ts +525 -0
  22. package/src/dynamic-components/registry/index.ts +6 -0
  23. package/src/dynamic-components/registry/layout.ts +86 -0
  24. package/src/dynamic-components/registry/uikit-dynamic-component.ts +84 -0
  25. package/src/dynamic-components/tools.ts +195 -0
  26. package/src/dynamic-components/types.ts +237 -0
  27. package/src/index.ts +7 -0
  28. package/src/paths/array-keys.ts +164 -0
  29. package/src/paths/array-ops.ts +124 -0
  30. package/src/paths/basic-ops.ts +181 -0
  31. package/src/paths/constants.ts +1 -0
  32. package/src/paths/index.ts +7 -0
  33. package/src/paths/tools.ts +42 -0
  34. package/src/paths/types.ts +133 -0
  35. package/src/y-components/index.ts +3 -0
  36. package/src/y-components/tools.ts +234 -0
  37. package/src/y-components/types.ts +19 -0
  38. package/src/y-tools/array-path-ops.ts +240 -0
  39. package/src/y-tools/basic-path-ops.ts +189 -0
  40. package/src/y-tools/index.ts +6 -0
  41. package/src/y-tools/tools.ts +122 -0
  42. package/src/y-tools/types.ts +32 -0
  43. package/src/y-tools/y-array-keys.ts +47 -0
  44. package/tests/assertions/basic-types.test.ts +78 -0
  45. package/tests/assertions/containers.test.ts +72 -0
  46. package/tests/assertions/paths.test.ts +23 -0
  47. package/tests/assertions/yjs.test.ts +33 -0
  48. package/tests/dynamic-components/paths.test.ts +171 -0
  49. package/tests/dynamic-components/tools.test.ts +121 -0
  50. package/tests/paths/array-keys.test.ts +182 -0
  51. package/tests/paths/array-ops.test.ts +164 -0
  52. package/tests/paths/basic-ops.test.ts +263 -0
  53. package/tests/paths/tools.test.ts +55 -0
  54. package/tests/y-components/tools.test.ts +198 -0
  55. package/tests/y-tools/array-base-ops.test.ts +55 -0
  56. package/tests/y-tools/array-path-ops.test.ts +95 -0
  57. package/tsconfig.json +13 -0
  58. package/tsconfig.lib.json +13 -0
  59. package/tsdown.config.ts +18 -0
  60. package/vitest.config.ts +19 -0
@@ -0,0 +1,124 @@
1
+ import { isIndex, isObject } from '@/assertions/index.js';
2
+ import { generateKeyBetween } from '@/dynamic-components/fractional-indexing.js';
3
+
4
+ import { getIndexByArrayKey, isArrayKey } from './array-keys.js';
5
+ import { getFromPath, setInPath } from './basic-ops.js';
6
+ import { NestedRecord } from './types.js';
7
+
8
+ /**
9
+ * Pushes a value into a nested array in an object using a path.
10
+ * @param {NestedRecord} object - The object to push the value into.
11
+ * @param {(string | number)[]} path - The path to the array in the object.
12
+ * @param {unknown} value - The value to push.
13
+ * @example
14
+ * let obj = { items: ['John'] };
15
+ * pushInPath(obj, ['items'], 'Jane'); // obj is now { items: ['John', 'Jane'] }
16
+ */
17
+ export const pushInPath = (object: NestedRecord, path: (string | number)[], value: unknown) => {
18
+ const array = getFromPath(object, path);
19
+ if (!Array.isArray(array)) {
20
+ setInPath(object, path, [value]);
21
+ } else {
22
+ array.push(value);
23
+ }
24
+ };
25
+
26
+ /**
27
+ * Inserts values into a nested array in an object at a specified index or key using a path.
28
+ * @param {NestedRecord} object - The object to insert the values into.
29
+ * @param {(string | number)[]} path - The path to the array in the object.
30
+ * @param {string | number} keyOrIndex - The index or key at which to insert the values.
31
+ * @param {unknown[]} values - The values to insert.
32
+ * @example
33
+ * let obj = { items: ['John', 'Jane'] };
34
+ * insertInPath(obj, ['items'], 1, ['Joe']); // obj is now { items: ['John', 'Joe', 'Jane'] }
35
+ */
36
+ export const insertInPath = (object: NestedRecord, path: (string | number)[], keyOrIndex: string | number, values: unknown[]) => {
37
+ const array = getFromPath(object, path) as unknown[];
38
+
39
+ if (Array.isArray(array)) {
40
+ if (isIndex(keyOrIndex)) {
41
+ array.splice(keyOrIndex, 0, ...values);
42
+ } else if (isArrayKey(keyOrIndex)) {
43
+ const index = getIndexByArrayKey(array, keyOrIndex);
44
+ array.splice(index, 0, ...values);
45
+ }
46
+ }
47
+ };
48
+
49
+ /**
50
+ * Removes a value from a nested array in an object at a specified index or array key using a path.
51
+ * @param {NestedRecord} object - The object to remove the value from.
52
+ * @param {(string | number)[]} path - The path to the array in the object.
53
+ * @param {string | number} key - The index or key at which to remove the value.
54
+ * @example
55
+ * let obj = { items: ['John', 'Joe', 'Jane'] };
56
+ * pullFromPath(obj, ['items'], 1); // obj is now { items: ['John', 'Jane'] }
57
+ */
58
+ export const pullFromPath = (object: NestedRecord, path: (string | number)[], key: string | number) => {
59
+ const array = getFromPath(object, path) as unknown[];
60
+
61
+ if (Array.isArray(array)) {
62
+ if (isIndex(key)) {
63
+ array.splice(key, 1);
64
+ } else if (isArrayKey(key)) {
65
+ const index = getIndexByArrayKey(array, key);
66
+ array.splice(index, 1);
67
+ }
68
+ }
69
+ };
70
+
71
+ const printPosition = (array: { position?: string }[]) => array.map((it) => isObject(it) ? it.position ?? 'no-position' : 'not-object');
72
+
73
+ /**
74
+ * Moves an item to a new position relative to another item in the array or object at the specified path.
75
+ *
76
+ * @example
77
+ * moveInYPath(values, ['components'], 'a0', 'b1', 'after');
78
+ */
79
+ export const moveInPath = (
80
+ object: NestedRecord,
81
+ path: (string | number)[],
82
+ oldPosition: string,
83
+ relativePosition: string,
84
+ insert: 'before' | 'after',
85
+ ) => {
86
+ const array = getFromPath(object, path);
87
+ if (!Array.isArray(array)) {
88
+ throw new Error(`Can't move. Target item is not array in path ${JSON.stringify(path)}`);
89
+ }
90
+
91
+ const item = array.find((it) => isObject(it) && it.position === oldPosition);
92
+ const relativeItemIndex = array.findIndex((it) => isObject(it) && it.position === relativePosition);
93
+
94
+ if (!isObject(item)) {
95
+ throw new Error(`Can't move. Item in position ${oldPosition} not found. ${printPosition(array)}`);
96
+ }
97
+
98
+ if (relativeItemIndex === -1) {
99
+ throw new Error(`Can't move. Relative item in position ${relativePosition} not found. ${printPosition(array)}`);
100
+ }
101
+
102
+ const relativeItem = array[relativeItemIndex];
103
+ if (!isObject(relativeItem)) {
104
+ throw new Error(`Can't move. Relative item in position ${relativePosition} must be an object. ${printPosition(array)}`);
105
+ }
106
+
107
+ if (insert === 'before') {
108
+ const left = array[relativeItemIndex - 1];
109
+ const leftPosition = isObject(left) ? left.position as string ?? null : null;
110
+
111
+ const newPosition = generateKeyBetween(leftPosition, relativeItem.position as string);
112
+ item.position = newPosition;
113
+
114
+ return newPosition;
115
+ }
116
+
117
+ const right = array[relativeItemIndex + 1];
118
+ const rightPosition = isObject(right) ? right.position as string ?? null : null;
119
+
120
+ const newPosition = generateKeyBetween(relativeItem.position as string, rightPosition);
121
+ item.position = newPosition;
122
+
123
+ return newPosition;
124
+ };
@@ -0,0 +1,181 @@
1
+ // Paths is an array with keys and indexes that represent a path to a value in a deep object.
2
+ // For example, for the object { items: [{ name: 'John' }]} the path to the first item name is ['items', 0, 'name'].
3
+
4
+ import {
5
+ isContainerValue, isIndex, isNullOrUndefined, isObject,
6
+ } from '@/assertions/index.js';
7
+
8
+ import { getIndexByArrayKey, isArrayKey } from './array-keys.js';
9
+ import { NestedRecord } from './types.js';
10
+
11
+ /**
12
+ * Gets a nested property value from an object using a path.
13
+ * @param {NestedRecord | undefined} object - The object to get the value from.
14
+ * @param {(string | number)[]} path - The path to the value in the object.
15
+ * @returns {TValue | undefined} Returns the value at the specified path in the object.
16
+ * @example
17
+ * getFromPath({ items: [{ name: 'John' }] }, ['items', 0, 'name']); // Returns 'John'
18
+ */
19
+ export const getFromPath = <TValue = unknown>(object: NestedRecord | undefined, path: (string | number)[]): TValue | undefined => {
20
+ if (!object) {
21
+ return;
22
+ }
23
+
24
+ if (path.length === 0) {
25
+ return object as TValue;
26
+ }
27
+
28
+ const resolvedValue = path
29
+ .reduce((acc, propKey) => {
30
+ const key = isArrayKey(propKey) ? getIndexByArrayKey(acc, propKey) : propKey;
31
+
32
+ if (isContainerValue(acc) && key in acc) {
33
+ return acc[key];
34
+ }
35
+
36
+ return;
37
+ }, object as unknown);
38
+
39
+ return resolvedValue as TValue | undefined;
40
+ };
41
+
42
+ /**
43
+ * Sets a nested property value in a path, creates the path properties if they don't exist.
44
+ * @param {NestedRecord} object - The object to set the value in.
45
+ * @param {(string | number)[]} path - The path to the value in the object.
46
+ * @param {unknown} value - The value to set.
47
+ * @example
48
+ * let obj = { items: [{ name: 'John' }] };
49
+ * setInPath(obj, ['items', 0, 'name'], 'Jane'); // obj is now { items: [{ name: 'Jane' }] }
50
+ */
51
+ export const setInPath = (object: NestedRecord, path: (string | number)[], value: unknown): void => {
52
+ let acc = object;
53
+ for (const [i, rawKey] of path.entries()) {
54
+ const key = isArrayKey(rawKey) ? getIndexByArrayKey(acc, rawKey) : rawKey;
55
+
56
+ if (Array.isArray(acc)) {
57
+ const index = Number(key);
58
+
59
+ // Last key, set it
60
+ if (i === path.length - 1) {
61
+ acc[index] = value;
62
+
63
+ return;
64
+ }
65
+
66
+ // Key does not exist, create a container for it
67
+ if (!(index in acc) || isNullOrUndefined(acc[index])) {
68
+ const nextRawKey = path[i + 1];
69
+ const nextKey = isArrayKey(nextRawKey) ? getIndexByArrayKey(acc, nextRawKey) : nextRawKey;
70
+ // container can be either an object or an array depending on the next key if it exists
71
+ acc[index] = isIndex(nextKey) ? [] : {
72
+ };
73
+ }
74
+
75
+ acc = acc[index] as Record<string, unknown>;
76
+ } else {
77
+ // Last key, set it
78
+ if (i === path.length - 1) {
79
+ acc[key] = value;
80
+
81
+ return;
82
+ }
83
+
84
+ // Key does not exist, create a container for it
85
+ if (!(key in acc) || isNullOrUndefined(acc[key])) {
86
+ const nextRawKey = path[i + 1];
87
+ const nextKey = isArrayKey(nextRawKey) ? getIndexByArrayKey(acc, nextRawKey) : nextRawKey;
88
+ // container can be either an object or an array depending on the next key if it exists
89
+ acc[key] = isIndex(nextKey) ? [] : {
90
+ };
91
+ }
92
+
93
+ acc = acc[key] as Record<string, unknown>;
94
+ }
95
+ }
96
+ };
97
+
98
+ const unset = (object: Record<string, unknown> | unknown[], key: string | number) => {
99
+ if (Array.isArray(object) && isIndex(key)) {
100
+ object.splice(Number(key), 1);
101
+
102
+ return;
103
+ }
104
+
105
+ if (isObject(object)) {
106
+ delete object[key];
107
+ }
108
+ };
109
+
110
+ /**
111
+ * Removes a nested property from an object using a path.
112
+ * @param {NestedRecord} object - The object to remove the value from.
113
+ * @param {(string | number)[]} path - The path to the value in the object.
114
+ * @example
115
+ * let obj = { items: [{ name: 'John' }] };
116
+ * unsetPath(obj, ['items', 0, 'name']); // obj is now { items: [{}] }
117
+ */
118
+ export const unsetPath = (object: NestedRecord, path: (string | number)[]): void => {
119
+ let acc = object;
120
+ for (const [i, rawKey] of path.entries()) {
121
+ const key = isArrayKey(rawKey) ? getIndexByArrayKey(acc, rawKey) : rawKey;
122
+
123
+ // Last key, unset it
124
+ if (i === path.length - 1) {
125
+ unset(acc, key);
126
+ break;
127
+ }
128
+
129
+ if (Array.isArray(acc)) {
130
+ const index = Number(key);
131
+
132
+ // Key does not exist, exit
133
+ if (!(key in acc) || isNullOrUndefined(acc[index])) {
134
+ break;
135
+ }
136
+
137
+ acc = acc[index] as Record<string, unknown>;
138
+ } else {
139
+ // Key does not exist, exit
140
+ if (!(key in acc) || isNullOrUndefined(acc[key])) {
141
+ break;
142
+ }
143
+
144
+ acc = acc[key] as Record<string, unknown>;
145
+ }
146
+ }
147
+ };
148
+
149
+ /** Checks if a nested property exists in an object using a path.
150
+ * @param {NestedRecord | undefined} object - The object to check the value in.
151
+ * @param {(string | number)[]} path - The path to the value in the object.
152
+ * @returns {boolean} Returns true if the value exists at the specified path, false otherwise.
153
+ * @example
154
+ * existsInPath({ items: [{ name: 'John' }] }, ['items', 0, 'name']); // Returns true
155
+ * existsInPath({ items: [{ name: 'John' }] }, ['items', 1, 'name']); // Returns false
156
+ */
157
+ export const existsInPath = (object: NestedRecord | undefined, path: (string | number)[]): boolean => {
158
+ return getFromPath(object, path) !== undefined;
159
+ };
160
+
161
+ /** * Compares two paths for equality.
162
+ * @param {(string | number)[]} pathA - The first path to compare.
163
+ * @param {(string | number)[]} pathB - The second path to compare.
164
+ * @returns {boolean} Returns true if the paths are equal, false otherwise.
165
+ * @example
166
+ * isSamePath(['items', 0, 'name'], ['items', 0, 'name']); // Returns true
167
+ * isSamePath(['items', 0, 'name'], ['items', 1, 'name']); // Returns false
168
+ */
169
+ export const isSamePath = (pathA: (string | number)[], pathB: (string | number)[]): boolean => {
170
+ if (pathA.length !== pathB.length) {
171
+ return false;
172
+ }
173
+
174
+ for (let i = 0; i < pathA.length; i++) {
175
+ if (pathA[i] !== pathB[i]) {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ return true;
181
+ };
@@ -0,0 +1 @@
1
+ export const PATH_REGEXP = /\.|\[(\d+)\]/;
@@ -0,0 +1,7 @@
1
+ export * from './array-keys.js';
2
+ export * from './array-ops.js';
3
+ export * from './basic-ops.js';
4
+ export * from './constants.js';
5
+ export * from './tools.js';
6
+ export * from './types.js';
7
+
@@ -0,0 +1,42 @@
1
+ import { isString } from '@/assertions/index.js';
2
+
3
+ /**
4
+ * Joins a path with additional items.
5
+ * @param {(string | number)[]} path - The initial path.
6
+ * @param {...(string | number)[]} items - The items to add to the path.
7
+ * @returns {(string | number)[]} Returns the joined path.
8
+ * @throws {Error} If `path` is not an array or if any of `items` are not a string or an integer.
9
+ * @example
10
+ * joinPath(['items', 'id:xxx'], 'name'); // Returns ['items', 'id:xxx', 'name']
11
+ */
12
+ export const joinPath = (path: (string | number)[], ...items: (string | number)[]) => {
13
+ if (!Array.isArray(path)) {
14
+ throw new Error(`path must be array: ${path}`);
15
+ }
16
+
17
+ for (const item of items) {
18
+ if (!isString(item) && !Number.isInteger(item)) {
19
+ throw new Error(`Invalid path item: ${item}`);
20
+ }
21
+ }
22
+
23
+ return path.concat(items);
24
+ };
25
+
26
+ /**
27
+ * Compares two fractional-based position strings for sorting.
28
+ * @param {string} aPosition - The first fractional-based position string.
29
+ * @param {string} bPosition - The second fractional-based position string.
30
+ * @returns {number} Returns -1 if `aPosition` comes before `bPosition`, 1 if `aPosition` comes after `bPosition`, or 0 if they are equal.
31
+ * @example
32
+ * sortByPosition('a1', 'a2'); // Returns -1
33
+ */
34
+ export const sortByPosition = (aPosition: string, bPosition: string) => {
35
+ if (aPosition < bPosition) {
36
+ return -1;
37
+ } else if (aPosition > bPosition) {
38
+ return 1;
39
+ }
40
+
41
+ return 0;
42
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The NestedRecord type represents a deeply nested object structure. It can be one of the following:
3
+ * - A Record with string keys and unknown values
4
+ * - An object with string keys and NestedRecord values
5
+ * - An array of NestedRecords
6
+ * - An array of unknown values
7
+ *
8
+ * This type is useful when working with deeply nested data structures where the shape of the data is not known beforehand.
9
+ *
10
+ * @example
11
+ * let record: NestedRecord = {
12
+ * key: "value",
13
+ * nested: {
14
+ * innerKey: "innerValue",
15
+ * array: [
16
+ * {
17
+ * deepKey: "deepValue"
18
+ * }
19
+ * ]
20
+ * },
21
+ * };
22
+ */
23
+ export type NestedRecord = Record<string, unknown> | { [k: string]: NestedRecord } | NestedRecord[] | unknown[];
24
+ /**
25
+ * Represents the payload for a set operation in a NestedRecord.
26
+ */
27
+
28
+ export interface SetPayload<T> {
29
+ /**
30
+ * The path to the field in the NestedRecord.
31
+ */
32
+ path: (string | number)[];
33
+
34
+ /**
35
+ * The real path to the field in the NestedRecord, represented as an array of component (editor) IDs.
36
+ */
37
+ rpath: string[];
38
+
39
+ /**
40
+ * The change like input letter in text input
41
+ */
42
+ atomic?: boolean;
43
+
44
+ /**
45
+ * The new value for the field.
46
+ */
47
+ value: T;
48
+ }
49
+
50
+ /**
51
+ * Represents the payload for an unset operation in a NestedRecord.
52
+ */
53
+ export interface UnsetPayload {
54
+ /**
55
+ * The path to the field in the NestedRecord.
56
+ */
57
+ path: (string | number)[];
58
+
59
+ /**
60
+ * The real path to the field in the NestedRecord, represented as an array of component (editor) IDs.
61
+ */
62
+ rpath: string[];
63
+ }
64
+
65
+ /**
66
+ * Represents the payload for a push operation in a NestedRecord.
67
+ */
68
+ export interface PushPayload<T> {
69
+ /**
70
+ * The path to the field in the NestedRecord.
71
+ */
72
+ path: (string | number)[];
73
+
74
+ /**
75
+ * The real path to the field in the NestedRecord, represented as an array of component (editor) IDs.
76
+ */
77
+ rpath: string[];
78
+
79
+ /**
80
+ * The value to push into the field.
81
+ */
82
+ value: T;
83
+ }
84
+
85
+ /**
86
+ * Represents the payload for a pull operation in a NestedRecord.
87
+ */
88
+ export interface PullPayload {
89
+ /**
90
+ * The path to the field in the NestedRecord.
91
+ */
92
+ path: (string | number)[];
93
+
94
+ /**
95
+ * The real path to the field in the NestedRecord, represented as an array of component (editor) IDs.
96
+ */
97
+ rpath: string[];
98
+
99
+ /**
100
+ * The key of the value to pull from the field.
101
+ */
102
+ key: string | number;
103
+ }
104
+
105
+ /**
106
+ * Represents the payload for a move operation in a NestedRecord.
107
+ */
108
+ export interface MovePayload {
109
+ /**
110
+ * The path to the field in the NestedRecord.
111
+ */
112
+ path: (string | number)[];
113
+
114
+ /**
115
+ * The real path to the field in the NestedRecord, represented as an array of component (editor) IDs.
116
+ */
117
+ rpath: string[];
118
+
119
+ /**
120
+ * The old fractional position of the field
121
+ */
122
+ oldPosition: string;
123
+
124
+ /**
125
+ * The fractional position of the related field, rely on which the field should be moved.
126
+ */
127
+ relativePosition: string;
128
+
129
+ /**
130
+ * Indicates whether the field should be inserted before or after the relative position.
131
+ */
132
+ insert: 'before' | 'after';
133
+ }
@@ -0,0 +1,3 @@
1
+ export * from './tools.js';
2
+ export * from './types.js';
3
+