@fiscozen/composables 0.1.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/.eslintignore +1 -0
- package/.eslintrc.cjs +18 -0
- package/.prettierrc.js +6 -0
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/composables.js +212 -0
- package/dist/composables.umd.cjs +1 -0
- package/package.json +40 -0
- package/src/FzFloating.vue +58 -0
- package/src/__tests__/FzFloating.spec.ts +48 -0
- package/src/__tests__/__snapshots__/FzFloating.spec.ts.snap +109 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/useFloating.ts +260 -0
- package/src/index.ts +4 -0
- package/src/types.ts +31 -0
- package/src/utils/index.ts +76 -0
- package/tsconfig.json +3 -0
- package/vite.config.ts +31 -0
- package/vitest.config.ts +19 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dist
|
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
require('@rushstack/eslint-patch/modern-module-resolution')
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
root: true,
|
|
6
|
+
'extends': ['@fiscozen/eslint-config', 'plugin:storybook/recommended'],
|
|
7
|
+
overrides: [
|
|
8
|
+
{
|
|
9
|
+
files: [
|
|
10
|
+
'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}',
|
|
11
|
+
'cypress/support/**/*.{js,ts,jsx,tsx}'
|
|
12
|
+
],
|
|
13
|
+
'extends': [
|
|
14
|
+
'plugin:cypress/recommended'
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/.prettierrc.js
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Fiscozen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { ref as d, reactive as w, onUnmounted as O, nextTick as B, defineComponent as $, useSlots as C, computed as P, toRef as S, watch as y, openBlock as z, createElementBlock as F, createElementVNode as k, renderSlot as R, unref as x, withDirectives as N, vShow as E } from "vue";
|
|
2
|
+
const g = (s, n, l, p) => {
|
|
3
|
+
let i = "right", e = "right-start";
|
|
4
|
+
const r = s.getBoundingClientRect(), c = n.getBoundingClientRect(), h = l.getBoundingClientRect(), m = (h.left - r.left - c.width) / r.width, t = (r.right - h.right - c.width) / r.width, o = (h.top - r.top - c.height) / r.height, f = (r.bottom - h.bottom - c.height) / r.height;
|
|
5
|
+
if (i = [
|
|
6
|
+
{
|
|
7
|
+
key: "right",
|
|
8
|
+
space: t
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
key: "top",
|
|
12
|
+
space: o
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
key: "bottom",
|
|
16
|
+
space: f
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: "left",
|
|
20
|
+
space: m
|
|
21
|
+
}
|
|
22
|
+
].sort((a, v) => v.space - a.space)[0].key, !p)
|
|
23
|
+
switch (i) {
|
|
24
|
+
case "right":
|
|
25
|
+
case "left":
|
|
26
|
+
const a = (c.height - h.height) / 2;
|
|
27
|
+
r.top > h.top - a && (p = "end"), r.bottom < h.top - a && (p = "start");
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
return e = p ? `${i}-${p}` : i, e;
|
|
31
|
+
}, q = (s, n, l, p) => ({
|
|
32
|
+
x: n.x + s.width * l / 100,
|
|
33
|
+
y: n.y + s.height * p / 100
|
|
34
|
+
}), D = (s) => {
|
|
35
|
+
const n = d(null), l = d(null), p = d(null), i = d(null), e = w({
|
|
36
|
+
position: { x: 0, y: 0 }
|
|
37
|
+
}), r = {
|
|
38
|
+
root: null,
|
|
39
|
+
rootMargin: "0px",
|
|
40
|
+
threshold: 1,
|
|
41
|
+
...s.element.intersectionOptions
|
|
42
|
+
}, c = (t, o) => {
|
|
43
|
+
}, h = d(new IntersectionObserver(c, r)), m = () => B(() => {
|
|
44
|
+
var b;
|
|
45
|
+
if (n.value = typeof s.element.domRef.value == "string" ? document.querySelector(s.element.domRef.value) : s.element.domRef.value, s.container ? (l.value = typeof s.container.domRef.value == "string" ? document.querySelector(s.container.domRef.value) : s.container.domRef.value, l.value ?? (l.value = document.body)) : l.value = document.body, !n.value)
|
|
46
|
+
throw new Error("missing reference element for floating behavior");
|
|
47
|
+
s.opener && (p.value = typeof s.opener.domRef.value == "string" ? document.querySelector(s.opener.domRef.value) : s.opener.domRef.value), i.value = n.value.getBoundingClientRect();
|
|
48
|
+
const t = (b = p.value) == null ? void 0 : b.getBoundingClientRect(), o = l.value.getBoundingClientRect();
|
|
49
|
+
h.value.observe(n.value), h.value.observe(l.value);
|
|
50
|
+
let f = s.position ? s.position.value : "auto", u = 0, a = 0;
|
|
51
|
+
if (s.opener && p.value && t) {
|
|
52
|
+
switch (f) {
|
|
53
|
+
case "auto":
|
|
54
|
+
f = g(
|
|
55
|
+
l.value,
|
|
56
|
+
n.value,
|
|
57
|
+
p.value
|
|
58
|
+
);
|
|
59
|
+
break;
|
|
60
|
+
case "auto-start":
|
|
61
|
+
f = g(
|
|
62
|
+
l.value,
|
|
63
|
+
n.value,
|
|
64
|
+
p.value,
|
|
65
|
+
"start"
|
|
66
|
+
);
|
|
67
|
+
break;
|
|
68
|
+
case "auto-end":
|
|
69
|
+
f = g(
|
|
70
|
+
l.value,
|
|
71
|
+
n.value,
|
|
72
|
+
p.value,
|
|
73
|
+
"end"
|
|
74
|
+
);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
switch (f) {
|
|
78
|
+
case "bottom":
|
|
79
|
+
e.position.y = t.bottom, e.position.x = t.left + t.width / 2, a = -50, u = 0;
|
|
80
|
+
break;
|
|
81
|
+
case "bottom-start":
|
|
82
|
+
e.position.y = t.bottom, e.position.x = t.left, a = 0, u = 0;
|
|
83
|
+
break;
|
|
84
|
+
case "bottom-end":
|
|
85
|
+
e.position.y = t.bottom, e.position.x = t.right, a = -100, u = 0;
|
|
86
|
+
break;
|
|
87
|
+
case "left-start":
|
|
88
|
+
e.position.y = t.top, e.position.x = t.left, a = -100, u = 0;
|
|
89
|
+
break;
|
|
90
|
+
case "left":
|
|
91
|
+
e.position.y = t.top + t.height / 2, e.position.x = t.left, u = -50, a = -100;
|
|
92
|
+
break;
|
|
93
|
+
case "left-end":
|
|
94
|
+
e.position.y = t.bottom, e.position.x = t.left, u = -100, a = -100;
|
|
95
|
+
break;
|
|
96
|
+
case "top-start":
|
|
97
|
+
e.position.y = t.top, e.position.x = t.left, u = -100, a = 0;
|
|
98
|
+
break;
|
|
99
|
+
case "top":
|
|
100
|
+
e.position.y = t.top, e.position.x = t.left + t.width / 2, a = -50, u = -100;
|
|
101
|
+
break;
|
|
102
|
+
case "top-end":
|
|
103
|
+
e.position.y = t.top, e.position.x = t.right, a = -100, u = -100;
|
|
104
|
+
break;
|
|
105
|
+
case "right-start":
|
|
106
|
+
e.position.y = t.top, e.position.x = t.right, a = 0, u = 0;
|
|
107
|
+
break;
|
|
108
|
+
case "right":
|
|
109
|
+
e.position.y = t.top + t.height / 2, e.position.x = t.right, u = -50, a = 0;
|
|
110
|
+
break;
|
|
111
|
+
case "right-end":
|
|
112
|
+
e.position.y = t.bottom, e.position.x = t.right, u = -100, a = 0;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
} else
|
|
116
|
+
switch (f) {
|
|
117
|
+
case "bottom":
|
|
118
|
+
e.position.y = o.bottom - i.value.height, e.position.x = o.left + o.width / 2, a = -50;
|
|
119
|
+
break;
|
|
120
|
+
case "left-end":
|
|
121
|
+
case "bottom-start":
|
|
122
|
+
e.position.y = o.bottom - i.value.height, e.position.x = o.left;
|
|
123
|
+
break;
|
|
124
|
+
case "right-end":
|
|
125
|
+
case "bottom-end":
|
|
126
|
+
e.position.y = o.bottom - i.value.height, e.position.x = o.right - i.value.width;
|
|
127
|
+
break;
|
|
128
|
+
case "left":
|
|
129
|
+
e.position.y = o.top + (o.height - i.value.height) / 2, e.position.x = o.left;
|
|
130
|
+
break;
|
|
131
|
+
case "top-start":
|
|
132
|
+
case "left-start":
|
|
133
|
+
e.position.y = o.top, e.position.x = o.left;
|
|
134
|
+
break;
|
|
135
|
+
case "top":
|
|
136
|
+
e.position.y = o.top, e.position.x = o.left + (o.width - i.value.width) / 2;
|
|
137
|
+
break;
|
|
138
|
+
case "top-end":
|
|
139
|
+
case "right-start":
|
|
140
|
+
e.position.y = o.top, e.position.x = o.right - i.value.width;
|
|
141
|
+
case "right":
|
|
142
|
+
e.position.y = o.top + (o.height - i.value.height) / 2, e.position.x = o.right - i.value.width;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
const v = q(i.value, e.position, a, u);
|
|
146
|
+
v.x < o.left && (e.position.x = o.left, a = 0), v.x + i.value.width > o.right && (e.position.x = o.right - i.value.width, a = 0), v.y < o.top && (e.position.y = o.top, u = 0), v.y + i.value.height > o.bottom && (e.position.y = o.bottom - i.value.height, u = 0), n.value.style.top = `${e.position.y}px`, n.value.style.left = `${e.position.x}px`, n.value.style.transform = `translateY(${u}%) translateX(${a}%)`, n.value.style.position = "absolute", i.value = n.value.getBoundingClientRect();
|
|
147
|
+
});
|
|
148
|
+
return O(() => {
|
|
149
|
+
h.value.disconnect();
|
|
150
|
+
}), {
|
|
151
|
+
float: e,
|
|
152
|
+
rect: i,
|
|
153
|
+
floatObserver: h,
|
|
154
|
+
setPosition: m
|
|
155
|
+
};
|
|
156
|
+
}, I = /* @__PURE__ */ $({
|
|
157
|
+
__name: "FzFloating",
|
|
158
|
+
props: {
|
|
159
|
+
isOpen: { type: Boolean, default: !1 },
|
|
160
|
+
position: { default: "auto" },
|
|
161
|
+
container: {}
|
|
162
|
+
},
|
|
163
|
+
setup(s) {
|
|
164
|
+
const n = s, l = d(null), p = d(null), i = C(), e = {
|
|
165
|
+
position: P(() => n.position),
|
|
166
|
+
element: {
|
|
167
|
+
domRef: p
|
|
168
|
+
},
|
|
169
|
+
container: {
|
|
170
|
+
domRef: S(n.container || document.body)
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
i.opener && (e.opener = {
|
|
174
|
+
domRef: l
|
|
175
|
+
});
|
|
176
|
+
const r = D(e);
|
|
177
|
+
return y(
|
|
178
|
+
() => n.position,
|
|
179
|
+
() => n.isOpen && r.setPosition()
|
|
180
|
+
), y(
|
|
181
|
+
() => n.isOpen,
|
|
182
|
+
(c) => c && r.setPosition()
|
|
183
|
+
), (c, h) => (z(), F("div", null, [
|
|
184
|
+
k("div", {
|
|
185
|
+
ref_key: "opener",
|
|
186
|
+
ref: l,
|
|
187
|
+
class: "inline-flex"
|
|
188
|
+
}, [
|
|
189
|
+
R(c.$slots, "opener", {
|
|
190
|
+
isOpen: c.isOpen,
|
|
191
|
+
floating: x(r)
|
|
192
|
+
})
|
|
193
|
+
], 512),
|
|
194
|
+
N(k("div", {
|
|
195
|
+
ref_key: "content",
|
|
196
|
+
ref: p,
|
|
197
|
+
class: "bg-core-white absolute p-4"
|
|
198
|
+
}, [
|
|
199
|
+
R(c.$slots, "default", {
|
|
200
|
+
isOpen: c.isOpen,
|
|
201
|
+
floating: x(r)
|
|
202
|
+
})
|
|
203
|
+
], 512), [
|
|
204
|
+
[E, !c.$slots.opener || c.$slots.opener && c.isOpen]
|
|
205
|
+
])
|
|
206
|
+
]));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
export {
|
|
210
|
+
I as FzFloating,
|
|
211
|
+
D as useFloating
|
|
212
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(d,i){typeof exports=="object"&&typeof module<"u"?i(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],i):(d=typeof globalThis<"u"?globalThis:d||self,i(d.FzComposables={},d.Vue))})(this,function(d,i){"use strict";const g=(a,s,r,p)=>{let n="right",e="right-start";const c=a.getBoundingClientRect(),f=s.getBoundingClientRect(),u=r.getBoundingClientRect(),y=(u.left-c.left-f.width)/c.width,t=(c.right-u.right-f.width)/c.width,o=(u.top-c.top-f.height)/c.height,m=(c.bottom-u.bottom-f.height)/c.height;if(n=[{key:"right",space:t},{key:"top",space:o},{key:"bottom",space:m},{key:"left",space:y}].sort((l,b)=>b.space-l.space)[0].key,!p)switch(n){case"right":case"left":const l=(f.height-u.height)/2;c.top>u.top-l&&(p="end"),c.bottom<u.top-l&&(p="start");break}return e=p?`${n}-${p}`:n,e},R=(a,s,r,p)=>({x:s.x+a.width*r/100,y:s.y+a.height*p/100}),v=a=>{const s=i.ref(null),r=i.ref(null),p=i.ref(null),n=i.ref(null),e=i.reactive({position:{x:0,y:0}}),c={root:null,rootMargin:"0px",threshold:1,...a.element.intersectionOptions},f=(t,o)=>{},u=i.ref(new IntersectionObserver(f,c)),y=()=>i.nextTick(()=>{var k;if(s.value=typeof a.element.domRef.value=="string"?document.querySelector(a.element.domRef.value):a.element.domRef.value,a.container?(r.value=typeof a.container.domRef.value=="string"?document.querySelector(a.container.domRef.value):a.container.domRef.value,r.value??(r.value=document.body)):r.value=document.body,!s.value)throw new Error("missing reference element for floating behavior");a.opener&&(p.value=typeof a.opener.domRef.value=="string"?document.querySelector(a.opener.domRef.value):a.opener.domRef.value),n.value=s.value.getBoundingClientRect();const t=(k=p.value)==null?void 0:k.getBoundingClientRect(),o=r.value.getBoundingClientRect();u.value.observe(s.value),u.value.observe(r.value);let m=a.position?a.position.value:"auto",h=0,l=0;if(a.opener&&p.value&&t){switch(m){case"auto":m=g(r.value,s.value,p.value);break;case"auto-start":m=g(r.value,s.value,p.value,"start");break;case"auto-end":m=g(r.value,s.value,p.value,"end");break}switch(m){case"bottom":e.position.y=t.bottom,e.position.x=t.left+t.width/2,l=-50,h=0;break;case"bottom-start":e.position.y=t.bottom,e.position.x=t.left,l=0,h=0;break;case"bottom-end":e.position.y=t.bottom,e.position.x=t.right,l=-100,h=0;break;case"left-start":e.position.y=t.top,e.position.x=t.left,l=-100,h=0;break;case"left":e.position.y=t.top+t.height/2,e.position.x=t.left,h=-50,l=-100;break;case"left-end":e.position.y=t.bottom,e.position.x=t.left,h=-100,l=-100;break;case"top-start":e.position.y=t.top,e.position.x=t.left,h=-100,l=0;break;case"top":e.position.y=t.top,e.position.x=t.left+t.width/2,l=-50,h=-100;break;case"top-end":e.position.y=t.top,e.position.x=t.right,l=-100,h=-100;break;case"right-start":e.position.y=t.top,e.position.x=t.right,l=0,h=0;break;case"right":e.position.y=t.top+t.height/2,e.position.x=t.right,h=-50,l=0;break;case"right-end":e.position.y=t.bottom,e.position.x=t.right,h=-100,l=0;break}}else switch(m){case"bottom":e.position.y=o.bottom-n.value.height,e.position.x=o.left+o.width/2,l=-50;break;case"left-end":case"bottom-start":e.position.y=o.bottom-n.value.height,e.position.x=o.left;break;case"right-end":case"bottom-end":e.position.y=o.bottom-n.value.height,e.position.x=o.right-n.value.width;break;case"left":e.position.y=o.top+(o.height-n.value.height)/2,e.position.x=o.left;break;case"top-start":case"left-start":e.position.y=o.top,e.position.x=o.left;break;case"top":e.position.y=o.top,e.position.x=o.left+(o.width-n.value.width)/2;break;case"top-end":case"right-start":e.position.y=o.top,e.position.x=o.right-n.value.width;case"right":e.position.y=o.top+(o.height-n.value.height)/2,e.position.x=o.right-n.value.width;break}const b=R(n.value,e.position,l,h);b.x<o.left&&(e.position.x=o.left,l=0),b.x+n.value.width>o.right&&(e.position.x=o.right-n.value.width,l=0),b.y<o.top&&(e.position.y=o.top,h=0),b.y+n.value.height>o.bottom&&(e.position.y=o.bottom-n.value.height,h=0),s.value.style.top=`${e.position.y}px`,s.value.style.left=`${e.position.x}px`,s.value.style.transform=`translateY(${h}%) translateX(${l}%)`,s.value.style.position="absolute",n.value=s.value.getBoundingClientRect()});return i.onUnmounted(()=>{u.value.disconnect()}),{float:e,rect:n,floatObserver:u,setPosition:y}},x=i.defineComponent({__name:"FzFloating",props:{isOpen:{type:Boolean,default:!1},position:{default:"auto"},container:{}},setup(a){const s=a,r=i.ref(null),p=i.ref(null),n=i.useSlots(),e={position:i.computed(()=>s.position),element:{domRef:p},container:{domRef:i.toRef(s.container||document.body)}};n.opener&&(e.opener={domRef:r});const c=v(e);return i.watch(()=>s.position,()=>s.isOpen&&c.setPosition()),i.watch(()=>s.isOpen,f=>f&&c.setPosition()),(f,u)=>(i.openBlock(),i.createElementBlock("div",null,[i.createElementVNode("div",{ref_key:"opener",ref:r,class:"inline-flex"},[i.renderSlot(f.$slots,"opener",{isOpen:f.isOpen,floating:i.unref(c)})],512),i.withDirectives(i.createElementVNode("div",{ref_key:"content",ref:p,class:"bg-core-white absolute p-4"},[i.renderSlot(f.$slots,"default",{isOpen:f.isOpen,floating:i.unref(c)})],512),[[i.vShow,!f.$slots.opener||f.$slots.opener&&f.isOpen]])]))}});d.FzFloating=x,d.useFloating=v,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fiscozen/composables",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Design System utility composables",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"author": "Riccardo Agnoletto",
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"tailwindcss": "^3.4.1",
|
|
11
|
+
"vue": "^3.4.13"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@rushstack/eslint-patch": "^1.3.3",
|
|
15
|
+
"@types/jsdom": "^21.1.6",
|
|
16
|
+
"@types/node": "^18.19.3",
|
|
17
|
+
"@vitejs/plugin-vue": "^4.5.2",
|
|
18
|
+
"@vitest/coverage-v8": "^1.2.1",
|
|
19
|
+
"@vue/test-utils": "^2.4.3",
|
|
20
|
+
"@vue/tsconfig": "^0.5.0",
|
|
21
|
+
"eslint": "^8.49.0",
|
|
22
|
+
"jsdom": "^23.0.1",
|
|
23
|
+
"prettier": "^3.0.3",
|
|
24
|
+
"typescript": "~5.3.0",
|
|
25
|
+
"vite": "^5.0.10",
|
|
26
|
+
"vitest": "^1.2.0",
|
|
27
|
+
"vue-tsc": "^1.8.25",
|
|
28
|
+
"@fiscozen/eslint-config": "^0.1.0",
|
|
29
|
+
"@fiscozen/prettier-config": "^0.1.0",
|
|
30
|
+
"@fiscozen/tsconfig": "^0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"license": "ISC",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"coverage": "vitest run --coverage",
|
|
35
|
+
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
|
|
36
|
+
"format": "prettier --write src/",
|
|
37
|
+
"test:unit": "vitest run",
|
|
38
|
+
"build": "vite build"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, useSlots, watch, toRef, computed } from 'vue'
|
|
3
|
+
import { useFloating } from './composables'
|
|
4
|
+
import { FzFloatingPosition, FzFloatingProps, FzUseFloatingArgs } from './types'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<FzFloatingProps>(), {
|
|
7
|
+
position: 'auto',
|
|
8
|
+
isOpen: false
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const opener = ref(null)
|
|
12
|
+
const content = ref(null)
|
|
13
|
+
|
|
14
|
+
const slots = useSlots()
|
|
15
|
+
|
|
16
|
+
const useFloatingOpts: FzUseFloatingArgs = {
|
|
17
|
+
position: computed(() => props.position),
|
|
18
|
+
element: {
|
|
19
|
+
domRef: content
|
|
20
|
+
},
|
|
21
|
+
container: {
|
|
22
|
+
domRef: toRef(props.container || document.body)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (slots.opener) {
|
|
26
|
+
useFloatingOpts.opener = {
|
|
27
|
+
domRef: opener
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const floating = useFloating(useFloatingOpts)
|
|
32
|
+
|
|
33
|
+
watch(
|
|
34
|
+
() => props.position,
|
|
35
|
+
() => props.isOpen && floating.setPosition()
|
|
36
|
+
)
|
|
37
|
+
watch(
|
|
38
|
+
() => props.isOpen,
|
|
39
|
+
(newVal) => newVal && floating.setPosition()
|
|
40
|
+
)
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<div>
|
|
45
|
+
<div ref="opener" class="inline-flex">
|
|
46
|
+
<slot name="opener" :isOpen :floating></slot>
|
|
47
|
+
</div>
|
|
48
|
+
<div
|
|
49
|
+
ref="content"
|
|
50
|
+
v-show="!$slots.opener || ($slots.opener && isOpen)"
|
|
51
|
+
class="bg-core-white absolute p-4"
|
|
52
|
+
>
|
|
53
|
+
<slot :isOpen :floating></slot>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<style></style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { mount } from '@vue/test-utils'
|
|
4
|
+
import FzFloating from '../FzFloating.vue'
|
|
5
|
+
import { FzFloatingPosition } from '../types'
|
|
6
|
+
|
|
7
|
+
const positions: FzFloatingPosition[] = [
|
|
8
|
+
'top',
|
|
9
|
+
'top-start',
|
|
10
|
+
'top-end',
|
|
11
|
+
'left',
|
|
12
|
+
'left-start',
|
|
13
|
+
'left-end',
|
|
14
|
+
'bottom',
|
|
15
|
+
'bottom-start',
|
|
16
|
+
'bottom-end',
|
|
17
|
+
'right',
|
|
18
|
+
'right-start',
|
|
19
|
+
'right-end'
|
|
20
|
+
]
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
const mockIntersectionObserver = vi.fn()
|
|
23
|
+
mockIntersectionObserver.mockReturnValue({
|
|
24
|
+
observe: () => null,
|
|
25
|
+
unobserve: () => null,
|
|
26
|
+
disconnect: () => null
|
|
27
|
+
})
|
|
28
|
+
window.IntersectionObserver = mockIntersectionObserver
|
|
29
|
+
})
|
|
30
|
+
describe('FzFloating', () => {
|
|
31
|
+
positions.forEach((pos) => {
|
|
32
|
+
it('should match snapshot', async () => {
|
|
33
|
+
const wrapper = mount(FzFloating, {
|
|
34
|
+
props: {
|
|
35
|
+
position: pos,
|
|
36
|
+
isOpen: true
|
|
37
|
+
},
|
|
38
|
+
slots: {
|
|
39
|
+
opener: '<button @click="params.floating.setPosition()">opener</button>',
|
|
40
|
+
default: '<div>content</div>'
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
wrapper.find('button').trigger('click')
|
|
45
|
+
expect(wrapper.html()).toMatchSnapshot()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`FzFloating > should match snapshot 1`] = `
|
|
4
|
+
"<div>
|
|
5
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
6
|
+
<div class="bg-core-white pb-4" style="top: 0px; left: 0px; transform: translateY(-100%) translateX(-50%); position: absolute;">
|
|
7
|
+
<div>content</div>
|
|
8
|
+
</div>
|
|
9
|
+
</div>"
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
exports[`FzFloating > should match snapshot 2`] = `
|
|
13
|
+
"<div>
|
|
14
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
15
|
+
<div class="bg-core-white pb-4" style="top: 0px; left: 0px; transform: translateY(-100%) translateX(0%); position: absolute;">
|
|
16
|
+
<div>content</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>"
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
exports[`FzFloating > should match snapshot 3`] = `
|
|
22
|
+
"<div>
|
|
23
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
24
|
+
<div class="bg-core-white pb-4" style="top: 0px; left: 0px; transform: translateY(-100%) translateX(-100%); position: absolute;">
|
|
25
|
+
<div>content</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>"
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
exports[`FzFloating > should match snapshot 4`] = `
|
|
31
|
+
"<div>
|
|
32
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
33
|
+
<div class="bg-core-white pr-4" style="top: 0px; left: 0px; transform: translateY(-50%) translateX(-100%); position: absolute;">
|
|
34
|
+
<div>content</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>"
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
exports[`FzFloating > should match snapshot 5`] = `
|
|
40
|
+
"<div>
|
|
41
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
42
|
+
<div class="bg-core-white pr-4" style="top: 0px; left: 0px; transform: translateY(0%) translateX(-100%); position: absolute;">
|
|
43
|
+
<div>content</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>"
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
exports[`FzFloating > should match snapshot 6`] = `
|
|
49
|
+
"<div>
|
|
50
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
51
|
+
<div class="bg-core-white pr-4" style="top: 0px; left: 0px; transform: translateY(-100%) translateX(-100%); position: absolute;">
|
|
52
|
+
<div>content</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>"
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
exports[`FzFloating > should match snapshot 7`] = `
|
|
58
|
+
"<div>
|
|
59
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
60
|
+
<div class="bg-core-white pt-4" style="top: 0px; left: 0px; transform: translateY(0%) translateX(-50%); position: absolute;">
|
|
61
|
+
<div>content</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>"
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
exports[`FzFloating > should match snapshot 8`] = `
|
|
67
|
+
"<div>
|
|
68
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
69
|
+
<div class="bg-core-white pt-4" style="top: 0px; left: 0px; transform: translateY(0%) translateX(0%); position: absolute;">
|
|
70
|
+
<div>content</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>"
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
exports[`FzFloating > should match snapshot 9`] = `
|
|
76
|
+
"<div>
|
|
77
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
78
|
+
<div class="bg-core-white pt-4" style="top: 0px; left: 0px; transform: translateY(0%) translateX(-100%); position: absolute;">
|
|
79
|
+
<div>content</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>"
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
exports[`FzFloating > should match snapshot 10`] = `
|
|
85
|
+
"<div>
|
|
86
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
87
|
+
<div class="bg-core-white pl-4" style="top: 0px; left: 0px; transform: translateY(-50%) translateX(0%); position: absolute;">
|
|
88
|
+
<div>content</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>"
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
exports[`FzFloating > should match snapshot 11`] = `
|
|
94
|
+
"<div>
|
|
95
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
96
|
+
<div class="bg-core-white pl-4" style="top: 0px; left: 0px; transform: translateY(0%) translateX(0%); position: absolute;">
|
|
97
|
+
<div>content</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>"
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
exports[`FzFloating > should match snapshot 12`] = `
|
|
103
|
+
"<div>
|
|
104
|
+
<div class="inline-flex"><button>opener</button></div>
|
|
105
|
+
<div class="bg-core-white pl-4" style="top: 0px; left: 0px; transform: translateY(-100%) translateX(0%); position: absolute;">
|
|
106
|
+
<div>content</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>"
|
|
109
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './useFloating'
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { FzFloatingPosition, FzRect, FzUseFloatingArgs } from '../types'
|
|
2
|
+
import { ref, reactive, onUnmounted, Ref, nextTick, computed } from 'vue'
|
|
3
|
+
import { calcRealPos, getHighestAvailableSpacePos } from '../utils'
|
|
4
|
+
|
|
5
|
+
export const useFloating = (
|
|
6
|
+
args: FzUseFloatingArgs
|
|
7
|
+
): {
|
|
8
|
+
float: FzRect;
|
|
9
|
+
rect: Ref<DOMRect | null>;
|
|
10
|
+
floatObserver: Ref<IntersectionObserver>;
|
|
11
|
+
setPosition: () => Promise<void>;
|
|
12
|
+
} => {
|
|
13
|
+
const safeElementDomRef = ref<HTMLElement | null>(null)
|
|
14
|
+
const safeContainerDomRef = ref<HTMLElement | null>(null)
|
|
15
|
+
const safeOpenerDomRef = ref<HTMLElement | null>(null)
|
|
16
|
+
const rect = ref<DOMRect | null>(null)
|
|
17
|
+
const float = reactive<FzRect>({
|
|
18
|
+
position: { x: 0, y: 0 }
|
|
19
|
+
})
|
|
20
|
+
const options: IntersectionObserverInit = {
|
|
21
|
+
root: null,
|
|
22
|
+
rootMargin: '0px',
|
|
23
|
+
threshold: 1.0,
|
|
24
|
+
...args.element.intersectionOptions
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const handleIntersect = (
|
|
28
|
+
entries: IntersectionObserverEntry[],
|
|
29
|
+
observer: IntersectionObserver
|
|
30
|
+
) => {}
|
|
31
|
+
|
|
32
|
+
const floatObserver = ref(new IntersectionObserver(handleIntersect, options))
|
|
33
|
+
|
|
34
|
+
const setPosition = () => nextTick(() => {
|
|
35
|
+
safeElementDomRef.value =
|
|
36
|
+
typeof args.element.domRef.value === 'string'
|
|
37
|
+
? document.querySelector(args.element.domRef.value)
|
|
38
|
+
: args.element.domRef.value
|
|
39
|
+
if (!args.container) {
|
|
40
|
+
safeContainerDomRef.value = document.body
|
|
41
|
+
} else {
|
|
42
|
+
safeContainerDomRef.value =
|
|
43
|
+
typeof args.container.domRef.value === 'string'
|
|
44
|
+
? document.querySelector(args.container.domRef.value)
|
|
45
|
+
: args.container.domRef.value
|
|
46
|
+
safeContainerDomRef.value ??= document.body
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!safeElementDomRef.value) {
|
|
50
|
+
throw new Error('missing reference element for floating behavior')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (args.opener) {
|
|
54
|
+
safeOpenerDomRef.value =
|
|
55
|
+
typeof args.opener.domRef.value === 'string'
|
|
56
|
+
? document.querySelector(args.opener.domRef.value)
|
|
57
|
+
: args.opener.domRef.value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
rect.value = safeElementDomRef.value.getBoundingClientRect()
|
|
61
|
+
const openerRect = safeOpenerDomRef.value?.getBoundingClientRect()
|
|
62
|
+
const containerRect = safeContainerDomRef.value.getBoundingClientRect()
|
|
63
|
+
|
|
64
|
+
// multiple observer calls on same target do not cause multiple registrations
|
|
65
|
+
floatObserver.value.observe(safeElementDomRef.value)
|
|
66
|
+
floatObserver.value.observe(safeContainerDomRef.value)
|
|
67
|
+
|
|
68
|
+
let position: FzFloatingPosition = args.position ? args.position.value : 'auto'
|
|
69
|
+
|
|
70
|
+
let translateY = 0
|
|
71
|
+
let translateX = 0
|
|
72
|
+
|
|
73
|
+
if (args.opener && safeOpenerDomRef.value && openerRect) {
|
|
74
|
+
switch (position) {
|
|
75
|
+
case 'auto':
|
|
76
|
+
position = getHighestAvailableSpacePos(
|
|
77
|
+
safeContainerDomRef.value,
|
|
78
|
+
safeElementDomRef.value,
|
|
79
|
+
safeOpenerDomRef.value
|
|
80
|
+
)
|
|
81
|
+
break
|
|
82
|
+
case 'auto-start':
|
|
83
|
+
position = getHighestAvailableSpacePos(
|
|
84
|
+
safeContainerDomRef.value,
|
|
85
|
+
safeElementDomRef.value,
|
|
86
|
+
safeOpenerDomRef.value,
|
|
87
|
+
'start'
|
|
88
|
+
)
|
|
89
|
+
break
|
|
90
|
+
case 'auto-end':
|
|
91
|
+
position = getHighestAvailableSpacePos(
|
|
92
|
+
safeContainerDomRef.value,
|
|
93
|
+
safeElementDomRef.value,
|
|
94
|
+
safeOpenerDomRef.value,
|
|
95
|
+
'end'
|
|
96
|
+
)
|
|
97
|
+
break
|
|
98
|
+
default:
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
switch (position) {
|
|
102
|
+
case 'bottom':
|
|
103
|
+
float.position.y = openerRect.bottom
|
|
104
|
+
float.position.x = openerRect.left + openerRect.width / 2
|
|
105
|
+
translateX = -50
|
|
106
|
+
translateY = 0
|
|
107
|
+
break
|
|
108
|
+
case 'bottom-start':
|
|
109
|
+
float.position.y = openerRect.bottom
|
|
110
|
+
float.position.x = openerRect.left
|
|
111
|
+
translateX = 0
|
|
112
|
+
translateY = 0
|
|
113
|
+
break
|
|
114
|
+
case 'bottom-end':
|
|
115
|
+
float.position.y = openerRect.bottom
|
|
116
|
+
float.position.x = openerRect.right
|
|
117
|
+
translateX = -100
|
|
118
|
+
translateY = 0
|
|
119
|
+
break
|
|
120
|
+
case 'left-start':
|
|
121
|
+
float.position.y = openerRect.top
|
|
122
|
+
float.position.x = openerRect.left
|
|
123
|
+
translateX = -100
|
|
124
|
+
translateY = 0
|
|
125
|
+
break
|
|
126
|
+
case 'left':
|
|
127
|
+
float.position.y = openerRect.top + openerRect.height / 2
|
|
128
|
+
float.position.x = openerRect.left
|
|
129
|
+
translateY = -50
|
|
130
|
+
translateX = -100
|
|
131
|
+
break
|
|
132
|
+
case 'left-end':
|
|
133
|
+
float.position.y = openerRect.bottom
|
|
134
|
+
float.position.x = openerRect.left
|
|
135
|
+
translateY = -100
|
|
136
|
+
translateX = -100
|
|
137
|
+
break
|
|
138
|
+
case 'top-start':
|
|
139
|
+
float.position.y = openerRect.top
|
|
140
|
+
float.position.x = openerRect.left
|
|
141
|
+
translateY = -100
|
|
142
|
+
translateX = 0
|
|
143
|
+
break
|
|
144
|
+
case 'top':
|
|
145
|
+
float.position.y = openerRect.top
|
|
146
|
+
float.position.x = openerRect.left + openerRect.width / 2
|
|
147
|
+
translateX = -50
|
|
148
|
+
translateY = -100
|
|
149
|
+
break
|
|
150
|
+
case 'top-end':
|
|
151
|
+
float.position.y = openerRect.top
|
|
152
|
+
float.position.x = openerRect.right
|
|
153
|
+
translateX = -100
|
|
154
|
+
translateY = -100
|
|
155
|
+
break
|
|
156
|
+
case 'right-start':
|
|
157
|
+
float.position.y = openerRect.top
|
|
158
|
+
float.position.x = openerRect.right
|
|
159
|
+
translateX = 0
|
|
160
|
+
translateY = 0
|
|
161
|
+
break
|
|
162
|
+
case 'right':
|
|
163
|
+
float.position.y = openerRect.top + openerRect.height / 2
|
|
164
|
+
float.position.x = openerRect.right
|
|
165
|
+
translateY = -50
|
|
166
|
+
translateX = 0
|
|
167
|
+
break
|
|
168
|
+
case 'right-end':
|
|
169
|
+
float.position.y = openerRect.bottom
|
|
170
|
+
float.position.x = openerRect.right
|
|
171
|
+
translateY = -100
|
|
172
|
+
translateX = 0
|
|
173
|
+
break
|
|
174
|
+
default:
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
switch (position) {
|
|
179
|
+
case 'bottom':
|
|
180
|
+
float.position.y = containerRect.bottom - rect.value.height
|
|
181
|
+
float.position.x = containerRect.left + containerRect.width / 2
|
|
182
|
+
translateX = -50
|
|
183
|
+
break
|
|
184
|
+
case 'left-end':
|
|
185
|
+
case 'bottom-start':
|
|
186
|
+
float.position.y = containerRect.bottom - rect.value.height
|
|
187
|
+
float.position.x = containerRect.left
|
|
188
|
+
break
|
|
189
|
+
case 'right-end':
|
|
190
|
+
case 'bottom-end':
|
|
191
|
+
float.position.y = containerRect.bottom - rect.value.height
|
|
192
|
+
float.position.x = containerRect.right - rect.value.width
|
|
193
|
+
break
|
|
194
|
+
case 'left':
|
|
195
|
+
float.position.y = containerRect.top + (containerRect.height - rect.value.height) / 2
|
|
196
|
+
float.position.x = containerRect.left
|
|
197
|
+
break
|
|
198
|
+
case 'top-start':
|
|
199
|
+
case 'left-start':
|
|
200
|
+
float.position.y = containerRect.top
|
|
201
|
+
float.position.x = containerRect.left
|
|
202
|
+
break
|
|
203
|
+
case 'top':
|
|
204
|
+
float.position.y = containerRect.top
|
|
205
|
+
float.position.x = containerRect.left + (containerRect.width - rect.value.width) / 2
|
|
206
|
+
break
|
|
207
|
+
case 'top-end':
|
|
208
|
+
case 'right-start':
|
|
209
|
+
float.position.y = containerRect.top
|
|
210
|
+
float.position.x = containerRect.right - rect.value.width
|
|
211
|
+
case 'right':
|
|
212
|
+
float.position.y = containerRect.top + (containerRect.height - rect.value.height) / 2
|
|
213
|
+
float.position.x = containerRect.right - rect.value.width
|
|
214
|
+
break
|
|
215
|
+
default:
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const realPos = calcRealPos(rect.value, float.position, translateX, translateY)
|
|
221
|
+
|
|
222
|
+
if (realPos.x < containerRect.left) {
|
|
223
|
+
float.position.x = containerRect.left;
|
|
224
|
+
translateX = 0
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if ((realPos.x + rect.value.width) > containerRect.right) {
|
|
228
|
+
float.position.x = containerRect.right - rect.value.width;
|
|
229
|
+
translateX = 0
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (realPos.y < containerRect.top) {
|
|
233
|
+
float.position.y = containerRect.top;
|
|
234
|
+
translateY = 0
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if ((realPos.y + rect.value.height) > containerRect.bottom) {
|
|
238
|
+
float.position.y = containerRect.bottom - rect.value.height;
|
|
239
|
+
translateY = 0
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
safeElementDomRef.value.style.top = `${float.position.y}px`
|
|
243
|
+
safeElementDomRef.value.style.left = `${float.position.x}px`
|
|
244
|
+
safeElementDomRef.value.style.transform = `translateY(${translateY}%) translateX(${translateX}%)`
|
|
245
|
+
safeElementDomRef.value.style.position = 'absolute'
|
|
246
|
+
|
|
247
|
+
rect.value = safeElementDomRef.value.getBoundingClientRect()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
onUnmounted(() => {
|
|
251
|
+
floatObserver.value.disconnect()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
float,
|
|
256
|
+
rect,
|
|
257
|
+
floatObserver,
|
|
258
|
+
setPosition
|
|
259
|
+
}
|
|
260
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
type PositionPrimary = 'bottom' | 'left' | 'top' | 'right' | 'auto'
|
|
4
|
+
type PositionSecondary = 'start' | 'end'
|
|
5
|
+
export type FzFloatingPosition = PositionPrimary | `${PositionPrimary}-${PositionSecondary}`
|
|
6
|
+
|
|
7
|
+
export interface FzFloatingProps {
|
|
8
|
+
isOpen: boolean
|
|
9
|
+
position?: FzFloatingPosition
|
|
10
|
+
container?: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FzAbsolutePosition {
|
|
14
|
+
x: number
|
|
15
|
+
y: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FzRect {
|
|
19
|
+
position: FzAbsolutePosition
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FzFloatElement {
|
|
23
|
+
domRef: Ref<string | HTMLElement | null>
|
|
24
|
+
intersectionOptions?: IntersectionObserverInit
|
|
25
|
+
}
|
|
26
|
+
export interface FzUseFloatingArgs {
|
|
27
|
+
element: FzFloatElement
|
|
28
|
+
container?: FzFloatElement
|
|
29
|
+
opener?: FzFloatElement
|
|
30
|
+
position?: Ref<FzFloatingPosition>
|
|
31
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { FzAbsolutePosition, FzFloatingPosition } from '../types'
|
|
2
|
+
|
|
3
|
+
type MainPosition = 'right' | 'left' | 'top' | 'bottom'
|
|
4
|
+
interface PositionListItem {
|
|
5
|
+
key: MainPosition
|
|
6
|
+
space: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getHighestAvailableSpacePos = (
|
|
10
|
+
container: HTMLElement,
|
|
11
|
+
el: HTMLElement,
|
|
12
|
+
opener: HTMLElement,
|
|
13
|
+
justify?: 'start' | 'end'
|
|
14
|
+
): FzFloatingPosition => {
|
|
15
|
+
let mainPosition: MainPosition = 'right'
|
|
16
|
+
|
|
17
|
+
let positionRes: FzFloatingPosition = 'right-start'
|
|
18
|
+
|
|
19
|
+
const containerRect = container.getBoundingClientRect()
|
|
20
|
+
const elRect = el.getBoundingClientRect()
|
|
21
|
+
const openerRect = opener.getBoundingClientRect()
|
|
22
|
+
|
|
23
|
+
const spaceLeftNormalized = (openerRect.left - containerRect.left - elRect.width) / containerRect.width
|
|
24
|
+
const spaceRightNormalized = (containerRect.right - openerRect.right - elRect.width) / containerRect.width
|
|
25
|
+
const spaceTopNormalized = (openerRect.top - containerRect.top - elRect.height) / containerRect.height
|
|
26
|
+
const spaceBottomNormalized = (containerRect.bottom - openerRect.bottom - elRect.height) / containerRect.height
|
|
27
|
+
|
|
28
|
+
const positionList = [
|
|
29
|
+
{
|
|
30
|
+
key: 'right',
|
|
31
|
+
space: spaceRightNormalized
|
|
32
|
+
} as PositionListItem,
|
|
33
|
+
{
|
|
34
|
+
key: 'top',
|
|
35
|
+
space: spaceTopNormalized
|
|
36
|
+
} as PositionListItem,
|
|
37
|
+
{
|
|
38
|
+
key: 'bottom',
|
|
39
|
+
space: spaceBottomNormalized
|
|
40
|
+
} as PositionListItem,
|
|
41
|
+
{
|
|
42
|
+
key: 'left',
|
|
43
|
+
space: spaceLeftNormalized
|
|
44
|
+
} as PositionListItem
|
|
45
|
+
].sort((a, b) => b.space - a.space)
|
|
46
|
+
|
|
47
|
+
mainPosition = positionList[0].key
|
|
48
|
+
|
|
49
|
+
if (!justify) {
|
|
50
|
+
switch (mainPosition) {
|
|
51
|
+
case 'right':
|
|
52
|
+
case 'left':
|
|
53
|
+
const centerDiff = (elRect.height - openerRect.height) / 2
|
|
54
|
+
if (containerRect.top > openerRect.top - centerDiff) {
|
|
55
|
+
justify = 'end'
|
|
56
|
+
}
|
|
57
|
+
if (containerRect.bottom < openerRect.top - centerDiff) {
|
|
58
|
+
justify = 'start'
|
|
59
|
+
}
|
|
60
|
+
break
|
|
61
|
+
default:
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
positionRes = justify ? `${mainPosition}-${justify}` : mainPosition
|
|
67
|
+
return positionRes
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
export const calcRealPos = (rect: DOMRect, pos: FzAbsolutePosition, translateX: number, translateY: number): FzAbsolutePosition => {
|
|
72
|
+
return {
|
|
73
|
+
x: pos.x + (rect.width * translateX)/100,
|
|
74
|
+
y: pos.y + (rect.height * translateY)/100,
|
|
75
|
+
}
|
|
76
|
+
}
|
package/tsconfig.json
ADDED
package/vite.config.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { fileURLToPath, URL } from 'node:url'
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { defineConfig } from 'vite'
|
|
5
|
+
import vue from '@vitejs/plugin-vue'
|
|
6
|
+
|
|
7
|
+
// https://vitejs.dev/config/
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [
|
|
10
|
+
vue(),
|
|
11
|
+
],
|
|
12
|
+
resolve: {
|
|
13
|
+
alias: {
|
|
14
|
+
'@': fileURLToPath(new URL('./src', import.meta.url))
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
build: {
|
|
18
|
+
lib: {
|
|
19
|
+
entry: resolve(__dirname, './src/index.ts'),
|
|
20
|
+
name: 'FzComposables',
|
|
21
|
+
},
|
|
22
|
+
rollupOptions: {
|
|
23
|
+
external: ['vue'],
|
|
24
|
+
output: {
|
|
25
|
+
globals: {
|
|
26
|
+
vue: 'Vue',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
|
3
|
+
import viteConfig from './vite.config'
|
|
4
|
+
|
|
5
|
+
export default mergeConfig(
|
|
6
|
+
viteConfig,
|
|
7
|
+
defineConfig({
|
|
8
|
+
test: {
|
|
9
|
+
environment: 'jsdom',
|
|
10
|
+
exclude: [...configDefaults.exclude, 'e2e/*'],
|
|
11
|
+
root: fileURLToPath(new URL('./', import.meta.url)),
|
|
12
|
+
coverage: {
|
|
13
|
+
provider: 'v8',
|
|
14
|
+
include: ['**/src/**'],
|
|
15
|
+
exclude: ['**/index.ts']
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
)
|