@redocly/theme 0.55.0-next.1 → 0.55.0-next.3
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/lib/components/JsonViewer/JsonViewer.d.ts +2 -0
- package/lib/components/JsonViewer/JsonViewer.js +3 -1
- package/lib/components/Marker/Marker.d.ts +10 -0
- package/lib/components/Marker/Marker.js +62 -0
- package/lib/core/contexts/CodeWalkthrough/CodeWalkthroughStepsContext.d.ts +1 -1
- package/lib/core/contexts/CodeWalkthrough/CodeWalkthroughStepsContext.js +5 -2
- package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.d.ts +17 -9
- package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.js +242 -47
- package/lib/core/hooks/code-walkthrough/use-code-walkthrough.d.ts +9 -2
- package/lib/core/hooks/code-walkthrough/use-code-walkthrough.js +2 -2
- package/lib/core/hooks/index.d.ts +1 -1
- package/lib/core/hooks/index.js +1 -1
- package/lib/core/hooks/use-active-page-version.d.ts +1 -0
- package/lib/core/hooks/{use-page-active-version.js → use-active-page-version.js} +3 -3
- package/lib/core/types/code-walkthrough.d.ts +13 -0
- package/lib/core/types/code-walkthrough.js +3 -0
- package/lib/core/types/index.d.ts +2 -0
- package/lib/core/types/index.js +2 -0
- package/lib/core/types/marker.d.ts +4 -0
- package/lib/core/types/marker.js +3 -0
- package/lib/core/utils/js-utils.d.ts +18 -0
- package/lib/core/utils/js-utils.js +31 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/markdoc/components/CodeWalkthrough/CodeStep.d.ts +1 -2
- package/lib/markdoc/components/CodeWalkthrough/CodeStep.js +26 -20
- package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +32 -3
- package/lib/markdoc/tags/code-step.js +0 -3
- package/lib/markdoc/tags/code-walkthrough.js +0 -1
- package/package.json +1 -1
- package/src/components/JsonViewer/JsonViewer.tsx +6 -0
- package/src/components/Marker/Marker.tsx +53 -0
- package/src/core/contexts/CodeWalkthrough/CodeWalkthroughStepsContext.tsx +6 -3
- package/src/core/hooks/code-walkthrough/use-code-walkthrough-steps.ts +326 -65
- package/src/core/hooks/code-walkthrough/use-code-walkthrough.ts +9 -6
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/{use-page-active-version.ts → use-active-page-version.ts} +1 -1
- package/src/core/types/code-walkthrough.ts +15 -0
- package/src/core/types/index.ts +2 -0
- package/src/core/types/marker.ts +4 -0
- package/src/core/utils/js-utils.ts +31 -0
- package/src/index.ts +1 -0
- package/src/markdoc/components/CodeWalkthrough/CodeStep.tsx +76 -36
- package/src/markdoc/components/CodeWalkthrough/CodeWalkthrough.tsx +8 -3
- package/src/markdoc/tags/code-step.ts +0 -3
- package/src/markdoc/tags/code-walkthrough.ts +0 -1
- package/lib/core/hooks/use-page-active-version.d.ts +0 -1
|
@@ -2,28 +2,31 @@ import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
|
2
2
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
3
3
|
|
|
4
4
|
import type { CodeWalkthroughStepAttr } from '@redocly/config';
|
|
5
|
+
import type { ActiveStep, MarkerArea, WalkthroughStepsState } from '@redocly/theme/core/types';
|
|
5
6
|
|
|
6
|
-
import { getAdjacentValues } from '@redocly/theme/core/utils';
|
|
7
|
+
import { getAdjacentValues, insertAt, isBrowser, removeElement } from '@redocly/theme/core/utils';
|
|
7
8
|
import { ACTIVE_STEP_QUERY_PARAM } from '@redocly/theme/core/constants';
|
|
8
9
|
|
|
9
|
-
type ActiveStep = string | null;
|
|
10
10
|
type CodeWalkthroughStep = CodeWalkthroughStepAttr & {
|
|
11
11
|
compRef?: HTMLElement;
|
|
12
|
+
markerRef?: HTMLElement;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
setActiveStep: (stepId: ActiveStep) => void;
|
|
17
|
-
register: (element: HTMLElement) => void;
|
|
18
|
-
unregister: (element: HTMLElement) => void;
|
|
19
|
-
lockObserver?: React.RefObject<boolean>;
|
|
20
|
-
filtersElementRef?: React.RefObject<HTMLDivElement | null>;
|
|
15
|
+
type StepWithIndex = CodeWalkthroughStep & {
|
|
16
|
+
index: number;
|
|
21
17
|
};
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
steps: CodeWalkthroughStep[]
|
|
25
|
-
enableDeepLink: boolean
|
|
26
|
-
|
|
19
|
+
type Params = {
|
|
20
|
+
steps: CodeWalkthroughStep[];
|
|
21
|
+
enableDeepLink: boolean;
|
|
22
|
+
root: React.RefObject<HTMLDivElement | null>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function useCodeWalkthroughSteps({
|
|
26
|
+
steps,
|
|
27
|
+
enableDeepLink,
|
|
28
|
+
root,
|
|
29
|
+
}: Params): WalkthroughStepsState {
|
|
27
30
|
const location = useLocation();
|
|
28
31
|
const navigate = useNavigate();
|
|
29
32
|
const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
|
|
@@ -38,80 +41,169 @@ export function useCodeWalkthroughSteps(
|
|
|
38
41
|
enableDeepLink ? searchParams.get(ACTIVE_STEP_QUERY_PARAM) : null,
|
|
39
42
|
);
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
const stepsMap = useMemo(() => {
|
|
45
|
+
const map = new Map<string, StepWithIndex>();
|
|
46
|
+
steps.forEach((step, index) => {
|
|
47
|
+
map.set(step.id, { ...step, index });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return map;
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
+
}, [JSON.stringify(steps)]);
|
|
53
|
+
|
|
54
|
+
const options = useMemo(() => {
|
|
55
|
+
if (!isBrowser()) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const filtersElementHeight = filtersElementRef.current?.clientHeight || 0;
|
|
60
|
+
const navbarHeight = document.querySelector('nav')?.getBoundingClientRect().height || 0;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
filtersElementHeight,
|
|
64
|
+
navbarHeight,
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const [visibleSteps, setVisibleSteps] = useState<StepWithIndex[]>([]);
|
|
69
|
+
|
|
70
|
+
const [markers, setMarkers] = useState<Record<string, MarkerArea>>({});
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!root.current || !visibleSteps.length || !options) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const markersMinTopOffset = options.filtersElementHeight + options.navbarHeight;
|
|
78
|
+
|
|
79
|
+
const rootHeight = root.current?.clientHeight ?? 0;
|
|
80
|
+
const lastStepOffset = visibleSteps[visibleSteps.length - 1]?.compRef?.offsetTop ?? 0;
|
|
81
|
+
const deficit = Math.max(lastStepOffset - (rootHeight - window.innerHeight), 0);
|
|
82
|
+
|
|
83
|
+
const groups = getGroups(visibleSteps);
|
|
84
|
+
let markers: number[] = groups.flatMap((group) => getGroupMarkers(group));
|
|
85
|
+
|
|
86
|
+
if (deficit) {
|
|
87
|
+
const startOffset = markersMinTopOffset;
|
|
88
|
+
const endOffset = Math.max(rootHeight - window.innerHeight, 0);
|
|
53
89
|
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
markers = distributeMarkers({
|
|
91
|
+
endOffset,
|
|
92
|
+
markers,
|
|
93
|
+
startOffset: markersMinTopOffset < endOffset ? startOffset : 0,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setMarkers(
|
|
98
|
+
markers.reduce(
|
|
99
|
+
(acc, marker, index) => {
|
|
100
|
+
const step = visibleSteps[index];
|
|
101
|
+
acc[step.id] = {
|
|
102
|
+
offset: marker,
|
|
103
|
+
height:
|
|
104
|
+
markers[index + 1] || !step.compRef
|
|
105
|
+
? (markers[index + 1] ?? rootHeight) - marker
|
|
106
|
+
: step.compRef.clientHeight,
|
|
107
|
+
};
|
|
108
|
+
return acc;
|
|
109
|
+
},
|
|
110
|
+
{} as Record<string, MarkerArea>,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
|
+
}, [visibleSteps, root.current, options]);
|
|
116
|
+
|
|
117
|
+
const registerMarker = useCallback(
|
|
118
|
+
(stepId: string, element: HTMLElement) => {
|
|
119
|
+
if (observerRef.current) {
|
|
120
|
+
const step = stepsMap.get(stepId);
|
|
121
|
+
if (step) {
|
|
122
|
+
step.markerRef = element;
|
|
56
123
|
}
|
|
57
|
-
|
|
124
|
+
|
|
125
|
+
observerRef.current.observe(element);
|
|
126
|
+
observedElementsRef.current.add(element);
|
|
127
|
+
}
|
|
58
128
|
},
|
|
59
|
-
[
|
|
129
|
+
[stepsMap],
|
|
60
130
|
);
|
|
61
131
|
|
|
62
|
-
const
|
|
63
|
-
(element: HTMLElement) => {
|
|
132
|
+
const removeMarker = useCallback(
|
|
133
|
+
(stepId: string, element: HTMLElement) => {
|
|
64
134
|
if (observerRef.current) {
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
|
|
135
|
+
const step = stepsMap.get(stepId);
|
|
136
|
+
if (step) {
|
|
137
|
+
step.markerRef = undefined;
|
|
68
138
|
}
|
|
69
139
|
|
|
70
140
|
observerRef.current.unobserve(element);
|
|
71
141
|
observedElementsRef.current.delete(element);
|
|
72
142
|
}
|
|
73
143
|
},
|
|
74
|
-
[
|
|
144
|
+
[stepsMap],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const registerStep = useCallback(
|
|
148
|
+
(stepId: string, element: HTMLElement) => {
|
|
149
|
+
const step = stepsMap.get(stepId);
|
|
150
|
+
if (!step) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
step.compRef = element;
|
|
155
|
+
setVisibleSteps((prevSteps) => insertAt(prevSteps, step.index, step));
|
|
156
|
+
},
|
|
157
|
+
[stepsMap],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const removeStep = useCallback(
|
|
161
|
+
(stepId: string) => {
|
|
162
|
+
const step = stepsMap.get(stepId);
|
|
163
|
+
if (!step) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
step.compRef = undefined;
|
|
168
|
+
setVisibleSteps((prevSteps) => removeElement(prevSteps, step));
|
|
169
|
+
setActiveStep((prevStep) => (prevStep === stepId ? null : prevStep));
|
|
170
|
+
},
|
|
171
|
+
[stepsMap],
|
|
75
172
|
);
|
|
76
173
|
|
|
77
174
|
const observerCallback = useCallback(
|
|
78
175
|
(entries: IntersectionObserverEntry[]) => {
|
|
79
|
-
if (lockObserver.current) {
|
|
176
|
+
if (lockObserver.current || !visibleSteps.length) {
|
|
80
177
|
return;
|
|
81
178
|
}
|
|
82
179
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (renderedSteps.length < 2) {
|
|
86
|
-
setActiveStep(renderedSteps[0]?.id || null);
|
|
180
|
+
if (visibleSteps.length < 2) {
|
|
181
|
+
setActiveStep(visibleSteps[0]?.id || null);
|
|
87
182
|
return;
|
|
88
183
|
}
|
|
89
184
|
|
|
90
185
|
for (const entry of entries) {
|
|
91
|
-
const
|
|
186
|
+
const stepId = (entry.target as HTMLElement)?.dataset?.stepId;
|
|
92
187
|
|
|
93
|
-
if (!
|
|
188
|
+
if (!stepId) {
|
|
94
189
|
continue;
|
|
95
190
|
}
|
|
96
191
|
|
|
97
192
|
const { intersectionRatio, boundingClientRect, rootBounds, isIntersecting } = entry;
|
|
98
|
-
const step =
|
|
193
|
+
const step = stepsMap.get(stepId);
|
|
99
194
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
195
|
+
if (!step) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const stepIndex = visibleSteps.findIndex((renderedStep) => renderedStep.id === stepId);
|
|
200
|
+
const { next } = getAdjacentValues(visibleSteps, stepIndex);
|
|
104
201
|
|
|
105
202
|
const intersectionAtTop =
|
|
106
203
|
rootBounds?.bottom !== undefined && boundingClientRect.top < rootBounds.top;
|
|
107
204
|
const stepGoesIn = isIntersecting;
|
|
108
205
|
|
|
109
|
-
if (
|
|
110
|
-
intersectionRatio > 0.8 &&
|
|
111
|
-
intersectionRatio < 1 &&
|
|
112
|
-
intersectionAtTop &&
|
|
113
|
-
activeStep === null
|
|
114
|
-
) {
|
|
206
|
+
if (intersectionRatio > 0.8 && intersectionRatio < 1 && intersectionAtTop) {
|
|
115
207
|
setActiveStep(step.id);
|
|
116
208
|
break;
|
|
117
209
|
}
|
|
@@ -125,32 +217,45 @@ export function useCodeWalkthroughSteps(
|
|
|
125
217
|
newStep = next.id;
|
|
126
218
|
}
|
|
127
219
|
|
|
128
|
-
|
|
129
|
-
setActiveStep(newStep);
|
|
130
|
-
}
|
|
220
|
+
setActiveStep((prevStep) => newStep || prevStep);
|
|
131
221
|
|
|
132
222
|
break;
|
|
133
223
|
}
|
|
134
224
|
}
|
|
135
225
|
},
|
|
136
|
-
[
|
|
226
|
+
[stepsMap, visibleSteps],
|
|
137
227
|
);
|
|
228
|
+
|
|
138
229
|
useEffect(() => {
|
|
139
|
-
|
|
140
|
-
|
|
230
|
+
if (!options) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
141
234
|
const newObserver = new IntersectionObserver(observerCallback, {
|
|
142
|
-
threshold: [0.
|
|
143
|
-
rootMargin: `-${filtersElementHeight + navbarHeight}px 0px 0px 0px`,
|
|
235
|
+
threshold: [0.3, 0.8, 0.9, 0.95],
|
|
236
|
+
rootMargin: `-${options.filtersElementHeight + options.navbarHeight}px 0px 0px 0px`,
|
|
144
237
|
});
|
|
145
238
|
|
|
146
|
-
for (const observedElement of observedElementsRef.current) {
|
|
239
|
+
for (const observedElement of observedElementsRef.current.values()) {
|
|
147
240
|
newObserver.observe(observedElement);
|
|
148
241
|
}
|
|
149
242
|
|
|
150
|
-
// Unobserve all from the old observer
|
|
151
243
|
observerRef.current?.disconnect();
|
|
152
244
|
observerRef.current = newObserver;
|
|
153
|
-
}, [observerCallback]);
|
|
245
|
+
}, [observerCallback, markers, options]);
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (!options) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const rootTopOffset = root.current?.offsetTop ?? 0;
|
|
253
|
+
if (!activeStep && rootTopOffset <= options.navbarHeight) {
|
|
254
|
+
setActiveStep(visibleSteps[0]?.id || null);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
258
|
+
}, [activeStep, root.current, options, visibleSteps]);
|
|
154
259
|
|
|
155
260
|
/**
|
|
156
261
|
* Update the URL search params with the current state of the filters and inputs
|
|
@@ -176,5 +281,161 @@ export function useCodeWalkthroughSteps(
|
|
|
176
281
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
177
282
|
}, [activeStep]);
|
|
178
283
|
|
|
179
|
-
return {
|
|
284
|
+
return {
|
|
285
|
+
registerStep,
|
|
286
|
+
removeStep,
|
|
287
|
+
markers,
|
|
288
|
+
registerMarker,
|
|
289
|
+
removeMarker,
|
|
290
|
+
lockObserver,
|
|
291
|
+
filtersElementRef,
|
|
292
|
+
activeStep,
|
|
293
|
+
setActiveStep,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
type StepsGroup = {
|
|
298
|
+
freeSpace: number;
|
|
299
|
+
usedSpace: number;
|
|
300
|
+
offset: number;
|
|
301
|
+
steps: { offset: number; height: number }[];
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* This function analyzes the offset and height of each step to determine
|
|
305
|
+
* when a new group should be created. A new group is started when there is a free space
|
|
306
|
+
* between the two steps, treating it as the content of the next group header.
|
|
307
|
+
*
|
|
308
|
+
* @param steps - An array of `CodeWalkthroughStep` objects
|
|
309
|
+
*
|
|
310
|
+
* @returns An array of `StepsGroup` objects, each containing the offset from the top of the relative
|
|
311
|
+
* block, the free space at the top of the group, the total space used by the steps within the group
|
|
312
|
+
* and the steps themselves with relative offset and height.
|
|
313
|
+
*/
|
|
314
|
+
function getGroups(steps: CodeWalkthroughStep[]): StepsGroup[] {
|
|
315
|
+
if (!steps.length) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const firstStepOffset = steps[0]?.compRef?.offsetTop ?? 0;
|
|
320
|
+
const firstStepHeight = steps[0]?.compRef?.clientHeight ?? 0;
|
|
321
|
+
const secondStepOffset = steps[1]?.compRef?.offsetTop ?? 0;
|
|
322
|
+
const margin = Math.max(secondStepOffset - firstStepOffset - firstStepHeight, 0);
|
|
323
|
+
|
|
324
|
+
let groupIndex = 0;
|
|
325
|
+
const groups: StepsGroup[] = [
|
|
326
|
+
{
|
|
327
|
+
offset: 0,
|
|
328
|
+
freeSpace: firstStepOffset,
|
|
329
|
+
usedSpace: 0,
|
|
330
|
+
steps: [],
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < steps.length; i++) {
|
|
335
|
+
let currentGroup = groups[groupIndex];
|
|
336
|
+
|
|
337
|
+
const step = steps[i];
|
|
338
|
+
const stepHeight = step.compRef?.clientHeight ?? 0;
|
|
339
|
+
const stepOffset = step.compRef?.offsetTop ?? 0;
|
|
340
|
+
|
|
341
|
+
const prevStepOffset = currentGroup.freeSpace + currentGroup.usedSpace;
|
|
342
|
+
|
|
343
|
+
if (prevStepOffset !== Math.max(stepOffset - currentGroup.offset, 0)) {
|
|
344
|
+
const offset = currentGroup.offset + currentGroup.freeSpace + currentGroup.usedSpace;
|
|
345
|
+
|
|
346
|
+
groupIndex++;
|
|
347
|
+
groups[groupIndex] = {
|
|
348
|
+
offset,
|
|
349
|
+
freeSpace: Math.max(stepOffset - offset, 0),
|
|
350
|
+
usedSpace: 0,
|
|
351
|
+
steps: [],
|
|
352
|
+
};
|
|
353
|
+
currentGroup = groups[groupIndex];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
currentGroup.steps.push({
|
|
357
|
+
offset: stepOffset - currentGroup.offset,
|
|
358
|
+
height: stepHeight,
|
|
359
|
+
ref: step.compRef,
|
|
360
|
+
} as any);
|
|
361
|
+
currentGroup.usedSpace += stepHeight + margin;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return groups;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function getGroupMarkers(group: StepsGroup) {
|
|
368
|
+
if (!group.steps.length) {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (group.steps.length === 1) {
|
|
373
|
+
return [group.offset + group.steps[0].offset - group.freeSpace];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const availableFreeSpace =
|
|
377
|
+
group.freeSpace > 0.3 * window.innerHeight ? 0.3 * window.innerHeight : group.freeSpace;
|
|
378
|
+
const unusedFreeSpace = group.freeSpace - availableFreeSpace;
|
|
379
|
+
const lastStepOffset = group.steps[group.steps.length - 1].offset;
|
|
380
|
+
|
|
381
|
+
// distribute group free space between steps
|
|
382
|
+
return distributeMarkers({
|
|
383
|
+
startOffset: 0,
|
|
384
|
+
endOffset: lastStepOffset - unusedFreeSpace,
|
|
385
|
+
markers: group.steps.map((step) => step.offset),
|
|
386
|
+
additionalSteps: [(marker) => group.offset + unusedFreeSpace + marker],
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Distribute markers preserving the relationship throughout the available space
|
|
392
|
+
* @param startOffset - the starting point of the available space
|
|
393
|
+
* @param endOffset - the end point of the available space
|
|
394
|
+
* @param markers - the markers to distribute
|
|
395
|
+
* @param additionalSteps - additional steps to apply to the markers
|
|
396
|
+
*
|
|
397
|
+
* @returns array of markers positions
|
|
398
|
+
*/
|
|
399
|
+
function distributeMarkers({
|
|
400
|
+
endOffset,
|
|
401
|
+
markers,
|
|
402
|
+
startOffset,
|
|
403
|
+
additionalSteps = [],
|
|
404
|
+
}: {
|
|
405
|
+
startOffset: number;
|
|
406
|
+
endOffset: number;
|
|
407
|
+
markers: number[];
|
|
408
|
+
additionalSteps?: ((marker: number) => number)[];
|
|
409
|
+
}) {
|
|
410
|
+
return markers.map((marker) => {
|
|
411
|
+
const normalizedOffset = getNormalizedNumber({
|
|
412
|
+
min: markers[0],
|
|
413
|
+
max: markers[markers.length - 1],
|
|
414
|
+
value: marker,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const availableSpace = endOffset - startOffset;
|
|
418
|
+
|
|
419
|
+
let result = startOffset + normalizedOffset * availableSpace;
|
|
420
|
+
|
|
421
|
+
for (const additionalStep of additionalSteps) {
|
|
422
|
+
result = additionalStep(result);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return result;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Normalize a number between a min and max value
|
|
431
|
+
* @param min - the minimum value of the distribution
|
|
432
|
+
* @param max - the maximum value of the distribution
|
|
433
|
+
* @param value - the value to normalize
|
|
434
|
+
*
|
|
435
|
+
* @returns normalized number between 0 and 1
|
|
436
|
+
*/
|
|
437
|
+
function getNormalizedNumber(options: { min: number; max: number; value: number }) {
|
|
438
|
+
const { min, max, value } = options;
|
|
439
|
+
|
|
440
|
+
return (value - min) / (max - min);
|
|
180
441
|
}
|
|
@@ -3,12 +3,12 @@ import type {
|
|
|
3
3
|
CodeWalkthroughStepAttr,
|
|
4
4
|
CodeWalkthroughAttr,
|
|
5
5
|
} from '@redocly/config';
|
|
6
|
+
import type { WalkthroughStepsState } from '@redocly/theme/core/types';
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
useCodeWalkthroughSteps,
|
|
9
10
|
useCodeWalkthroughControls,
|
|
10
11
|
type WalkthroughControlsState,
|
|
11
|
-
type WalkthroughStepsState,
|
|
12
12
|
} from '@redocly/theme/core/hooks';
|
|
13
13
|
|
|
14
14
|
export type WalkthroughState = {
|
|
@@ -18,10 +18,13 @@ export type WalkthroughState = {
|
|
|
18
18
|
files: CodeWalkthroughFile[];
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
steps: CodeWalkthroughStepAttr[]
|
|
23
|
-
attributes: Omit<CodeWalkthroughAttr, 'steps' | 'preview'
|
|
24
|
-
|
|
21
|
+
type Params = {
|
|
22
|
+
steps: CodeWalkthroughStepAttr[];
|
|
23
|
+
attributes: Omit<CodeWalkthroughAttr, 'steps' | 'preview'>;
|
|
24
|
+
root: React.RefObject<HTMLDivElement | null>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function useCodeWalkthrough({ steps, attributes, root }: Params): WalkthroughState {
|
|
25
28
|
const { filters, filesets, inputs, toggles, __idx } = attributes;
|
|
26
29
|
/*
|
|
27
30
|
We only enable deep linking for the first CodeWalkthrough,
|
|
@@ -31,7 +34,7 @@ export function useCodeWalkthrough(
|
|
|
31
34
|
*/
|
|
32
35
|
const enableDeepLink = __idx === 1;
|
|
33
36
|
|
|
34
|
-
const stepsState = useCodeWalkthroughSteps(steps, enableDeepLink);
|
|
37
|
+
const stepsState = useCodeWalkthroughSteps({ steps, enableDeepLink, root });
|
|
35
38
|
const controlsState = useCodeWalkthroughControls(filters, inputs, toggles, enableDeepLink);
|
|
36
39
|
|
|
37
40
|
const files: CodeWalkthroughFile[] = filesets
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -36,5 +36,5 @@ export * from '@redocly/theme/core/hooks/code-walkthrough/use-renderable-files';
|
|
|
36
36
|
export * from '@redocly/theme/core/hooks/use-element-size';
|
|
37
37
|
export * from '@redocly/theme/core/hooks/use-time-ago';
|
|
38
38
|
export * from '@redocly/theme/core/hooks/use-input-key-commands';
|
|
39
|
-
export * from '@redocly/theme/core/hooks/use-page-
|
|
39
|
+
export * from '@redocly/theme/core/hooks/use-active-page-version';
|
|
40
40
|
export * from '@redocly/theme/core/hooks/use-page-versions';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useThemeHooks } from '@redocly/theme/core/hooks';
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function useActivePageVersion(): string | undefined {
|
|
4
4
|
const { usePageVersions } = useThemeHooks();
|
|
5
5
|
const { versions } = usePageVersions();
|
|
6
6
|
const activeVersion = versions.find((version) => version.active);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MarkerArea } from '@redocly/theme/core/types';
|
|
2
|
+
|
|
3
|
+
export type ActiveStep = string | null;
|
|
4
|
+
|
|
5
|
+
export type WalkthroughStepsState = {
|
|
6
|
+
activeStep: ActiveStep;
|
|
7
|
+
setActiveStep: (stepId: ActiveStep) => void;
|
|
8
|
+
markers: Record<string, MarkerArea>;
|
|
9
|
+
registerMarker: (stepId: string, element: HTMLElement) => void;
|
|
10
|
+
removeMarker: (stepId: string, element: HTMLElement) => void;
|
|
11
|
+
registerStep: (stepId: string, element: HTMLElement) => void;
|
|
12
|
+
removeStep: (stepId: string) => void;
|
|
13
|
+
lockObserver?: React.RefObject<boolean>;
|
|
14
|
+
filtersElementRef?: React.RefObject<HTMLDivElement | null>;
|
|
15
|
+
};
|
package/src/core/types/index.ts
CHANGED
|
@@ -13,3 +13,5 @@ export * from '@redocly/theme/core/types/user-menu';
|
|
|
13
13
|
export * from '@redocly/theme/core/types/user-claims';
|
|
14
14
|
export * from '@redocly/theme/core/types/common';
|
|
15
15
|
export * from '@redocly/theme/core/types/open-api-server';
|
|
16
|
+
export * from '@redocly/theme/core/types/marker';
|
|
17
|
+
export * from '@redocly/theme/core/types/code-walkthrough';
|
|
@@ -71,3 +71,34 @@ export function getAdjacentValues<T>(
|
|
|
71
71
|
|
|
72
72
|
return { prev: prevValue, next: nextValue };
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Inserts an element at a given index in an array. Returns a new array with the element inserted.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const array = [10, 20, 30, 40];
|
|
80
|
+
* insertAt(array, 2, 25);
|
|
81
|
+
* // returns: [10, 20, 25, 30, 40]
|
|
82
|
+
*/
|
|
83
|
+
export function insertAt<T>(array: T[], index: number, newElement: T): T[] {
|
|
84
|
+
const result = array.concat();
|
|
85
|
+
result.splice(index, 0, newElement);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Removes an element from an array. Returns a new array with the element removed.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* const array = [10, 20, 30, 40];
|
|
94
|
+
* removeElement(array, 20);
|
|
95
|
+
* // returns: [10, 30, 40]
|
|
96
|
+
*/
|
|
97
|
+
export function removeElement<T>(array: T[], item: T): T[] {
|
|
98
|
+
const index = array.indexOf(item);
|
|
99
|
+
if (index === -1) return array;
|
|
100
|
+
|
|
101
|
+
const result = array.slice();
|
|
102
|
+
result.splice(index, 1);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from '@redocly/theme/components/Tooltip/Tooltip';
|
|
|
23
23
|
export * from '@redocly/theme/components/Tags/HttpTag';
|
|
24
24
|
export * from '@redocly/theme/components/Tags/CounterTag';
|
|
25
25
|
export * from '@redocly/theme/components/VersionPicker/VersionPicker';
|
|
26
|
+
export * from '@redocly/theme/components/Marker/Marker';
|
|
26
27
|
/* Buttons */
|
|
27
28
|
export * from '@redocly/theme/components/Buttons/CopyButton';
|
|
28
29
|
export * from '@redocly/theme/components/Buttons/EditPageButton';
|