|
1
|
|
|
/* eslint-disable */ |
|
2
|
|
|
|
|
3
|
|
|
import Vue, {VNode} from "vue"; |
|
4
|
|
|
|
|
5
|
|
|
const { Directive } = Shopware; |
|
6
|
|
|
|
|
7
|
|
|
/** |
|
8
|
|
|
* @package admin |
|
9
|
|
|
* |
|
10
|
|
|
* Directive for automatic edge detection of the element place |
|
11
|
|
|
* |
|
12
|
|
|
* Usage: |
|
13
|
|
|
* v-popover="{ active: true, targetSelector: '.my-element', resizeWidth: true }" |
|
14
|
|
|
*/ |
|
15
|
|
|
|
|
16
|
|
|
// add virtual scrolling |
|
17
|
|
|
const virtualScrollingElements = new Map(); |
|
18
|
|
|
|
|
19
|
|
|
// set class name for each border |
|
20
|
|
|
const outsideClasses = { |
|
21
|
|
|
top: '--placement-top-outside', |
|
22
|
|
|
right: '--placement-right-outside', |
|
23
|
|
|
bottom: '--placement-bottom-outside', |
|
24
|
|
|
left: '--placement-left-outside' |
|
25
|
|
|
}; |
|
26
|
|
|
|
|
27
|
|
|
interface PopoverConfig { |
|
28
|
|
|
active: boolean; |
|
29
|
|
|
targetSelector: string; |
|
30
|
|
|
resizeWidth: boolean; |
|
31
|
|
|
style: Record<string, string>; |
|
32
|
|
|
} |
|
33
|
|
|
|
|
34
|
|
|
const defaultConfig: PopoverConfig = { |
|
35
|
|
|
active: false, |
|
36
|
|
|
targetSelector: '', |
|
37
|
|
|
resizeWidth: false, |
|
38
|
|
|
style: {} |
|
39
|
|
|
}; |
|
40
|
|
|
|
|
41
|
|
|
const customStylingBlacklist = ['width', 'position', 'top', 'left', 'right', 'bottom']; |
|
42
|
|
|
|
|
43
|
|
|
Directive.register('popover', { |
|
44
|
|
|
inserted(element, binding, vnode) { |
|
45
|
|
|
// We need a configuration |
|
46
|
|
|
if (!binding.value) { |
|
47
|
|
|
return false; |
|
48
|
|
|
} |
|
49
|
|
|
|
|
50
|
|
|
// Merge user config with default config |
|
51
|
|
|
const config = { ...defaultConfig, ...binding.value }; |
|
52
|
|
|
if (!config.active) { |
|
53
|
|
|
return false; |
|
54
|
|
|
} |
|
55
|
|
|
|
|
56
|
|
|
// Configurable target element |
|
57
|
|
|
let targetElement = document.body; |
|
58
|
|
|
if (config.targetSelector && config.targetSelector.length > 0) { |
|
59
|
|
|
targetElement = element.closest(config.targetSelector); |
|
60
|
|
|
} |
|
61
|
|
|
|
|
62
|
|
|
targetElement.appendChild(element); |
|
63
|
|
|
setElementPosition(element, vnode.context?.$el, config); |
|
64
|
|
|
|
|
65
|
|
|
// Resize the width of the element |
|
66
|
|
|
if (config.resizeWidth) { |
|
67
|
|
|
element.style.width = `${vnode.context?.$el.clientWidth}px`; |
|
68
|
|
|
} |
|
69
|
|
|
|
|
70
|
|
|
// append to target element |
|
71
|
|
|
calculateOutsideEdges(element, vnode); |
|
72
|
|
|
|
|
73
|
|
|
registerVirtualScrollingElement(element, vnode.context, config); |
|
74
|
|
|
}, |
|
75
|
|
|
|
|
76
|
|
|
unbind(element, binding, vnode) { |
|
77
|
|
|
// remove element from body |
|
78
|
|
|
if (element.parentNode) { |
|
79
|
|
|
element.parentNode.removeChild(element); |
|
80
|
|
|
} |
|
81
|
|
|
|
|
82
|
|
|
// @ts-expect-error - _uid exists on the context but is private |
|
83
|
|
|
unregisterVirtualScrollingElement(vnode.context?._uid); |
|
84
|
|
|
} |
|
85
|
|
|
}); |
|
86
|
|
|
|
|
87
|
|
|
/** |
|
88
|
|
|
* Helper functions |
|
89
|
|
|
* |
|
90
|
|
|
* Usage: |
|
91
|
|
|
* v-placement |
|
92
|
|
|
*/ |
|
93
|
|
|
|
|
94
|
|
|
function calculateOutsideEdges(el: HTMLElement, vnode: VNode) { |
|
95
|
|
|
// orientation element is needed for calculating the available space |
|
96
|
|
|
const orientationElement = vnode?.context?.$parent?.$el ?? el; |
|
97
|
|
|
|
|
98
|
|
|
// get position |
|
99
|
|
|
const boundingClientRect = orientationElement.getBoundingClientRect(); |
|
100
|
|
|
const windowHeight = |
|
101
|
|
|
window.innerHeight || document.documentElement.clientHeight; |
|
102
|
|
|
const windowWidth = window.innerWidth || document.documentElement.clientWidth; |
|
103
|
|
|
|
|
104
|
|
|
// calculate which edges are in viewport |
|
105
|
|
|
const visibleEdges = { |
|
106
|
|
|
topSpace: boundingClientRect.top, |
|
107
|
|
|
rightSpace: windowWidth - boundingClientRect.right, |
|
108
|
|
|
bottomSpace: windowHeight - boundingClientRect.bottom, |
|
109
|
|
|
leftSpace: boundingClientRect.left |
|
110
|
|
|
}; |
|
111
|
|
|
|
|
112
|
|
|
// remove all existing placement classes |
|
113
|
|
|
el.classList.remove(...Object.values(outsideClasses)); |
|
114
|
|
|
|
|
115
|
|
|
// get new classes for placement |
|
116
|
|
|
const placementClasses = [ |
|
117
|
|
|
visibleEdges.bottomSpace < visibleEdges.topSpace ? outsideClasses.bottom : outsideClasses.top, |
|
118
|
|
|
visibleEdges.rightSpace > visibleEdges.leftSpace ? outsideClasses.left : outsideClasses.right |
|
119
|
|
|
] |
|
120
|
|
|
// add new classes to element |
|
121
|
|
|
el.classList.add(...placementClasses); |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
function setElementPosition(element: HTMLElement, refElement: Element|undefined, config: PopoverConfig) { |
|
125
|
|
|
const originElement = refElement ? refElement : element; |
|
126
|
|
|
const elementPosition = originElement.getBoundingClientRect(); |
|
127
|
|
|
|
|
128
|
|
|
let targetElement = originElement; |
|
129
|
|
|
let targetPosition = { |
|
130
|
|
|
top: 0, |
|
131
|
|
|
left: 0 |
|
132
|
|
|
}; |
|
133
|
|
|
|
|
134
|
|
|
if (config.targetSelector && config.targetSelector.length > 0) { |
|
135
|
|
|
targetElement = originElement.closest(config.targetSelector)!; |
|
136
|
|
|
targetPosition = targetElement.getBoundingClientRect(); |
|
137
|
|
|
} |
|
138
|
|
|
|
|
139
|
|
|
// set custom inline element styling |
|
140
|
|
|
Object.entries(config.style).forEach(([key, value]) => { |
|
141
|
|
|
if (customStylingBlacklist.includes(key)) { |
|
142
|
|
|
return; |
|
143
|
|
|
} |
|
144
|
|
|
|
|
145
|
|
|
// @ts-expect-error - key can be set |
|
146
|
|
|
element.style[key] = value; |
|
147
|
|
|
}); |
|
148
|
|
|
|
|
149
|
|
|
// add inline styling |
|
150
|
|
|
element.style.position = 'absolute'; |
|
151
|
|
|
element.style.top = `${(elementPosition.top - targetPosition.top) + originElement.clientHeight}px`; |
|
152
|
|
|
element.style.left = `${elementPosition.left - targetPosition.left}px`; |
|
153
|
|
|
} |
|
154
|
|
|
|
|
155
|
|
|
/* |
|
156
|
|
|
* Virtual Scrolling |
|
157
|
|
|
*/ |
|
158
|
|
|
|
|
159
|
|
|
function startVirtualScrolling() { |
|
160
|
|
|
window.addEventListener('scroll', virtualScrollingHandler, true); |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
function stopVirtualScrolling() { |
|
164
|
|
|
window.removeEventListener('scroll', virtualScrollingHandler, true); |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
function virtualScrollingHandler() { |
|
168
|
|
|
if (virtualScrollingElements.size <= 0) { |
|
169
|
|
|
stopVirtualScrolling(); |
|
170
|
|
|
return; |
|
171
|
|
|
} |
|
172
|
|
|
|
|
173
|
|
|
virtualScrollingElements.forEach((entry) => { |
|
174
|
|
|
setElementPosition(entry.el, entry.ref, entry.config); |
|
175
|
|
|
}); |
|
176
|
|
|
} |
|
177
|
|
|
|
|
178
|
|
|
function registerVirtualScrollingElement(modifiedElement: HTMLElement, vnodeContext: Vue|undefined, config: PopoverConfig) { |
|
179
|
|
|
// @ts-expect-error - _uid exists on the context but is private |
|
180
|
|
|
const uid = vnodeContext?._uid; |
|
181
|
|
|
|
|
182
|
|
|
if (!uid) { |
|
183
|
|
|
return; |
|
184
|
|
|
} |
|
185
|
|
|
|
|
186
|
|
|
if (virtualScrollingElements.size <= 0) { |
|
187
|
|
|
startVirtualScrolling(); |
|
188
|
|
|
} |
|
189
|
|
|
|
|
190
|
|
|
virtualScrollingElements.set(uid, { |
|
191
|
|
|
el: modifiedElement, |
|
192
|
|
|
ref: vnodeContext.$el, |
|
193
|
|
|
config |
|
194
|
|
|
}); |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
function unregisterVirtualScrollingElement(uid?: string) { |
|
198
|
|
|
if (!uid) { |
|
199
|
|
|
return; |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
virtualScrollingElements.delete(uid); |
|
203
|
|
|
|
|
204
|
|
|
if (virtualScrollingElements.size <= 0) { |
|
205
|
|
|
stopVirtualScrolling(); |
|
206
|
|
|
} |
|
207
|
|
|
} |
|
208
|
|
|
|
|
209
|
|
|
/** |
|
210
|
|
|
* @deprecated tag:v6.6.0 - Will be private |
|
211
|
|
|
*/ |
|
212
|
|
|
export default { |
|
213
|
|
|
virtualScrollingElements, |
|
214
|
|
|
registerVirtualScrollingElement, |
|
215
|
|
|
unregisterVirtualScrollingElement |
|
216
|
|
|
}; |
|
217
|
|
|
|