@central-design-system/components 3.0.0-alpha.3 → 3.0.0-alpha.4
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/package.json +3 -2
- package/src/scss/themes/index.ts +1 -1
- package/src/utils/__test__/helper.test.ts +59 -0
- package/src/utils/__test__/mocks/helper.mock.ts +362 -0
- package/src/utils/__test__/mocks/propFactory.mock.ts +88 -0
- package/src/utils/__test__/propFactory.test.ts +9 -0
- package/src/utils/anchor.ts +76 -0
- package/src/utils/animation.ts +62 -0
- package/src/utils/box.ts +39 -0
- package/src/utils/cache.ts +12 -0
- package/src/utils/changeCase/__test__/lowerCase.test.ts +12 -0
- package/src/utils/changeCase/__test__/mocks/lowerCase.mock.ts +80 -0
- package/src/utils/changeCase/__test__/mocks/noCase.mock.ts +131 -0
- package/src/utils/changeCase/__test__/mocks/paramCase.mock.ts +39 -0
- package/src/utils/changeCase/__test__/mocks/pascalCase.mock.ts +46 -0
- package/src/utils/changeCase/__test__/noCase.test.ts +8 -0
- package/src/utils/changeCase/__test__/paramCase.test.ts +8 -0
- package/src/utils/changeCase/__test__/pascalCase.test.ts +8 -0
- package/src/utils/changeCase/lowerCase.ts +56 -0
- package/src/utils/changeCase/noCase.ts +45 -0
- package/src/utils/changeCase/paramCase.ts +11 -0
- package/src/utils/changeCase/pascalCase.ts +24 -0
- package/src/utils/date.ts +1100 -0
- package/src/utils/dom.ts +24 -0
- package/src/utils/getCurrentInstance.ts +43 -0
- package/src/utils/getScrollParent.ts +29 -0
- package/src/utils/globals.ts +8 -0
- package/src/utils/helpers.ts +311 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/propsFactory.ts +92 -0
package/src/utils/dom.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns:
|
|
3
|
+
* - 'null' if the node is not attached to the DOM
|
|
4
|
+
* - the root node (HTMLDocument | ShadowRoot) otherwise
|
|
5
|
+
*/
|
|
6
|
+
export function attachedRoot(node: Node): null | HTMLDocument | ShadowRoot {
|
|
7
|
+
/* istanbul ignore next */
|
|
8
|
+
if (typeof node.getRootNode !== 'function') {
|
|
9
|
+
// Shadow DOM not supported (IE11), lets find the root of this node
|
|
10
|
+
while (node.parentNode) node = node.parentNode;
|
|
11
|
+
|
|
12
|
+
// The root parent is the document if the node is attached to the DOM
|
|
13
|
+
if (node !== document) return null;
|
|
14
|
+
|
|
15
|
+
return document;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const root = node.getRootNode();
|
|
19
|
+
|
|
20
|
+
// The composed root node is the document if the node is attached to the DOM
|
|
21
|
+
if (root !== document && root.getRootNode({ composed: true }) !== document) return null;
|
|
22
|
+
|
|
23
|
+
return root as HTMLDocument | ShadowRoot;
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getCurrentInstance as _getCurrentInstance } from 'vue';
|
|
2
|
+
import { paramCase } from './';
|
|
3
|
+
|
|
4
|
+
import type { ComponentInternalInstance } from 'vue';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This is a wrapper around Vue's getCurrentInstance
|
|
8
|
+
* WARNING! It's not a public API and can be removed in the future
|
|
9
|
+
* @param name
|
|
10
|
+
* @param message
|
|
11
|
+
*/
|
|
12
|
+
export function getCurrentInstance(name: string, message?: string) {
|
|
13
|
+
const vm = _getCurrentInstance();
|
|
14
|
+
|
|
15
|
+
if (!vm) {
|
|
16
|
+
throw new Error(`[CDS] ${name} ${message || 'must be called from inside a setup function'}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return vm;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getCurrentInstanceName(name = 'composable') {
|
|
23
|
+
const vm = getCurrentInstance(name);
|
|
24
|
+
return paramCase(vm.type?.name || '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let _uid = 0;
|
|
28
|
+
let _map = new WeakMap<ComponentInternalInstance, number>();
|
|
29
|
+
export function getUid() {
|
|
30
|
+
const vm = getCurrentInstance('getUid');
|
|
31
|
+
|
|
32
|
+
if (_map.has(vm)) {
|
|
33
|
+
return _map.get(vm)!;
|
|
34
|
+
} else {
|
|
35
|
+
const uid = _uid++;
|
|
36
|
+
_map.set(vm, uid);
|
|
37
|
+
return uid;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
getUid.reset = () => {
|
|
41
|
+
_uid = 0;
|
|
42
|
+
_map = new WeakMap();
|
|
43
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function getScrollParent(el?: HTMLElement) {
|
|
2
|
+
while (el) {
|
|
3
|
+
if (hasScrollbar(el)) return el;
|
|
4
|
+
el = el.parentElement!;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
return document.scrollingElement as HTMLElement;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getScrollParents(el?: Element | null, stopAt?: Element | null) {
|
|
11
|
+
const elements: HTMLElement[] = [];
|
|
12
|
+
|
|
13
|
+
if (stopAt && el && !stopAt.contains(el)) return elements;
|
|
14
|
+
|
|
15
|
+
while (el) {
|
|
16
|
+
if (hasScrollbar(el)) elements.push(el as HTMLElement);
|
|
17
|
+
if (el === stopAt) break;
|
|
18
|
+
el = el.parentElement!;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return elements;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function hasScrollbar(el?: Element | null) {
|
|
25
|
+
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false;
|
|
26
|
+
|
|
27
|
+
const style = window.getComputedStyle(el);
|
|
28
|
+
return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight);
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const IN_BROWSER = typeof window !== 'undefined';
|
|
2
|
+
export const SUPPORTS_INTERSECTION = IN_BROWSER && 'IntersectionObserver' in window;
|
|
3
|
+
export const SUPPORTS_TOUCH = IN_BROWSER && ('ontouchstart' in window || window.navigator.maxTouchPoints > 0);
|
|
4
|
+
export const SUPPORTS_FOCUS_VISIBLE =
|
|
5
|
+
IN_BROWSER &&
|
|
6
|
+
typeof CSS !== 'undefined' &&
|
|
7
|
+
typeof CSS.supports !== 'undefined' &&
|
|
8
|
+
CSS.supports('selector(:focus-visible)');
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { computed, reactive, watchEffect, toRefs } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type { ComponentPublicInstance, ComputedGetter, PropType, Ref, ToRefs } from 'vue';
|
|
4
|
+
import type { TItemKey } from '@components/composable';
|
|
5
|
+
|
|
6
|
+
export const keyNames = Object.freeze({
|
|
7
|
+
enter: 'Enter',
|
|
8
|
+
tab: 'Tab',
|
|
9
|
+
delete: 'Delete',
|
|
10
|
+
esc: 'Escape',
|
|
11
|
+
space: ' ',
|
|
12
|
+
up: 'ArrowUp',
|
|
13
|
+
down: 'ArrowDown',
|
|
14
|
+
left: 'ArrowLeft',
|
|
15
|
+
right: 'ArrowRight',
|
|
16
|
+
end: 'End',
|
|
17
|
+
home: 'Home',
|
|
18
|
+
del: 'Del',
|
|
19
|
+
backspace: 'Backspace',
|
|
20
|
+
insert: 'Insert',
|
|
21
|
+
pageup: 'PageUp',
|
|
22
|
+
pagedown: 'PageDown',
|
|
23
|
+
shift: 'Shift'
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const classesStates = Object.freeze({
|
|
27
|
+
hover: 'cds-state-hover',
|
|
28
|
+
focus: 'cds-state-focus',
|
|
29
|
+
active: 'cds-state-active',
|
|
30
|
+
visited: 'cds-state-visited',
|
|
31
|
+
disabled: 'cds-state-disabled',
|
|
32
|
+
checked: 'cds-state-checked',
|
|
33
|
+
indeterminate: 'cds-state-indeterminate',
|
|
34
|
+
invalid: 'cds-state-invalid',
|
|
35
|
+
empty: 'cds-state-empty',
|
|
36
|
+
loading: 'cds-state-loading'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if value is a object.
|
|
41
|
+
* @param obj
|
|
42
|
+
*/
|
|
43
|
+
export function isObject(obj: any): obj is object {
|
|
44
|
+
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if value is a Date.
|
|
49
|
+
* @param value
|
|
50
|
+
*/
|
|
51
|
+
export function isDate(value: any) {
|
|
52
|
+
return Object.prototype.toString.call(value) === '[object Date]';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Deep merge two objects.
|
|
57
|
+
* @param source
|
|
58
|
+
* @param target
|
|
59
|
+
* @param arrayFn
|
|
60
|
+
*/
|
|
61
|
+
export function mergeDeep(
|
|
62
|
+
source: Record<string, any> = {},
|
|
63
|
+
target: Record<string, any> = {},
|
|
64
|
+
arrayFn?: (a: unknown[], b: unknown[]) => unknown[]
|
|
65
|
+
) {
|
|
66
|
+
const out: Record<string, any> = {};
|
|
67
|
+
|
|
68
|
+
for (const key in source) {
|
|
69
|
+
out[key] = source[key];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const key in target) {
|
|
73
|
+
const sourceProperty = source[key];
|
|
74
|
+
const targetProperty = target[key];
|
|
75
|
+
|
|
76
|
+
// Only continue deep merging if
|
|
77
|
+
// both properties are objects
|
|
78
|
+
if (isObject(sourceProperty) && isObject(targetProperty)) {
|
|
79
|
+
out[key] = mergeDeep(sourceProperty, targetProperty, arrayFn);
|
|
80
|
+
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(sourceProperty) && Array.isArray(targetProperty) && arrayFn) {
|
|
85
|
+
out[key] = arrayFn(sourceProperty, targetProperty);
|
|
86
|
+
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
out[key] = targetProperty;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Deep compare two values to determine if they are equivalent.
|
|
98
|
+
* @param a
|
|
99
|
+
* @param b
|
|
100
|
+
*/
|
|
101
|
+
export function deepEqual(a: any, b: any): boolean {
|
|
102
|
+
if (a === b) return true;
|
|
103
|
+
|
|
104
|
+
if (a instanceof Date && b instanceof Date && a.getTime() !== b.getTime()) {
|
|
105
|
+
// If the values are Date, compare them as timestamps
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (a !== Object(a) || b !== Object(b)) {
|
|
110
|
+
// If the values aren't objects, they were already checked for equality
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const props = Object.keys(a);
|
|
115
|
+
|
|
116
|
+
if (props.length !== Object.keys(b).length) {
|
|
117
|
+
// Different number of props, don't bother to check
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return props.every((p) => deepEqual(a[p], b[p]));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Wrap value in array if it's not already an array
|
|
126
|
+
* @param value
|
|
127
|
+
*/
|
|
128
|
+
export function wrapInArray<T>(value: T | T[] | null | undefined): T[] {
|
|
129
|
+
return value == null ? [] : Array.isArray(value) ? value : [value];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get the nested value of an object at a given path.
|
|
134
|
+
* @param obj
|
|
135
|
+
* @param path
|
|
136
|
+
* @param fallback
|
|
137
|
+
*/
|
|
138
|
+
export function getNestedValue(obj: any, path: (string | number)[], fallback?: any): any {
|
|
139
|
+
const last = path.length - 1;
|
|
140
|
+
|
|
141
|
+
if (last < 0) return obj === undefined ? fallback : obj;
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < last; i++) {
|
|
144
|
+
if (obj == null) {
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
obj = obj[path[i]];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (obj == null) return fallback;
|
|
151
|
+
|
|
152
|
+
return obj[path[last]] === undefined ? fallback : obj[path[last]];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the value of an object at a given path.
|
|
157
|
+
* @param obj
|
|
158
|
+
* @param path
|
|
159
|
+
* @param fallback
|
|
160
|
+
*/
|
|
161
|
+
export function getObjectValueByPath(obj: any, path: string, fallback?: any) {
|
|
162
|
+
if (obj == null || !path || typeof path !== 'string') return fallback;
|
|
163
|
+
if (obj[path] !== undefined) return obj[path];
|
|
164
|
+
path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
|
165
|
+
path = path.replace(/^\./, ''); // strip a leading dot
|
|
166
|
+
return getNestedValue(obj, path.split('.'), fallback);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function getPropertyFromItem(item: any, property: SelectItemKey, fallback?: any): any {
|
|
170
|
+
if (property == null) return item === undefined ? fallback : item;
|
|
171
|
+
if (item !== Object(item)) return fallback;
|
|
172
|
+
if (typeof property === 'string') return getObjectValueByPath(item, property, fallback);
|
|
173
|
+
if (Array.isArray(property)) return getNestedValue(item, property, fallback);
|
|
174
|
+
if (typeof property !== 'function') return fallback;
|
|
175
|
+
|
|
176
|
+
const value = property(item, fallback);
|
|
177
|
+
return typeof value === 'undefined' ? fallback : value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @description This function is used to return a copy of the object that is composed of the picked object properties.
|
|
182
|
+
* @param obj
|
|
183
|
+
* @param paths
|
|
184
|
+
*/
|
|
185
|
+
export function pick<T extends object, U extends Extract<keyof T, string>>(
|
|
186
|
+
obj: T,
|
|
187
|
+
paths: U[]
|
|
188
|
+
): [yes: MaybePick<T, U>, no: Omit<T, U>];
|
|
189
|
+
export function pick<T extends object, U extends Extract<keyof T, string>>(
|
|
190
|
+
obj: T,
|
|
191
|
+
paths: (U | RegExp)[]
|
|
192
|
+
): [yes: Partial<T>, no: Partial<T>] {
|
|
193
|
+
const found = Object.create(null);
|
|
194
|
+
const rest = Object.create(null);
|
|
195
|
+
|
|
196
|
+
for (const key in obj) {
|
|
197
|
+
if (paths.some((path) => (path instanceof RegExp ? path.test(key) : path === key))) {
|
|
198
|
+
found[key] = obj[key];
|
|
199
|
+
} else {
|
|
200
|
+
rest[key] = obj[key];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return [found, rest];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Convert a computed ref to a record of refs.
|
|
209
|
+
* The getter function must always return an object with the same keys.
|
|
210
|
+
*/
|
|
211
|
+
export function destructComputed<T extends object>(getter: ComputedGetter<T & NotAUnion<T>>): ToRefs<T>;
|
|
212
|
+
export function destructComputed<T extends object>(getter: ComputedGetter<T>) {
|
|
213
|
+
const refs = reactive({}) as T;
|
|
214
|
+
const base = computed(getter);
|
|
215
|
+
watchEffect(
|
|
216
|
+
() => {
|
|
217
|
+
for (const key in base.value) {
|
|
218
|
+
refs[key] = base.value[key];
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{ flush: 'sync' }
|
|
222
|
+
);
|
|
223
|
+
return toRefs(refs);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Array.includes but value can be any type
|
|
228
|
+
* @param arr
|
|
229
|
+
* @param val
|
|
230
|
+
*/
|
|
231
|
+
export function includes(arr: readonly any[], val: any) {
|
|
232
|
+
return arr.includes(val);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function convertToUnit(str: number, unit?: string): string;
|
|
236
|
+
export function convertToUnit(str: string | number | null | undefined, unit?: string): string | undefined;
|
|
237
|
+
export function convertToUnit(str: string | number | null | undefined, unit = 'px'): string | undefined {
|
|
238
|
+
if (str == null || str === '') {
|
|
239
|
+
return undefined;
|
|
240
|
+
} else if (isNaN(+str!)) {
|
|
241
|
+
return String(str);
|
|
242
|
+
} else if (!isFinite(+str!)) {
|
|
243
|
+
return undefined;
|
|
244
|
+
} else {
|
|
245
|
+
return `${Number(str)}${unit}`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Clamp a value between a min and max
|
|
251
|
+
* @param value
|
|
252
|
+
* @param min
|
|
253
|
+
* @param max
|
|
254
|
+
*/
|
|
255
|
+
export function clamp(value: number, min = 0, max = 1) {
|
|
256
|
+
return Math.max(min, Math.min(max, value));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Is value an like event
|
|
261
|
+
* @param key
|
|
262
|
+
*/
|
|
263
|
+
export const isOn = (key: string) => key.startsWith('on');
|
|
264
|
+
|
|
265
|
+
export const EventProp = [Function, Array] as PropType<EventProp>;
|
|
266
|
+
|
|
267
|
+
export function isComponentInstance(obj: any): obj is ComponentPublicInstance {
|
|
268
|
+
return obj?.$el;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function isFixedPosition(el?: HTMLElement) {
|
|
272
|
+
while (el) {
|
|
273
|
+
if (window.getComputedStyle(el).position === 'fixed') {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
el = el.offsetParent as HTMLElement;
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function padStart(string: number | string, targetLength: number, padString: string) {
|
|
282
|
+
targetLength = targetLength >> 0;
|
|
283
|
+
string = String(string);
|
|
284
|
+
padString = String(padString);
|
|
285
|
+
if (string.length > targetLength) {
|
|
286
|
+
return String(string);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
targetLength = targetLength - string.length;
|
|
290
|
+
if (targetLength > padString.length) {
|
|
291
|
+
padString += padString.repeat(targetLength / padString.length);
|
|
292
|
+
}
|
|
293
|
+
return padString.slice(0, targetLength) + String(string);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function pad(n: string | number, length = 2) {
|
|
297
|
+
return padStart(n, length, '0');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Types
|
|
302
|
+
*/
|
|
303
|
+
export type MaybeRef<T> = T | Ref<T>;
|
|
304
|
+
export type SelectItemKey = TItemKey;
|
|
305
|
+
export type MaybePick<T extends object, U extends Extract<keyof T, string>> = Record<string, unknown> extends T
|
|
306
|
+
? Partial<Pick<T, U>>
|
|
307
|
+
: Pick<T, U>;
|
|
308
|
+
export type EventProp<T = (...args: any[]) => any> = T;
|
|
309
|
+
// Only allow a single return type
|
|
310
|
+
type NotAUnion<T> = [T] extends [infer U] ? _NotAUnion<U, U> : never;
|
|
311
|
+
type _NotAUnion<T, U> = U extends any ? ([T] extends [U] ? unknown : never) : never;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from './propsFactory';
|
|
2
|
+
export * from './helpers';
|
|
3
|
+
export * from './getCurrentInstance';
|
|
4
|
+
export * from './getScrollParent';
|
|
5
|
+
export * from './globals';
|
|
6
|
+
export * from './dom';
|
|
7
|
+
export * from './anchor';
|
|
8
|
+
export * from './box';
|
|
9
|
+
export * from './animation';
|
|
10
|
+
export * from './cache';
|
|
11
|
+
|
|
12
|
+
// change case
|
|
13
|
+
export * from './changeCase/noCase';
|
|
14
|
+
export * from './changeCase/lowerCase';
|
|
15
|
+
export * from './changeCase/pascalCase';
|
|
16
|
+
export * from './changeCase/paramCase';
|
|
17
|
+
|
|
18
|
+
// date
|
|
19
|
+
export * from './date';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { PropType } from 'vue';
|
|
2
|
+
import type { Prop, ComponentObjectPropsOptions } from '../shims-vue';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a factory function for props definitions.
|
|
6
|
+
* This is used to define props in a composable then override
|
|
7
|
+
* default values in an implementing component.
|
|
8
|
+
*
|
|
9
|
+
* @example Simplified signature
|
|
10
|
+
* (props: Props) => (defaults?: Record<keyof props, any>) => Props
|
|
11
|
+
*
|
|
12
|
+
* @example Usage
|
|
13
|
+
* const makeProps = propsFactory({
|
|
14
|
+
* foo: String,
|
|
15
|
+
* })
|
|
16
|
+
*
|
|
17
|
+
* defineComponent({
|
|
18
|
+
* props: {
|
|
19
|
+
* ...makeProps({
|
|
20
|
+
* foo: 'a',
|
|
21
|
+
* }),
|
|
22
|
+
* },
|
|
23
|
+
* setup (props) {
|
|
24
|
+
* // would be "string | undefined", now "string" because a default has been provided
|
|
25
|
+
* props.foo
|
|
26
|
+
* },
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
29
|
+
export function propsFactory<PropsOptions extends ComponentObjectPropsOptions = ComponentObjectPropsOptions>(
|
|
30
|
+
props: PropsOptions,
|
|
31
|
+
source: string
|
|
32
|
+
) {
|
|
33
|
+
return <Defaults extends PartialKeys<PropsOptions> = {}>(
|
|
34
|
+
defaults?: Defaults
|
|
35
|
+
): AppendDefault<PropsOptions, Defaults> => {
|
|
36
|
+
return Object.keys(props).reduce<any>((obj, prop) => {
|
|
37
|
+
const isObjectDefinition = typeof props[prop] === 'object' && props[prop] != null && !Array.isArray(props[prop]);
|
|
38
|
+
const definition = isObjectDefinition ? props[prop] : { type: props[prop] };
|
|
39
|
+
|
|
40
|
+
if (defaults && prop in defaults) {
|
|
41
|
+
obj[prop] = {
|
|
42
|
+
...definition,
|
|
43
|
+
default: defaults[prop]
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
obj[prop] = definition;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (source && !obj[prop].source) {
|
|
50
|
+
obj[prop].source = source;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return obj;
|
|
54
|
+
}, {});
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type AppendDefault<T extends ComponentObjectPropsOptions, D extends PartialKeys<T>> = {
|
|
59
|
+
[P in keyof T]-?: unknown extends D[P]
|
|
60
|
+
? T[P]
|
|
61
|
+
: T[P] extends Record<string, unknown>
|
|
62
|
+
? Omit<T[P], 'type' | 'default'> & {
|
|
63
|
+
type: PropType<MergeDefaults<T[P], D[P]>>;
|
|
64
|
+
default: MergeDefaults<T[P], D[P]>;
|
|
65
|
+
}
|
|
66
|
+
: {
|
|
67
|
+
type: PropType<MergeDefaults<T[P], D[P]>>;
|
|
68
|
+
default: MergeDefaults<T[P], D[P]>;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type MergeDefaults<T, D> = unknown extends D ? InferPropType<T> : NonNullable<InferPropType<T>> | D;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Like `Partial<T>` but doesn't care what the value is
|
|
76
|
+
*/
|
|
77
|
+
type PartialKeys<T> = { [P in keyof T]?: unknown };
|
|
78
|
+
|
|
79
|
+
// Copied from Vue
|
|
80
|
+
type InferPropType<T> = T extends null
|
|
81
|
+
? any // null & true would fail to infer
|
|
82
|
+
: T extends { type: null | true }
|
|
83
|
+
? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean`
|
|
84
|
+
: T extends ObjectConstructor | { type: ObjectConstructor }
|
|
85
|
+
? Record<string, any>
|
|
86
|
+
: T extends BooleanConstructor | { type: BooleanConstructor }
|
|
87
|
+
? boolean
|
|
88
|
+
: T extends Prop<infer V, infer D>
|
|
89
|
+
? unknown extends V
|
|
90
|
+
? D
|
|
91
|
+
: V
|
|
92
|
+
: T;
|