Passed
Push — trunk ( 54c4dd...cf5ce9 )
by Christian
15:20 queued 20s
created

popover.directive.ts ➔ calculateOutsideEdges   B

Complexity

Conditions 8

Size

Total Lines 36
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 23
dl 0
loc 36
rs 7.3333
c 0
b 0
f 0
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