@numidev/react-joyride 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +93 -0
- package/dist/index.cjs +2677 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +753 -0
- package/dist/index.d.mts +753 -0
- package/dist/index.mjs +2638 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +158 -0
- package/src/components/Arrow.tsx +138 -0
- package/src/components/Beacon.tsx +141 -0
- package/src/components/Floater.tsx +381 -0
- package/src/components/Loader.tsx +80 -0
- package/src/components/Overlay.tsx +167 -0
- package/src/components/Portal.tsx +17 -0
- package/src/components/Step.tsx +72 -0
- package/src/components/Tooltip/CloseButton.tsx +29 -0
- package/src/components/Tooltip/DefaultTooltip.tsx +82 -0
- package/src/components/Tooltip/index.tsx +163 -0
- package/src/components/TourRenderer.tsx +157 -0
- package/src/defaults.ts +64 -0
- package/src/global.d.ts +8 -0
- package/src/hooks/useControls.ts +219 -0
- package/src/hooks/useDebugLogger.ts +58 -0
- package/src/hooks/useEventEmitter.ts +55 -0
- package/src/hooks/useFocusTrap.ts +72 -0
- package/src/hooks/useJoyride.tsx +32 -0
- package/src/hooks/useLifecycleEffect.ts +512 -0
- package/src/hooks/usePortalElement.ts +49 -0
- package/src/hooks/usePropSync.ts +84 -0
- package/src/hooks/useScrollEffect.ts +217 -0
- package/src/hooks/useTargetPosition.ts +154 -0
- package/src/hooks/useTourEngine.ts +106 -0
- package/src/index.tsx +23 -0
- package/src/literals/index.ts +61 -0
- package/src/modules/changes.ts +20 -0
- package/src/modules/dom.ts +359 -0
- package/src/modules/helpers.tsx +230 -0
- package/src/modules/step.ts +156 -0
- package/src/modules/store.ts +215 -0
- package/src/modules/svg.ts +40 -0
- package/src/styles.ts +183 -0
- package/src/types/common.ts +315 -0
- package/src/types/components.ts +84 -0
- package/src/types/events.ts +89 -0
- package/src/types/floating.ts +60 -0
- package/src/types/index.ts +8 -0
- package/src/types/props.ts +124 -0
- package/src/types/state.ts +49 -0
- package/src/types/step.ts +108 -0
- package/src/types/utilities.ts +26 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import scroll from 'scroll';
|
|
2
|
+
import scrollParent from 'scrollparent';
|
|
3
|
+
|
|
4
|
+
import { LIFECYCLE } from '~/literals';
|
|
5
|
+
|
|
6
|
+
import type { Lifecycle, StepMerged, StepTarget } from '~/types';
|
|
7
|
+
|
|
8
|
+
interface NeedsScrollingOptions {
|
|
9
|
+
isFirstStep: boolean;
|
|
10
|
+
scrollToFirstStep: boolean;
|
|
11
|
+
step: StepMerged;
|
|
12
|
+
target: HTMLElement | null;
|
|
13
|
+
targetLifecycle?: Lifecycle;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function canUseDOM() {
|
|
17
|
+
return !!(typeof window !== 'undefined' && window.document?.createElement);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the absolute document-relative offset of an element by walking up the offsetParent chain.
|
|
22
|
+
*/
|
|
23
|
+
export function getAbsoluteOffset(element: HTMLElement): { left: number; top: number } {
|
|
24
|
+
let top = 0;
|
|
25
|
+
let left = 0;
|
|
26
|
+
let current: HTMLElement | null = element;
|
|
27
|
+
|
|
28
|
+
while (current) {
|
|
29
|
+
top += current.offsetTop;
|
|
30
|
+
left += current.offsetLeft;
|
|
31
|
+
current = current.offsetParent as HTMLElement | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { left, top };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find the bounding client rect
|
|
39
|
+
*/
|
|
40
|
+
export function getClientRect(element: HTMLElement | null) {
|
|
41
|
+
if (!element) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return element.getBoundingClientRect();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Helper function to get the browser-normalized "document height"
|
|
50
|
+
*/
|
|
51
|
+
export function getDocumentHeight(median = false): number {
|
|
52
|
+
const { body, documentElement } = document;
|
|
53
|
+
|
|
54
|
+
if (!body || !documentElement) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (median) {
|
|
59
|
+
const heights = [
|
|
60
|
+
body.scrollHeight,
|
|
61
|
+
body.offsetHeight,
|
|
62
|
+
documentElement.clientHeight,
|
|
63
|
+
documentElement.scrollHeight,
|
|
64
|
+
documentElement.offsetHeight,
|
|
65
|
+
].sort((a, b) => a - b);
|
|
66
|
+
const middle = Math.floor(heights.length / 2);
|
|
67
|
+
|
|
68
|
+
if (heights.length % 2 === 0) {
|
|
69
|
+
return (heights[middle - 1] + heights[middle]) / 2;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return heights[middle];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Math.max(
|
|
76
|
+
body.scrollHeight,
|
|
77
|
+
body.offsetHeight,
|
|
78
|
+
documentElement.clientHeight,
|
|
79
|
+
documentElement.scrollHeight,
|
|
80
|
+
documentElement.offsetHeight,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find and return the target DOM element based on a step's 'target'.
|
|
86
|
+
*/
|
|
87
|
+
export function getElement(element?: StepTarget): HTMLElement | null {
|
|
88
|
+
if (!element) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof element === 'function') {
|
|
93
|
+
try {
|
|
94
|
+
return element();
|
|
95
|
+
} catch (error: any) {
|
|
96
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error(error);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof element === 'object' && 'current' in element) {
|
|
106
|
+
return element.current;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (typeof element === 'string') {
|
|
110
|
+
try {
|
|
111
|
+
return document.querySelector(element);
|
|
112
|
+
} catch (error: any) {
|
|
113
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
console.error(error);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return element;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Find and return the target DOM element based on a step's 'target'.
|
|
127
|
+
*/
|
|
128
|
+
export function getElementPosition(
|
|
129
|
+
element: HTMLElement | null,
|
|
130
|
+
offset: number,
|
|
131
|
+
isFixed?: boolean,
|
|
132
|
+
): number {
|
|
133
|
+
const elementRect = getClientRect(element);
|
|
134
|
+
const parent = getScrollParent(element);
|
|
135
|
+
const hasScrollParent = parent ? !parent.isSameNode(scrollDocument()) : false;
|
|
136
|
+
const isFixedTarget = isFixed ?? hasPosition(element);
|
|
137
|
+
let parentTop = 0;
|
|
138
|
+
let top = elementRect?.top ?? 0;
|
|
139
|
+
|
|
140
|
+
if (hasScrollParent && isFixedTarget) {
|
|
141
|
+
top = elementRect?.top ?? 0;
|
|
142
|
+
} else if (parent instanceof HTMLElement) {
|
|
143
|
+
parentTop = parent.scrollTop;
|
|
144
|
+
|
|
145
|
+
if (!hasScrollParent && !isFixedTarget) {
|
|
146
|
+
top += parentTop;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!parent.isSameNode(scrollDocument())) {
|
|
150
|
+
top += scrollDocument().scrollTop;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return Math.floor(top - offset);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the scroll parent of an element.
|
|
159
|
+
* If the detected parent doesn't actually scroll, fall back to the document.
|
|
160
|
+
*/
|
|
161
|
+
export function getScrollParent(element: HTMLElement | null, forListener?: boolean) {
|
|
162
|
+
if (!element) {
|
|
163
|
+
return scrollDocument();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const parent = scrollParent(element) as HTMLElement;
|
|
167
|
+
|
|
168
|
+
if (parent) {
|
|
169
|
+
if (parent.isSameNode(scrollDocument())) {
|
|
170
|
+
if (forListener) {
|
|
171
|
+
return document;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return scrollDocument();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const hasScrolling = parent.scrollHeight > parent.offsetHeight;
|
|
178
|
+
|
|
179
|
+
if (!hasScrolling) {
|
|
180
|
+
return scrollDocument();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return parent;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function getScrollTargetToCenter(element: Element): number {
|
|
188
|
+
const rect = element.getBoundingClientRect();
|
|
189
|
+
const documentElement = scrollDocument();
|
|
190
|
+
const containerCenter = rect.top + rect.height / 2;
|
|
191
|
+
const viewportCenter = window.innerHeight / 2;
|
|
192
|
+
|
|
193
|
+
return Math.max(0, documentElement.scrollTop + containerCenter - viewportCenter);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get the scrollTop position
|
|
198
|
+
*/
|
|
199
|
+
export function getScrollTo(element: HTMLElement | null, offset: number): number {
|
|
200
|
+
if (!element) {
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const parentElement = scrollParent(element) ?? (scrollDocument() as HTMLElement);
|
|
205
|
+
const scrollMarginTop = parseFloat(getComputedStyle(element).scrollMarginTop) || 0;
|
|
206
|
+
|
|
207
|
+
const parentRect = getClientRect(parentElement);
|
|
208
|
+
const parentScrollTop = parentElement.scrollTop ?? 0;
|
|
209
|
+
|
|
210
|
+
const { offsetTop = 0, scrollTop = 0 } = parentElement;
|
|
211
|
+
let top = element.getBoundingClientRect().top + scrollTop;
|
|
212
|
+
|
|
213
|
+
if (!!offsetTop && (hasCustomScrollParent(element) || hasCustomOffsetParent(element))) {
|
|
214
|
+
const elementRect = element.getBoundingClientRect();
|
|
215
|
+
const elementTopInContainer = elementRect.top - (parentRect?.top ?? 0);
|
|
216
|
+
const elementBottomInContainer = elementTopInContainer + elementRect.height;
|
|
217
|
+
const containerHeight = parentElement.clientHeight;
|
|
218
|
+
const margin = containerHeight * 0.2;
|
|
219
|
+
|
|
220
|
+
// eslint-disable-next-line unicorn/prefer-ternary
|
|
221
|
+
if (elementTopInContainer >= margin && elementBottomInContainer <= containerHeight - margin) {
|
|
222
|
+
top = parentScrollTop;
|
|
223
|
+
} else {
|
|
224
|
+
top = elementTopInContainer + parentScrollTop;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const output = Math.floor(top - offset - scrollMarginTop);
|
|
229
|
+
|
|
230
|
+
return output < 0 ? 0 : output;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check if the element has custom offset parent
|
|
235
|
+
*/
|
|
236
|
+
export function hasCustomOffsetParent(element: HTMLElement): boolean {
|
|
237
|
+
return element.offsetParent !== document.body;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if the element has custom scroll parent
|
|
242
|
+
*/
|
|
243
|
+
export function hasCustomScrollParent(element: HTMLElement | null): boolean {
|
|
244
|
+
if (!element) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const parent = getScrollParent(element);
|
|
249
|
+
|
|
250
|
+
return parent ? !parent.isSameNode(scrollDocument()) : false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check if an element has fixed/sticky position
|
|
255
|
+
*/
|
|
256
|
+
export function hasPosition(el: Element | Node | null, type: string = 'fixed'): boolean {
|
|
257
|
+
if (!el || !(el instanceof Element)) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const { nodeName } = el;
|
|
262
|
+
|
|
263
|
+
if (nodeName === 'BODY' || nodeName === 'HTML') {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (getComputedStyle(el).position === type) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!el.parentNode) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return hasPosition(el.parentNode, type);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Check if the element is visible
|
|
280
|
+
*/
|
|
281
|
+
export function isElementVisible(element: HTMLElement): boolean {
|
|
282
|
+
if (!element) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let parentElement: HTMLElement | null = element;
|
|
287
|
+
|
|
288
|
+
while (parentElement) {
|
|
289
|
+
if (parentElement === document.body) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (parentElement instanceof HTMLElement) {
|
|
294
|
+
const { display, visibility } = getComputedStyle(parentElement);
|
|
295
|
+
|
|
296
|
+
if (display === 'none' || visibility === 'hidden') {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
parentElement = parentElement.parentElement ?? null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function needsScrolling(options: NeedsScrollingOptions): boolean {
|
|
308
|
+
const { isFirstStep, scrollToFirstStep, step, target, targetLifecycle } = options;
|
|
309
|
+
|
|
310
|
+
if (
|
|
311
|
+
step.skipScroll ||
|
|
312
|
+
(isFirstStep && !scrollToFirstStep && targetLifecycle !== LIFECYCLE.TOOLTIP) ||
|
|
313
|
+
step.placement === 'center'
|
|
314
|
+
) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const parent = (target?.isConnected ? getScrollParent(target) : scrollDocument()) as Element;
|
|
319
|
+
const isCustomScrollParent = parent ? !parent.isSameNode(scrollDocument()) : false;
|
|
320
|
+
|
|
321
|
+
if ((step.isFixed || hasPosition(target)) && !isCustomScrollParent) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return parent.scrollHeight > parent.clientHeight;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function scrollDocument(): Element | HTMLElement {
|
|
329
|
+
return document.scrollingElement ?? document.documentElement;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Scroll to position
|
|
334
|
+
*/
|
|
335
|
+
export function scrollTo(
|
|
336
|
+
value: number,
|
|
337
|
+
options: { duration?: number; element: Element | HTMLElement },
|
|
338
|
+
): { cancel: () => void; promise: Promise<void> } {
|
|
339
|
+
const { duration, element } = options;
|
|
340
|
+
|
|
341
|
+
let cancel: () => void = () => {};
|
|
342
|
+
|
|
343
|
+
const promise = new Promise<void>(resolve => {
|
|
344
|
+
const { scrollTop } = element;
|
|
345
|
+
|
|
346
|
+
const limit = value > scrollTop ? value - scrollTop : scrollTop - value;
|
|
347
|
+
|
|
348
|
+
cancel = scroll.top(
|
|
349
|
+
element as HTMLElement,
|
|
350
|
+
value,
|
|
351
|
+
{ duration: limit < 100 ? 50 : duration },
|
|
352
|
+
() => {
|
|
353
|
+
resolve();
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return { cancel, promise };
|
|
359
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { cloneElement, type FC, isValidElement, type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import innerText from 'react-innertext';
|
|
3
|
+
import deepmergeFactory from '@fastify/deepmerge';
|
|
4
|
+
import is from 'is-lite';
|
|
5
|
+
|
|
6
|
+
import type { AnyObject, NarrowPlainObject, PlainObject, Simplify } from '~/types';
|
|
7
|
+
|
|
8
|
+
type RemoveType<TObject, TExclude = undefined> = {
|
|
9
|
+
[Key in keyof TObject as TObject[Key] extends TExclude ? never : Key]: TObject[Key];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
interface GetReactNodeTextOptions {
|
|
13
|
+
defaultValue?: any;
|
|
14
|
+
step?: number;
|
|
15
|
+
steps?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Remove properties with undefined value from an object
|
|
20
|
+
*/
|
|
21
|
+
export function cleanUpObject<T extends PlainObject>(input: T) {
|
|
22
|
+
const output: Record<string, unknown> = {};
|
|
23
|
+
|
|
24
|
+
for (const key in input) {
|
|
25
|
+
if (input[key] !== undefined) {
|
|
26
|
+
output[key] = input[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return output as RemoveType<T>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function deepMerge<T extends object>(...objects: object[]): T {
|
|
34
|
+
return deepmergeFactory({
|
|
35
|
+
all: true,
|
|
36
|
+
isMergeableObject: (value): value is object =>
|
|
37
|
+
!(!is.plainObject(value) || isValidElement(value)),
|
|
38
|
+
})(...objects) as T;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get Object type
|
|
43
|
+
*/
|
|
44
|
+
export function getObjectType(value: unknown): string {
|
|
45
|
+
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getReactNodeText(input: ReactNode, options: GetReactNodeTextOptions = {}): string {
|
|
49
|
+
const { defaultValue, step, steps } = options;
|
|
50
|
+
let text = innerText(input);
|
|
51
|
+
|
|
52
|
+
if (!text) {
|
|
53
|
+
if (
|
|
54
|
+
isValidElement(input) &&
|
|
55
|
+
!Object.values(input.props as Record<string, unknown>).length &&
|
|
56
|
+
getObjectType(input.type) === 'function'
|
|
57
|
+
) {
|
|
58
|
+
try {
|
|
59
|
+
const component = (input.type as FC)({}) as ReactNode;
|
|
60
|
+
|
|
61
|
+
text = getReactNodeText(component, options);
|
|
62
|
+
} catch {
|
|
63
|
+
text = innerText(defaultValue);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
text = innerText(defaultValue);
|
|
67
|
+
}
|
|
68
|
+
} else if ((text.includes('{current}') || text.includes('{total}')) && step && steps) {
|
|
69
|
+
text = text.replace('{current}', step.toString()).replace('{total}', steps.toString());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return text;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Log method calls if debug is enabled
|
|
77
|
+
*/
|
|
78
|
+
export function log(debug: boolean, scope: string, title: string, ...data: unknown[]): void {
|
|
79
|
+
if (!debug) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const now = new Date();
|
|
84
|
+
const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}`;
|
|
85
|
+
|
|
86
|
+
// eslint-disable-next-line no-console
|
|
87
|
+
console.log(
|
|
88
|
+
`${scope} %c${title}%c ${time}`,
|
|
89
|
+
'font-weight: bold',
|
|
90
|
+
'color: gray; font-weight: normal',
|
|
91
|
+
...data,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Merges the defaultProps with literal values with the incoming props, removing undefined values from it that would override the defaultProps.
|
|
97
|
+
* The result is a type-safe object with the defaultProps as required properties.
|
|
98
|
+
*/
|
|
99
|
+
export function mergeProps<TDefaultProps extends PlainObject<any>, TProps extends PlainObject<any>>(
|
|
100
|
+
defaultProps: TDefaultProps,
|
|
101
|
+
props: TProps,
|
|
102
|
+
) {
|
|
103
|
+
const cleanProps = cleanUpObject(props);
|
|
104
|
+
|
|
105
|
+
return { ...defaultProps, ...cleanProps } as unknown as Simplify<
|
|
106
|
+
TProps & Required<Pick<TProps, keyof TDefaultProps & string>>
|
|
107
|
+
>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A function that does nothing.
|
|
112
|
+
*/
|
|
113
|
+
export function noop() {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Type-safe Object.keys()
|
|
119
|
+
*/
|
|
120
|
+
export function objectKeys<T extends AnyObject>(input: T) {
|
|
121
|
+
return Object.keys(input) as Array<keyof T>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Remove properties from an object
|
|
126
|
+
*/
|
|
127
|
+
export function omit<T extends Record<string, any>, K extends keyof T>(
|
|
128
|
+
input: NarrowPlainObject<T>,
|
|
129
|
+
...filter: K[]
|
|
130
|
+
) {
|
|
131
|
+
if (!is.plainObject(input)) {
|
|
132
|
+
throw new TypeError('Expected an object');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const output: any = {};
|
|
136
|
+
|
|
137
|
+
for (const key in input) {
|
|
138
|
+
/* istanbul ignore else */
|
|
139
|
+
if ({}.hasOwnProperty.call(input, key) && !filter.includes(key as unknown as K)) {
|
|
140
|
+
output[key] = input[key];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return output as Omit<T, K>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Select properties from an object
|
|
149
|
+
*/
|
|
150
|
+
export function pick<T extends Record<string, any>, K extends keyof T>(
|
|
151
|
+
input: NarrowPlainObject<T>,
|
|
152
|
+
...filter: K[]
|
|
153
|
+
) {
|
|
154
|
+
if (!is.plainObject(input)) {
|
|
155
|
+
throw new TypeError('Expected an object');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!filter.length) {
|
|
159
|
+
return input;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const output: any = {};
|
|
163
|
+
|
|
164
|
+
for (const key in input) {
|
|
165
|
+
/* istanbul ignore else */
|
|
166
|
+
if ({}.hasOwnProperty.call(input, key) && filter.includes(key as unknown as K)) {
|
|
167
|
+
output[key] = input[key];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return output as Pick<T, K>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function replaceLocaleContent(input: ReactNode, step: number, steps: number): ReactNode {
|
|
175
|
+
const replacer = (text: string) =>
|
|
176
|
+
text.replace('{current}', String(step)).replace('{total}', String(steps));
|
|
177
|
+
|
|
178
|
+
if (getObjectType(input) === 'string') {
|
|
179
|
+
return replacer(input as string);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!isValidElement(input)) {
|
|
183
|
+
return input;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { children } = input.props as { children?: ReactNode };
|
|
187
|
+
|
|
188
|
+
if (is.string(children) && children.includes('{current}')) {
|
|
189
|
+
return cloneElement(input as ReactElement<{ children?: ReactNode }>, {
|
|
190
|
+
children: replacer(children),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (Array.isArray(children)) {
|
|
195
|
+
return cloneElement(input as ReactElement<{ children?: ReactNode }>, {
|
|
196
|
+
children: children.map((child: ReactNode) => {
|
|
197
|
+
if (typeof child === 'string') {
|
|
198
|
+
return replacer(child);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return replaceLocaleContent(child, step, steps);
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (is.function(input.type) && !Object.values(input.props as Record<string, unknown>).length) {
|
|
207
|
+
try {
|
|
208
|
+
const component = (input.type as FC)({}) as ReactNode;
|
|
209
|
+
|
|
210
|
+
return replaceLocaleContent(component, step, steps);
|
|
211
|
+
} catch {
|
|
212
|
+
return input;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return input;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Sort object keys
|
|
221
|
+
*/
|
|
222
|
+
export function sortObjectKeys<T extends PlainObject>(input: T) {
|
|
223
|
+
return objectKeys(input)
|
|
224
|
+
.sort()
|
|
225
|
+
.reduce((acc, key) => {
|
|
226
|
+
acc[key] = input[key];
|
|
227
|
+
|
|
228
|
+
return acc;
|
|
229
|
+
}, {} as T);
|
|
230
|
+
}
|