-
-
Notifications
You must be signed in to change notification settings - Fork 659
Expand file tree
/
Copy pathshepherd-modal.svelte
More file actions
243 lines (208 loc) · 6.47 KB
/
Copy pathshepherd-modal.svelte
File metadata and controls
243 lines (208 loc) · 6.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
<script>
import { uuid, parseElementOrSelector } from '../utils/general.js';
import { makeOverlayPath } from '../utils/overlay-path.js';
export let element, openingProperties;
const guid = uuid();
let modalIsVisible = false;
let rafId = undefined;
let pathDefinition;
$: pathDefinition = makeOverlayPath(openingProperties);
closeModalOpening();
export const getElement = () => element;
export const getOpeningProperties = () => openingProperties;
export function closeModalOpening() {
openingProperties = {
width: 0,
height: 0,
x: 0,
y: 0,
r: 0
};
}
/**
* Hide the modal overlay
*/
export function hide() {
modalIsVisible = false;
// Ensure we cleanup all event listeners when we hide the modal
_cleanupStepEventListeners();
}
/**
* Uses the bounds of the element we want the opening overtop of to set the dimensions of the opening and position it
* @param {Number} modalOverlayOpeningPadding An amount of padding to add around the modal overlay opening
* @param {Number} modalOverlayOpeningRadius An amount of border radius to add around the modal overlay opening
* @param {HTMLElement} scrollParent The scrollable parent of the target element
* @param {HTMLElement} targetElement The element the opening will expose
*/
export function positionModal(
modalOverlayOpeningPadding = 0,
modalOverlayOpeningRadius = 0,
scrollParent,
targetElement
) {
if (targetElement) {
const { y, height } = _getVisibleHeight(targetElement, scrollParent);
const { x, width, left } = targetElement.getBoundingClientRect();
// getBoundingClientRect is not consistent. Some browsers use x and y, while others use left and top
openingProperties = {
width: width + modalOverlayOpeningPadding * 2,
height: height + modalOverlayOpeningPadding * 2,
x: (x || left) - modalOverlayOpeningPadding,
y: y - modalOverlayOpeningPadding,
r: modalOverlayOpeningRadius
};
} else {
closeModalOpening();
}
}
/**
* If modal is enabled, setup the svg mask opening and modal overlay for the step
* @param {Step} step The step instance
*/
export function setupForStep(step) {
// Ensure we move listeners from the previous step, before we setup new ones
_cleanupStepEventListeners();
if (step.tour.options.useModalOverlay) {
_styleForStep(step);
show();
} else {
hide();
}
}
/**
* Show the modal overlay
*/
export function show() {
modalIsVisible = true;
}
const _preventModalBodyTouch = (e) => {
e.preventDefault();
};
const _preventModalOverlayTouch = (e) => {
e.stopPropagation();
};
/**
* Add touchmove event listener
* @private
*/
function _addStepEventListeners() {
// Prevents window from moving on touch.
window.addEventListener('touchmove', _preventModalBodyTouch, {
passive: false
});
}
/**
* Cancel the requestAnimationFrame loop and remove touchmove event listeners
* @private
*/
function _cleanupStepEventListeners() {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = undefined;
}
window.removeEventListener('touchmove', _preventModalBodyTouch, {
passive: false
});
}
/**
* Style the modal for the step
* @param {Step} step The step to style the opening for
* @private
*/
function _styleForStep(step) {
const {
modalOverlayOpeningElement,
modalOverlayOpeningPadding,
modalOverlayOpeningRadius
} = step.options;
const target = parseElementOrSelector(modalOverlayOpeningElement) || step.target;
const scrollParent = _getScrollParent(target);
// Setup recursive function to call requestAnimationFrame to update the modal opening position
const rafLoop = () => {
rafId = undefined;
positionModal(
modalOverlayOpeningPadding,
modalOverlayOpeningRadius,
scrollParent,
target
);
rafId = requestAnimationFrame(rafLoop);
};
rafLoop();
_addStepEventListeners();
}
/**
* Find the closest scrollable parent element
* @param {HTMLElement} element The target element
* @returns {HTMLElement}
* @private
*/
function _getScrollParent(element) {
if (!element) {
return null;
}
const isHtmlElement = element instanceof HTMLElement;
const overflowY =
isHtmlElement && window.getComputedStyle(element).overflowY;
const isScrollable = overflowY !== 'hidden' && overflowY !== 'visible';
if (isScrollable && element.scrollHeight >= element.clientHeight) {
return element;
}
return _getScrollParent(element.parentElement);
}
/**
* Get the visible height of the target element relative to its scrollParent.
* If there is no scroll parent, the height of the element is returned.
*
* @param {HTMLElement} element The target element
* @param {HTMLElement} [scrollParent] The scrollable parent element
* @returns {{y: number, height: number}}
* @private
*/
function _getVisibleHeight(element, scrollParent) {
const elementRect = element.getBoundingClientRect();
let top = elementRect.y || elementRect.top;
let bottom = elementRect.bottom || top + elementRect.height;
if (scrollParent) {
const scrollRect = scrollParent.getBoundingClientRect();
const scrollTop = scrollRect.y || scrollRect.top;
const scrollBottom = scrollRect.bottom || scrollTop + scrollRect.height;
top = Math.max(top, scrollTop);
bottom = Math.min(bottom, scrollBottom);
}
const height = Math.max(bottom - top, 0); // Default to 0 if height is negative
return { y: top, height };
}
</script>
<svg
bind:this={element}
class={`${
modalIsVisible ? 'shepherd-modal-is-visible' : ''
} shepherd-modal-overlay-container`}
on:touchmove={_preventModalOverlayTouch}
>
<path d={pathDefinition} />
</svg>
<style global>
.shepherd-modal-overlay-container {
height: 0;
left: 0;
opacity: 0;
overflow: hidden;
pointer-events: none;
position: fixed;
top: 0;
transition: all 0.3s ease-out, height 0ms 0.3s, opacity 0.3s 0ms;
width: 100vw;
z-index: 9997;
}
.shepherd-modal-overlay-container.shepherd-modal-is-visible {
height: 100vh;
opacity: 0.5;
transition: all 0.3s ease-out, height 0s 0s, opacity 0.3s 0s;
transform: translateZ(0);
}
.shepherd-modal-overlay-container.shepherd-modal-is-visible path {
pointer-events: all;
}
</style>