@gm-pc/tour 1.27.0 → 1.27.1-beta.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/src/tour.tsx CHANGED
@@ -1,276 +1,276 @@
1
- import React, {
2
- forwardRef,
3
- memo,
4
- MouseEvent,
5
- useEffect,
6
- useImperativeHandle,
7
- useReducer,
8
- useRef,
9
- useState,
10
- ReactText,
11
- } from 'react'
12
- import _ from 'lodash'
13
- import { TourRefOptions, TourProps, TourStepItem } from './types'
14
- import reducer, { initialState, ReducerState } from './reducer'
15
- import { useMutationObserver } from './hook'
16
- import { Portal } from './components'
17
- import SvgMask from './components/svg_mask'
18
- import Guide from './components/guide'
19
- import Action from './components/action'
20
- import {
21
- getNodeRect,
22
- GetNodeRectOptions,
23
- getWindow,
24
- inView,
25
- isBody,
26
- scrollParent,
27
- scrollSmooth,
28
- } from './utils'
29
-
30
- const Tour = forwardRef<TourRefOptions, TourProps>(
31
- (
32
- {
33
- children,
34
- isOpen,
35
- startAt = 0,
36
- steps = [],
37
- scrollDuration = 1,
38
- className,
39
- closeWithMask = false,
40
- onRequestClose,
41
- onAfterOpen,
42
- onBeforeClose,
43
- disableButtons = false,
44
- disableInteraction = true,
45
- disableInteractionClassName,
46
- maskClassName,
47
- rounded = 3,
48
- maskSpace = 10,
49
- },
50
- ref
51
- ) => {
52
- const [current, setCurrent] = useState(0)
53
- const [started, setStarted] = useState(false)
54
- const [state, dispatch] = useReducer(reducer, initialState)
55
- const helper = useRef<HTMLDivElement>(null)
56
- const observer = useRef<HTMLDivElement>()
57
-
58
- useMutationObserver(observer, (mutations, observer) => {
59
- if (isOpen) {
60
- showStep()
61
- mutations.forEach((mutation) => {
62
- if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
63
- setTimeout(() => {
64
- makeCalculation(getNodeRect(mutation.addedNodes[0] as HTMLElement))
65
- }, 500)
66
- }
67
- })
68
- } else {
69
- observer.disconnect()
70
- }
71
- })
72
-
73
- useEffect(() => {
74
- const debouncedShowStep = _.debounce(() => {
75
- showStep()
76
- }, 100)
77
- window.addEventListener('resize', debouncedShowStep, false)
78
-
79
- if (isOpen) {
80
- if (!started) {
81
- setStarted(true)
82
- makeCalculation(
83
- {
84
- width: maskSpace * -1,
85
- height: maskSpace * -1,
86
- top: rounded * -1,
87
- left: rounded * -1,
88
- },
89
- 'center'
90
- )
91
- setCurrent(startAt)
92
- showStep(startAt)
93
- } else {
94
- showStep()
95
- }
96
- if (helper.current) {
97
- helper.current.focus()
98
- document.body.style.overflowY = 'hidden'
99
- if (onAfterOpen && typeof onAfterOpen === 'function') {
100
- onAfterOpen(helper.current)
101
- }
102
- }
103
- }
104
-
105
- return () => {
106
- window.removeEventListener('resize', debouncedShowStep)
107
- }
108
- }, [current, isOpen])
109
-
110
- useImperativeHandle(ref, () => ({
111
- apiToNextStep() {
112
- handleNextStep()
113
- },
114
- apiClose() {
115
- close()
116
- },
117
- apiRecalculate() {
118
- calculatePosition()
119
- },
120
- }))
121
-
122
- async function showStep(nextStep?: number) {
123
- const step = steps[nextStep!] ?? steps[current]
124
- if (step.actionBefore && typeof step.actionBefore === 'function') {
125
- await step.actionBefore()
126
- }
127
- if (step.observe) {
128
- observer.current = document.querySelector(step.observe) as HTMLDivElement
129
- }
130
- calculatePosition(nextStep)
131
- }
132
-
133
- function calculatePosition(nextStep?: number) {
134
- const step = steps[nextStep!] ?? steps[current]
135
- const node = getNode(step)
136
- const { w, h } = getWindow()
137
- if (node) {
138
- const nodeRect = getNodeRect(node as HTMLElement)
139
- if (!inView({ ...nodeRect, w, h })) {
140
- const parentScroll = scrollParent(node)
141
- const offset = nodeRect.height > h ? -25 : -(h / 2) + nodeRect.height / 2
142
- scrollSmooth(node as HTMLElement, {
143
- context: isBody(parentScroll as HTMLElement)
144
- ? window
145
- : (parentScroll as HTMLElement),
146
- duration: scrollDuration,
147
- offset,
148
- callback(target: ReactText | HTMLElement) {
149
- makeCalculation(getNodeRect(target as HTMLElement), step.position)
150
- },
151
- })
152
- } else {
153
- makeCalculation(nodeRect, step.position)
154
- }
155
- } else {
156
- dispatch({
157
- type: 'NO_DOM_NODE',
158
- helperPosition: step.position as ReducerState['helperPosition'],
159
- w,
160
- h,
161
- inDOM: false,
162
- })
163
- }
164
- }
165
-
166
- function makeCalculation(
167
- nodeRect: Partial<GetNodeRectOptions>,
168
- helperPosition?: TourStepItem['position']
169
- ) {
170
- const { w, h } = getWindow()
171
- const { width: helperWidth, height: helperHeight } = getNodeRect(
172
- helper.current as HTMLDivElement
173
- )
174
- dispatch({
175
- type: 'HAS_DOM_NODE',
176
- ...nodeRect,
177
- helperWidth,
178
- helperHeight,
179
- helperPosition: helperPosition as ReducerState['helperPosition'],
180
- w,
181
- h,
182
- inDOM: true,
183
- })
184
- }
185
-
186
- function getNode(step: TourStepItem) {
187
- return step.selector ? document.querySelector(step.selector) : null
188
- }
189
-
190
- async function runCurrentActionAfter() {
191
- const step = steps[current]
192
- if (step.actionAfter && typeof step.actionAfter === 'function') {
193
- await step.actionAfter(getNode(step))
194
- }
195
- }
196
-
197
- async function close(event?: MouseEvent) {
198
- document.body.style.overflowY = 'auto'
199
- await runCurrentActionAfter()
200
- if (onBeforeClose && typeof onBeforeClose === 'function') {
201
- onBeforeClose(helper.current)
202
- }
203
- onRequestClose && onRequestClose(event)
204
- }
205
-
206
- const handleMaskClickHandler = (event: MouseEvent<HTMLDivElement>): void => {
207
- if (closeWithMask) {
208
- close(event)
209
- }
210
- }
211
-
212
- async function goTo(index: number) {
213
- await runCurrentActionAfter()
214
- setCurrent(index)
215
- }
216
-
217
- const handleNextStep = (): void => {
218
- goTo(current < steps?.length - 1 ? current + 1 : current)
219
- }
220
-
221
- return isOpen ? (
222
- <Portal>
223
- <SvgMask
224
- onClick={handleMaskClickHandler}
225
- windowWidth={state.w}
226
- windowHeight={state.h}
227
- targetWidth={state.width}
228
- targetHeight={state.height}
229
- targetTop={state.top}
230
- targetLeft={state.left}
231
- padding={maskSpace}
232
- rounded={rounded}
233
- className={maskClassName}
234
- disableInteraction={disableInteraction || steps[current].stepInteraction}
235
- disableInteractionClassName={disableInteractionClassName}
236
- />
237
- <Guide
238
- ref={helper}
239
- windowWidth={state.w}
240
- windowHeight={state.h}
241
- targetWidth={state.width}
242
- targetHeight={state.height}
243
- targetTop={state.top}
244
- targetLeft={state.left}
245
- targetRight={state.right}
246
- targetBottom={state.bottom}
247
- helperWidth={state.helperWidth!}
248
- helperHeight={state.helperHeight!}
249
- helperPosition={state.helperPosition}
250
- padding={maskSpace}
251
- tabIndex={-1}
252
- current={current}
253
- style={steps[current].style ?? {}}
254
- rounded={rounded}
255
- className={className}
256
- >
257
- <>
258
- {children}
259
- {steps[current].content}
260
- {!disableButtons && (
261
- <Action
262
- isLastItem={current === steps?.length - 1}
263
- onNextStep={handleNextStep}
264
- onClose={(event) => close(event)}
265
- />
266
- )}
267
- </>
268
- </Guide>
269
- </Portal>
270
- ) : null
271
- }
272
- )
273
-
274
- Tour.displayName = 'Tour'
275
-
276
- export default memo(Tour)
1
+ import React, {
2
+ forwardRef,
3
+ memo,
4
+ MouseEvent,
5
+ useEffect,
6
+ useImperativeHandle,
7
+ useReducer,
8
+ useRef,
9
+ useState,
10
+ ReactText,
11
+ } from 'react'
12
+ import _ from 'lodash'
13
+ import { TourRefOptions, TourProps, TourStepItem } from './types'
14
+ import reducer, { initialState, ReducerState } from './reducer'
15
+ import { useMutationObserver } from './hook'
16
+ import { Portal } from './components'
17
+ import SvgMask from './components/svg_mask'
18
+ import Guide from './components/guide'
19
+ import Action from './components/action'
20
+ import {
21
+ getNodeRect,
22
+ GetNodeRectOptions,
23
+ getWindow,
24
+ inView,
25
+ isBody,
26
+ scrollParent,
27
+ scrollSmooth,
28
+ } from './utils'
29
+
30
+ const Tour = forwardRef<TourRefOptions, TourProps>(
31
+ (
32
+ {
33
+ children,
34
+ isOpen,
35
+ startAt = 0,
36
+ steps = [],
37
+ scrollDuration = 1,
38
+ className,
39
+ closeWithMask = false,
40
+ onRequestClose,
41
+ onAfterOpen,
42
+ onBeforeClose,
43
+ disableButtons = false,
44
+ disableInteraction = true,
45
+ disableInteractionClassName,
46
+ maskClassName,
47
+ rounded = 3,
48
+ maskSpace = 10,
49
+ },
50
+ ref
51
+ ) => {
52
+ const [current, setCurrent] = useState(0)
53
+ const [started, setStarted] = useState(false)
54
+ const [state, dispatch] = useReducer(reducer, initialState)
55
+ const helper = useRef<HTMLDivElement>(null)
56
+ const observer = useRef<HTMLDivElement>()
57
+
58
+ useMutationObserver(observer, (mutations, observer) => {
59
+ if (isOpen) {
60
+ showStep()
61
+ mutations.forEach((mutation) => {
62
+ if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
63
+ setTimeout(() => {
64
+ makeCalculation(getNodeRect(mutation.addedNodes[0] as HTMLElement))
65
+ }, 500)
66
+ }
67
+ })
68
+ } else {
69
+ observer.disconnect()
70
+ }
71
+ })
72
+
73
+ useEffect(() => {
74
+ const debouncedShowStep = _.debounce(() => {
75
+ showStep()
76
+ }, 100)
77
+ window.addEventListener('resize', debouncedShowStep, false)
78
+
79
+ if (isOpen) {
80
+ if (!started) {
81
+ setStarted(true)
82
+ makeCalculation(
83
+ {
84
+ width: maskSpace * -1,
85
+ height: maskSpace * -1,
86
+ top: rounded * -1,
87
+ left: rounded * -1,
88
+ },
89
+ 'center'
90
+ )
91
+ setCurrent(startAt)
92
+ showStep(startAt)
93
+ } else {
94
+ showStep()
95
+ }
96
+ if (helper.current) {
97
+ helper.current.focus()
98
+ document.body.style.overflowY = 'hidden'
99
+ if (onAfterOpen && typeof onAfterOpen === 'function') {
100
+ onAfterOpen(helper.current)
101
+ }
102
+ }
103
+ }
104
+
105
+ return () => {
106
+ window.removeEventListener('resize', debouncedShowStep)
107
+ }
108
+ }, [current, isOpen])
109
+
110
+ useImperativeHandle(ref, () => ({
111
+ apiToNextStep() {
112
+ handleNextStep()
113
+ },
114
+ apiClose() {
115
+ close()
116
+ },
117
+ apiRecalculate() {
118
+ calculatePosition()
119
+ },
120
+ }))
121
+
122
+ async function showStep(nextStep?: number) {
123
+ const step = steps[nextStep!] ?? steps[current]
124
+ if (step.actionBefore && typeof step.actionBefore === 'function') {
125
+ await step.actionBefore()
126
+ }
127
+ if (step.observe) {
128
+ observer.current = document.querySelector(step.observe) as HTMLDivElement
129
+ }
130
+ calculatePosition(nextStep)
131
+ }
132
+
133
+ function calculatePosition(nextStep?: number) {
134
+ const step = steps[nextStep!] ?? steps[current]
135
+ const node = getNode(step)
136
+ const { w, h } = getWindow()
137
+ if (node) {
138
+ const nodeRect = getNodeRect(node as HTMLElement)
139
+ if (!inView({ ...nodeRect, w, h })) {
140
+ const parentScroll = scrollParent(node)
141
+ const offset = nodeRect.height > h ? -25 : -(h / 2) + nodeRect.height / 2
142
+ scrollSmooth(node as HTMLElement, {
143
+ context: isBody(parentScroll as HTMLElement)
144
+ ? window
145
+ : (parentScroll as HTMLElement),
146
+ duration: scrollDuration,
147
+ offset,
148
+ callback(target: ReactText | HTMLElement) {
149
+ makeCalculation(getNodeRect(target as HTMLElement), step.position)
150
+ },
151
+ })
152
+ } else {
153
+ makeCalculation(nodeRect, step.position)
154
+ }
155
+ } else {
156
+ dispatch({
157
+ type: 'NO_DOM_NODE',
158
+ helperPosition: step.position as ReducerState['helperPosition'],
159
+ w,
160
+ h,
161
+ inDOM: false,
162
+ })
163
+ }
164
+ }
165
+
166
+ function makeCalculation(
167
+ nodeRect: Partial<GetNodeRectOptions>,
168
+ helperPosition?: TourStepItem['position']
169
+ ) {
170
+ const { w, h } = getWindow()
171
+ const { width: helperWidth, height: helperHeight } = getNodeRect(
172
+ helper.current as HTMLDivElement
173
+ )
174
+ dispatch({
175
+ type: 'HAS_DOM_NODE',
176
+ ...nodeRect,
177
+ helperWidth,
178
+ helperHeight,
179
+ helperPosition: helperPosition as ReducerState['helperPosition'],
180
+ w,
181
+ h,
182
+ inDOM: true,
183
+ })
184
+ }
185
+
186
+ function getNode(step: TourStepItem) {
187
+ return step.selector ? document.querySelector(step.selector) : null
188
+ }
189
+
190
+ async function runCurrentActionAfter() {
191
+ const step = steps[current]
192
+ if (step.actionAfter && typeof step.actionAfter === 'function') {
193
+ await step.actionAfter(getNode(step))
194
+ }
195
+ }
196
+
197
+ async function close(event?: MouseEvent) {
198
+ document.body.style.overflowY = 'auto'
199
+ await runCurrentActionAfter()
200
+ if (onBeforeClose && typeof onBeforeClose === 'function') {
201
+ onBeforeClose(helper.current)
202
+ }
203
+ onRequestClose && onRequestClose(event)
204
+ }
205
+
206
+ const handleMaskClickHandler = (event: MouseEvent<HTMLDivElement>): void => {
207
+ if (closeWithMask) {
208
+ close(event)
209
+ }
210
+ }
211
+
212
+ async function goTo(index: number) {
213
+ await runCurrentActionAfter()
214
+ setCurrent(index)
215
+ }
216
+
217
+ const handleNextStep = (): void => {
218
+ goTo(current < steps?.length - 1 ? current + 1 : current)
219
+ }
220
+
221
+ return isOpen ? (
222
+ <Portal>
223
+ <SvgMask
224
+ onClick={handleMaskClickHandler}
225
+ windowWidth={state.w}
226
+ windowHeight={state.h}
227
+ targetWidth={state.width}
228
+ targetHeight={state.height}
229
+ targetTop={state.top}
230
+ targetLeft={state.left}
231
+ padding={maskSpace}
232
+ rounded={rounded}
233
+ className={maskClassName}
234
+ disableInteraction={disableInteraction || steps[current].stepInteraction}
235
+ disableInteractionClassName={disableInteractionClassName}
236
+ />
237
+ <Guide
238
+ ref={helper}
239
+ windowWidth={state.w}
240
+ windowHeight={state.h}
241
+ targetWidth={state.width}
242
+ targetHeight={state.height}
243
+ targetTop={state.top}
244
+ targetLeft={state.left}
245
+ targetRight={state.right}
246
+ targetBottom={state.bottom}
247
+ helperWidth={state.helperWidth!}
248
+ helperHeight={state.helperHeight!}
249
+ helperPosition={state.helperPosition}
250
+ padding={maskSpace}
251
+ tabIndex={-1}
252
+ current={current}
253
+ style={steps[current].style ?? {}}
254
+ rounded={rounded}
255
+ className={className}
256
+ >
257
+ <>
258
+ {children}
259
+ {steps[current].content}
260
+ {!disableButtons && (
261
+ <Action
262
+ isLastItem={current === steps?.length - 1}
263
+ onNextStep={handleNextStep}
264
+ onClose={(event) => close(event)}
265
+ />
266
+ )}
267
+ </>
268
+ </Guide>
269
+ </Portal>
270
+ ) : null
271
+ }
272
+ )
273
+
274
+ Tour.displayName = 'Tour'
275
+
276
+ export default memo(Tour)
package/src/types.ts CHANGED
@@ -1,52 +1,52 @@
1
- import { CSSProperties, MouseEvent, ReactNode } from 'react'
2
-
3
- interface TourStepItem {
4
- selector?: string
5
- content: ReactNode
6
- observe?: string
7
- position?: number[] | 'top' | 'right' | 'bottom' | 'left' | 'center'
8
- actionAfter?(element: Element | null): void | Promise<void>
9
- actionBefore?(): void | Promise<void>
10
- style?: CSSProperties
11
- stepInteraction?: boolean
12
- }
13
-
14
- interface TourProps {
15
- className?: string
16
- isOpen: boolean
17
-
18
- /* 生命周期 */
19
- /* 打开之后的回调 */
20
- onAfterOpen?(dom: HTMLDivElement): void
21
- /* 关闭之前的回调 */
22
- onBeforeClose?(dom: HTMLDivElement | null): void
23
- /* 请求之后关闭的回调 */
24
- onRequestClose?(event?: MouseEvent): void
25
-
26
- /* 延迟 */
27
- scrollDuration?: number
28
- /* 开始 */
29
- startAt?: number
30
- /* 引导与内容的间隙 */
31
- maskSpace?: number
32
- maskClassName?: string
33
- /* 蒙层是否触发关闭 */
34
- closeWithMask?: boolean
35
- /* 步骤设置 */
36
- steps?: TourStepItem[]
37
- /* 禁用按钮 */
38
- disableButtons?: boolean
39
- /* 禁用互动 */
40
- disableInteraction?: boolean
41
- /* 禁用互动样式 */
42
- disableInteractionClassName?: string
43
- rounded?: number
44
- }
45
-
46
- interface TourRefOptions {
47
- apiToNextStep(): void
48
- apiClose(): void
49
- apiRecalculate(): void
50
- }
51
-
52
- export type { TourProps, TourStepItem, TourRefOptions }
1
+ import { CSSProperties, MouseEvent, ReactNode } from 'react'
2
+
3
+ interface TourStepItem {
4
+ selector?: string
5
+ content: ReactNode
6
+ observe?: string
7
+ position?: number[] | 'top' | 'right' | 'bottom' | 'left' | 'center'
8
+ actionAfter?(element: Element | null): void | Promise<void>
9
+ actionBefore?(): void | Promise<void>
10
+ style?: CSSProperties
11
+ stepInteraction?: boolean
12
+ }
13
+
14
+ interface TourProps {
15
+ className?: string
16
+ isOpen: boolean
17
+
18
+ /* 生命周期 */
19
+ /* 打开之后的回调 */
20
+ onAfterOpen?(dom: HTMLDivElement): void
21
+ /* 关闭之前的回调 */
22
+ onBeforeClose?(dom: HTMLDivElement | null): void
23
+ /* 请求之后关闭的回调 */
24
+ onRequestClose?(event?: MouseEvent): void
25
+
26
+ /* 延迟 */
27
+ scrollDuration?: number
28
+ /* 开始 */
29
+ startAt?: number
30
+ /* 引导与内容的间隙 */
31
+ maskSpace?: number
32
+ maskClassName?: string
33
+ /* 蒙层是否触发关闭 */
34
+ closeWithMask?: boolean
35
+ /* 步骤设置 */
36
+ steps?: TourStepItem[]
37
+ /* 禁用按钮 */
38
+ disableButtons?: boolean
39
+ /* 禁用互动 */
40
+ disableInteraction?: boolean
41
+ /* 禁用互动样式 */
42
+ disableInteractionClassName?: string
43
+ rounded?: number
44
+ }
45
+
46
+ interface TourRefOptions {
47
+ apiToNextStep(): void
48
+ apiClose(): void
49
+ apiRecalculate(): void
50
+ }
51
+
52
+ export type { TourProps, TourStepItem, TourRefOptions }