@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,321 @@
|
|
|
1
|
+
// License: CC0 (no rights reserved).
|
|
2
|
+
|
|
3
|
+
// This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
|
|
4
|
+
|
|
5
|
+
export const BASE_62_DIGITS =
|
|
6
|
+
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
7
|
+
|
|
8
|
+
const SMALLEST_INTEGER = 'A00000000000000000000000000';
|
|
9
|
+
|
|
10
|
+
const INTEGER_ZERO = 'a0';
|
|
11
|
+
|
|
12
|
+
// `a` may be empty string, `b` is null or non-empty string.
|
|
13
|
+
// `a < b` lexicographically if `b` is non-null.
|
|
14
|
+
// no trailing zeros allowed.
|
|
15
|
+
// digits is a string such as '0123456789' for base 10. Digits must be in
|
|
16
|
+
// ascending character code order!
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} a
|
|
19
|
+
* @param {string | null} b
|
|
20
|
+
* @param {string} digits
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
const midpoint = (a: string, b: string | null, digits: string): string => {
|
|
24
|
+
if (b !== null && a >= b) {
|
|
25
|
+
throw new Error(a + ' >= ' + b);
|
|
26
|
+
}
|
|
27
|
+
if (a.slice(-1) === '0' || (b && b.slice(-1) === '0')) {
|
|
28
|
+
throw new Error('trailing zero');
|
|
29
|
+
}
|
|
30
|
+
if (b) {
|
|
31
|
+
// remove longest common prefix. pad `a` with 0s as we
|
|
32
|
+
// go. note that we don't need to pad `b`, because it can't
|
|
33
|
+
// end before `a` while traversing the common prefix.
|
|
34
|
+
let n = 0;
|
|
35
|
+
while ((a[n] || '0') === b[n]) {
|
|
36
|
+
n++;
|
|
37
|
+
}
|
|
38
|
+
if (n > 0) {
|
|
39
|
+
return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// first digits (or lack of digit) are different
|
|
43
|
+
const digitA = a ? digits.indexOf(a[0]) : 0;
|
|
44
|
+
const digitB = b !== null ? digits.indexOf(b[0]) : digits.length;
|
|
45
|
+
if (digitB - digitA > 1) {
|
|
46
|
+
const midDigit = Math.round(0.5 * (digitA + digitB));
|
|
47
|
+
|
|
48
|
+
return digits[midDigit];
|
|
49
|
+
} else {
|
|
50
|
+
// first digits are consecutive
|
|
51
|
+
if (b && b.length > 1) {
|
|
52
|
+
return b.slice(0, 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// `b` is null or has length 1 (a single digit).
|
|
56
|
+
// the first digit of `a` is the previous digit to `b`,
|
|
57
|
+
// or 9 if `b` is null.
|
|
58
|
+
// given, for example, midpoint('49', '5'), return
|
|
59
|
+
// '4' + midpoint('9', null), which will become
|
|
60
|
+
// '4' + '9' + midpoint('', null), which is '495'
|
|
61
|
+
return digits[digitA] + midpoint(a.slice(1), null, digits);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} int
|
|
67
|
+
* @return {void}
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
const validateInteger = (int: string) => {
|
|
71
|
+
if (int.length !== getIntegerLength(int[0])) {
|
|
72
|
+
throw new Error('invalid integer part of order key: ' + int);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} head
|
|
78
|
+
* @return {number}
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
const getIntegerLength = (head: string): number => {
|
|
82
|
+
if (head >= 'a' && head <= 'z') {
|
|
83
|
+
return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2;
|
|
84
|
+
} else if (head >= 'A' && head <= 'Z') {
|
|
85
|
+
return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2;
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error('invalid order key head: ' + head);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} key
|
|
93
|
+
* @return {string}
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
const getIntegerPart = (key: string) => {
|
|
97
|
+
const integerPartLength = getIntegerLength(key[0]);
|
|
98
|
+
if (integerPartLength > key.length) {
|
|
99
|
+
throw new Error('invalid order key: ' + key);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return key.slice(0, integerPartLength);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} key
|
|
107
|
+
* @return {void}
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
const validateOrderKey = (key: string) => {
|
|
111
|
+
if (key === SMALLEST_INTEGER) {
|
|
112
|
+
throw new Error('invalid order key: ' + key);
|
|
113
|
+
}
|
|
114
|
+
// getIntegerPart will throw if the first character is bad,
|
|
115
|
+
// or the key is too short. we'd call it to check these things
|
|
116
|
+
// even if we didn't need the result
|
|
117
|
+
const i = getIntegerPart(key);
|
|
118
|
+
const f = key.slice(i.length);
|
|
119
|
+
if (f.slice(-1) === '0') {
|
|
120
|
+
throw new Error('invalid order key: ' + key);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// note that this may return null, as there is a largest integer
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} x
|
|
127
|
+
* @param {string} digits
|
|
128
|
+
* @return {string | null}
|
|
129
|
+
*/
|
|
130
|
+
const incrementInteger = (x: string, digits: string) => {
|
|
131
|
+
validateInteger(x);
|
|
132
|
+
const [head, ...digs] = x.split('');
|
|
133
|
+
let carry = true;
|
|
134
|
+
for (let i = digs.length - 1; carry && i >= 0; i--) {
|
|
135
|
+
const d = digits.indexOf(digs[i]) + 1;
|
|
136
|
+
if (d === digits.length) {
|
|
137
|
+
digs[i] = '0';
|
|
138
|
+
} else {
|
|
139
|
+
digs[i] = digits[d];
|
|
140
|
+
carry = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (carry) {
|
|
144
|
+
if (head === 'Z') {
|
|
145
|
+
return 'a0';
|
|
146
|
+
}
|
|
147
|
+
if (head === 'z') {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const h = String.fromCharCode(head.charCodeAt(0) + 1);
|
|
151
|
+
if (h > 'a') {
|
|
152
|
+
digs.push('0');
|
|
153
|
+
} else {
|
|
154
|
+
digs.pop();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return h + digs.join('');
|
|
158
|
+
} else {
|
|
159
|
+
return head + digs.join('');
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// note that this may return null, as there is a smallest integer
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} x
|
|
166
|
+
* @param {string} digits
|
|
167
|
+
* @return {string | null}
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
const decrementInteger = (x: string, digits: string) => {
|
|
171
|
+
validateInteger(x);
|
|
172
|
+
const [head, ...digs] = x.split('');
|
|
173
|
+
let borrow = true;
|
|
174
|
+
for (let i = digs.length - 1; borrow && i >= 0; i--) {
|
|
175
|
+
const d = digits.indexOf(digs[i]) - 1;
|
|
176
|
+
if (d === -1) {
|
|
177
|
+
digs[i] = digits.slice(-1);
|
|
178
|
+
} else {
|
|
179
|
+
digs[i] = digits[d];
|
|
180
|
+
borrow = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (borrow) {
|
|
184
|
+
if (head === 'a') {
|
|
185
|
+
return 'Z' + digits.slice(-1);
|
|
186
|
+
}
|
|
187
|
+
if (head === 'A') {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const h = String.fromCharCode(head.charCodeAt(0) - 1);
|
|
191
|
+
if (h < 'Z') {
|
|
192
|
+
digs.push(digits.slice(-1));
|
|
193
|
+
} else {
|
|
194
|
+
digs.pop();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return h + digs.join('');
|
|
198
|
+
} else {
|
|
199
|
+
return head + digs.join('');
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// `a` is an order key or null (START).
|
|
204
|
+
// `b` is an order key or null (END).
|
|
205
|
+
// `a < b` lexicographically if both are non-null.
|
|
206
|
+
// digits is a string such as '0123456789' for base 10. Digits must be in
|
|
207
|
+
// ascending character code order!
|
|
208
|
+
/**
|
|
209
|
+
* @param {string | null} a
|
|
210
|
+
* @param {string | null} b
|
|
211
|
+
* @param {string=} digits
|
|
212
|
+
* @return {string}
|
|
213
|
+
*/
|
|
214
|
+
export const generateKeyBetween = (a: string | null, b: string | null, digits = BASE_62_DIGITS) => {
|
|
215
|
+
if (a !== null) {
|
|
216
|
+
validateOrderKey(a);
|
|
217
|
+
}
|
|
218
|
+
if (b !== null) {
|
|
219
|
+
validateOrderKey(b);
|
|
220
|
+
}
|
|
221
|
+
if (a !== null && b !== null && a >= b) {
|
|
222
|
+
throw new Error(a + ' >= ' + b);
|
|
223
|
+
}
|
|
224
|
+
if (a === null) {
|
|
225
|
+
if (b === null) {
|
|
226
|
+
return INTEGER_ZERO;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const ib = getIntegerPart(b);
|
|
230
|
+
const fb = b.slice(ib.length);
|
|
231
|
+
if (ib === SMALLEST_INTEGER) {
|
|
232
|
+
return ib + midpoint('', fb, digits);
|
|
233
|
+
}
|
|
234
|
+
if (ib < b) {
|
|
235
|
+
return ib;
|
|
236
|
+
}
|
|
237
|
+
const res = decrementInteger(ib, digits);
|
|
238
|
+
if (res === null) {
|
|
239
|
+
throw new Error('cannot decrement any more');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return res;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (b === null) {
|
|
246
|
+
const ia = getIntegerPart(a);
|
|
247
|
+
const fa = a.slice(ia.length);
|
|
248
|
+
const i = incrementInteger(ia, digits);
|
|
249
|
+
|
|
250
|
+
return i === null ? ia + midpoint(fa, null, digits) : i;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const ia = getIntegerPart(a);
|
|
254
|
+
const fa = a.slice(ia.length);
|
|
255
|
+
const ib = getIntegerPart(b);
|
|
256
|
+
const fb = b.slice(ib.length);
|
|
257
|
+
if (ia === ib) {
|
|
258
|
+
return ia + midpoint(fa, fb, digits);
|
|
259
|
+
}
|
|
260
|
+
const i = incrementInteger(ia, digits);
|
|
261
|
+
if (i === null) {
|
|
262
|
+
throw new Error('cannot increment any more');
|
|
263
|
+
}
|
|
264
|
+
if (i < b) {
|
|
265
|
+
return i;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return ia + midpoint(fa, null, digits);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* same preconditions as generateKeysBetween.
|
|
273
|
+
* n >= 0.
|
|
274
|
+
* Returns an array of n distinct keys in sorted order.
|
|
275
|
+
* If a and b are both null, returns [a0, a1, ...]
|
|
276
|
+
* If one or the other is null, returns consecutive "integer"
|
|
277
|
+
* keys. Otherwise, returns relatively short keys between
|
|
278
|
+
* a and b.
|
|
279
|
+
* @param {string | null} a
|
|
280
|
+
* @param {string | null} b
|
|
281
|
+
* @param {number} n
|
|
282
|
+
* @param {string} digits
|
|
283
|
+
* @return {string[]}
|
|
284
|
+
*/
|
|
285
|
+
export const generateNKeysBetween = (a: string | null, b: string | null, n: number, digits = BASE_62_DIGITS): string[] => {
|
|
286
|
+
if (n === 0) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
if (n === 1) {
|
|
290
|
+
return [generateKeyBetween(a, b, digits)];
|
|
291
|
+
}
|
|
292
|
+
if (b === null) {
|
|
293
|
+
let c = generateKeyBetween(a, b, digits);
|
|
294
|
+
const result = [c];
|
|
295
|
+
for (let i = 0; i < n - 1; i++) {
|
|
296
|
+
c = generateKeyBetween(c, b, digits);
|
|
297
|
+
result.push(c);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
if (a === null) {
|
|
303
|
+
let c = generateKeyBetween(a, b, digits);
|
|
304
|
+
const result = [c];
|
|
305
|
+
for (let i = 0; i < n - 1; i++) {
|
|
306
|
+
c = generateKeyBetween(a, c, digits);
|
|
307
|
+
result.push(c);
|
|
308
|
+
}
|
|
309
|
+
result.reverse();
|
|
310
|
+
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
const mid = Math.floor(n / 2);
|
|
314
|
+
const c = generateKeyBetween(a, b, digits);
|
|
315
|
+
|
|
316
|
+
return [
|
|
317
|
+
...generateNKeysBetween(a, c, mid, digits),
|
|
318
|
+
c,
|
|
319
|
+
...generateNKeysBetween(c, b, n - mid - 1, digits),
|
|
320
|
+
];
|
|
321
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { isString } from '@/assertions/index.js';
|
|
2
|
+
import { isArrayKey } from '@/paths/index.js';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isCollectionComponent,
|
|
6
|
+
isColumnComponent, isRepeatableCollectionComponent, isRowComponent,
|
|
7
|
+
} from './registry/index.js';
|
|
8
|
+
import { DynamicComponent } from './types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get array with components founded by rpath
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const components = [
|
|
15
|
+
* {
|
|
16
|
+
* id: "xxx",
|
|
17
|
+
* component: "collection",
|
|
18
|
+
* props: {
|
|
19
|
+
* components: [
|
|
20
|
+
* {
|
|
21
|
+
* id: "yyy",
|
|
22
|
+
* component: "text-input",
|
|
23
|
+
* props: {
|
|
24
|
+
* label: "Title"
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ]
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* ]
|
|
31
|
+
*
|
|
32
|
+
* const { pathWithComponents } = getComponentsSubtreeByRPath(components, ['xxx', 'yyy']);
|
|
33
|
+
*
|
|
34
|
+
* // pathWithComponents
|
|
35
|
+
* // [
|
|
36
|
+
* // {
|
|
37
|
+
* // id: "xxx",
|
|
38
|
+
* // component: "collection",
|
|
39
|
+
* // ...
|
|
40
|
+
* // },
|
|
41
|
+
* // {
|
|
42
|
+
* // id: "yyy",
|
|
43
|
+
* // component: "text-input",
|
|
44
|
+
* // ...
|
|
45
|
+
* // }
|
|
46
|
+
* // ]
|
|
47
|
+
*
|
|
48
|
+
*/
|
|
49
|
+
export const getComponentsSubtreeByRPath = (components: DynamicComponent[], rpath: string[]) => {
|
|
50
|
+
const pathWithComponents: DynamicComponent[] = [];
|
|
51
|
+
|
|
52
|
+
deepGetComponentsSubtreeByRPath(components, rpath, pathWithComponents);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
pathWithComponents,
|
|
56
|
+
complete: pathWithComponents.length === rpath.length,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const deepGetComponentsSubtreeByRPath = (components: DynamicComponent[], rpath: string[], pathWithComponents: DynamicComponent[]) => {
|
|
61
|
+
const id = rpath[0];
|
|
62
|
+
|
|
63
|
+
const component = components.find((cmp) => cmp.id === id);
|
|
64
|
+
|
|
65
|
+
if (!component) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pathWithComponents.push(component);
|
|
70
|
+
|
|
71
|
+
if (!Array.isArray(component.props.components)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
deepGetComponentsSubtreeByRPath(component.props.components, rpath.slice(1), pathWithComponents);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get array with components founded by values path.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const components = [
|
|
83
|
+
* {
|
|
84
|
+
* id: "xxx",
|
|
85
|
+
* component: "collection",
|
|
86
|
+
* props: {
|
|
87
|
+
* name: "seo",
|
|
88
|
+
* components: [
|
|
89
|
+
* {
|
|
90
|
+
* id: "yyy",
|
|
91
|
+
* component: "text-input",
|
|
92
|
+
* props: {
|
|
93
|
+
* label: "Title",
|
|
94
|
+
* name: "title",
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* ]
|
|
98
|
+
* }
|
|
99
|
+
* }
|
|
100
|
+
* ]
|
|
101
|
+
*
|
|
102
|
+
* const { pathWithComponents } = getComponentsSubtreeByPath(components, ['seo', 'title']);
|
|
103
|
+
*
|
|
104
|
+
* // pathWithComponents
|
|
105
|
+
* // [
|
|
106
|
+
* // {
|
|
107
|
+
* // id: "xxx",
|
|
108
|
+
* // component: "collection",
|
|
109
|
+
* // ...
|
|
110
|
+
* // },
|
|
111
|
+
* // {
|
|
112
|
+
* // id: "yyy",
|
|
113
|
+
* // component: "text-input",
|
|
114
|
+
* // ...
|
|
115
|
+
* // }
|
|
116
|
+
* // ]
|
|
117
|
+
*
|
|
118
|
+
*/
|
|
119
|
+
export const getComponentsSubtreeByPath = (components: DynamicComponent[], path: (string | number)[]) => {
|
|
120
|
+
const pathWithComponents: DynamicComponent[] = [];
|
|
121
|
+
const complete = true;
|
|
122
|
+
|
|
123
|
+
deepGetComponentsSubtreeByPath(components, path, pathWithComponents, complete);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
pathWithComponents,
|
|
127
|
+
complete,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const deepFindComponentPathKey = (components: DynamicComponent[], key: string): DynamicComponent | null => {
|
|
132
|
+
for (const component of components) {
|
|
133
|
+
if ((component.props.name && component.props.name === key) || component.id === key) {
|
|
134
|
+
return component;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (isRowComponent(component) || isColumnComponent(component)) {
|
|
138
|
+
const result = deepFindComponentPathKey(component.props.components, key);
|
|
139
|
+
if (result) {
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const deepGetComponentsSubtreeByPath = (
|
|
149
|
+
components: DynamicComponent[],
|
|
150
|
+
path: (string | number)[],
|
|
151
|
+
pathWithComponents: DynamicComponent[],
|
|
152
|
+
complete: boolean,
|
|
153
|
+
) => {
|
|
154
|
+
const key = path[0];
|
|
155
|
+
|
|
156
|
+
if (!isString(key)) {
|
|
157
|
+
complete = false;
|
|
158
|
+
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const component = deepFindComponentPathKey(components, key);
|
|
163
|
+
|
|
164
|
+
if (!component) {
|
|
165
|
+
complete = false;
|
|
166
|
+
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pathWithComponents.push(component);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Deep dive if repeatable collection.
|
|
174
|
+
*
|
|
175
|
+
* If next key is array key find in props.components next key after array key
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* path = ['fields', 'id:xxx', 'title']
|
|
179
|
+
*
|
|
180
|
+
* cpnst key = 'fields';
|
|
181
|
+
* const nextKey = 'id:xxx', // array key
|
|
182
|
+
* const nextKeyAfterArrayKey = 'title';
|
|
183
|
+
*/
|
|
184
|
+
if (isRepeatableCollectionComponent(component)) {
|
|
185
|
+
const nextKey = path[1];
|
|
186
|
+
if (isArrayKey(nextKey) && path[2]) {
|
|
187
|
+
deepGetComponentsSubtreeByPath(component.props.components, path.slice(2), pathWithComponents, complete);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (isCollectionComponent(component)) {
|
|
192
|
+
deepGetComponentsSubtreeByPath(component.props.components, path.slice(1), pathWithComponents, complete);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { RichText } from '../../data-objects/rich-text.js';
|
|
2
|
+
import { DynamicComponent } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/* Text Message */
|
|
5
|
+
export interface DChatTextMessage extends DynamicComponent {
|
|
6
|
+
component: 'chat-text-message';
|
|
7
|
+
props: {
|
|
8
|
+
content: RichText;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const isChatTextMessageComponent = (component: DynamicComponent): component is DChatTextMessage => component.component === 'chat-text-message';
|
|
13
|
+
|
|
14
|
+
/* Message Delay */
|
|
15
|
+
export interface DChatMessageDelay extends DynamicComponent {
|
|
16
|
+
component: 'chat-message-delay';
|
|
17
|
+
props: {
|
|
18
|
+
delay: number;
|
|
19
|
+
typing: boolean;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line max-len
|
|
24
|
+
export const isChatMessageDelayComponent = (component: DynamicComponent): component is DChatMessageDelay => component.component === 'chat-message-delay';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { RichText } from '../../data-objects/rich-text.js';
|
|
2
|
+
import { DynamicComponent } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/* TEXT INPUT */
|
|
5
|
+
export interface DRichText extends DynamicComponent {
|
|
6
|
+
component: 'rich-text';
|
|
7
|
+
props: {
|
|
8
|
+
content: RichText;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const isRichTextComponent = (component: DynamicComponent): component is DRichText => component.component === 'rich-text';
|
|
13
|
+
|
|
14
|
+
/* RowSettings */
|
|
15
|
+
export interface DEmbedCode extends DynamicComponent {
|
|
16
|
+
component: 'embed-code';
|
|
17
|
+
props: {
|
|
18
|
+
js?: string;
|
|
19
|
+
css?: string;
|
|
20
|
+
html?: string;
|
|
21
|
+
visible?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const isEmbedCodeComponent = (component: DynamicComponent): component is DEmbedCode => {
|
|
26
|
+
return component.component === 'embed-code';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/* Image */
|
|
30
|
+
export interface DImage extends DynamicComponent {
|
|
31
|
+
component: 'embed-code';
|
|
32
|
+
props: {
|
|
33
|
+
imageId: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const isImageComponent = (component: DynamicComponent): component is DImage => component.component === 'image';
|
|
38
|
+
|
|
39
|
+
/* Legacy Content */
|
|
40
|
+
export interface DLegacyContent extends DynamicComponent {
|
|
41
|
+
component: 'legacy-content';
|
|
42
|
+
props: {
|
|
43
|
+
html: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const isLegacyContentComponent = (component: DynamicComponent): component is DLegacyContent => component.component === 'legacy-content';
|
|
48
|
+
|
|
49
|
+
/* Bible Quote */
|
|
50
|
+
export interface DBibleQuote extends DynamicComponent {
|
|
51
|
+
component: 'bible-quote';
|
|
52
|
+
props: {
|
|
53
|
+
verseId: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const isBibleQuoteComponent = (component: DynamicComponent): component is DBibleQuote => component.component === 'bible-quote';
|
|
58
|
+
|
|
59
|
+
/* Bible Passage */
|
|
60
|
+
export interface DBiblePassage extends DynamicComponent {
|
|
61
|
+
component: 'bible-passage';
|
|
62
|
+
props: {
|
|
63
|
+
passage: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const isBiblePassageComponent = (component: DynamicComponent): component is DBiblePassage => component.component === 'bible-passage';
|
|
68
|
+
|
|
69
|
+
/* TESTS */
|
|
70
|
+
|
|
71
|
+
export interface TestItemVariant {
|
|
72
|
+
id: string;
|
|
73
|
+
content: RichText;
|
|
74
|
+
feedback?: RichText;
|
|
75
|
+
correct?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TestItem {
|
|
79
|
+
id: string;
|
|
80
|
+
content: RichText;
|
|
81
|
+
variants: TestItemVariant[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface DTest extends DynamicComponent {
|
|
85
|
+
component: 'test';
|
|
86
|
+
props: {
|
|
87
|
+
title?: string;
|
|
88
|
+
items: TestItem[];
|
|
89
|
+
milestone?: string;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const isTestComponent = (component: DynamicComponent): component is DTest => component.component === 'test';
|
|
94
|
+
|
|
95
|
+
export interface DStatement extends DynamicComponent {
|
|
96
|
+
component: 'statement';
|
|
97
|
+
props: {
|
|
98
|
+
content: RichText;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const isStatementComponent = (component: DynamicComponent): component is DStatement => component.component === 'statement';
|
|
103
|
+
|
|
104
|
+
export interface DPractice extends DynamicComponent {
|
|
105
|
+
component: 'practice';
|
|
106
|
+
props: {
|
|
107
|
+
title?: string;
|
|
108
|
+
content: RichText;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Date when the practice should be checked in ISO 8601 format
|
|
112
|
+
* @example '2023-10-01T00:08:00Z'
|
|
113
|
+
*/
|
|
114
|
+
checkAt?: string;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const isPracticeComponent = (component: DynamicComponent): component is DPractice => component.component === 'practice';
|