@tak-ps/vue-tabler 4.25.0 → 4.26.1
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/CHANGELOG.md +12 -0
- package/components/Dropdown.vue +154 -62
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
## Version History
|
|
12
12
|
|
|
13
|
+
### v4.26.1
|
|
14
|
+
|
|
15
|
+
- :bug: Fix Lint Errors
|
|
16
|
+
|
|
17
|
+
### v4.26.0
|
|
18
|
+
|
|
19
|
+
- :rocket: Smarter Dropdown Positioning
|
|
20
|
+
|
|
21
|
+
### v4.25.1
|
|
22
|
+
|
|
23
|
+
- :rocket: Allow Dropdown strategy to be specified via prop
|
|
24
|
+
|
|
13
25
|
### v4.25.0
|
|
14
26
|
|
|
15
27
|
- :rocket: Update Core Deps
|
package/components/Dropdown.vue
CHANGED
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div ref='triggerRef' class='tabler-dropdown d-inline-flex'>
|
|
3
3
|
<div
|
|
4
4
|
:id='id'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@click.stop.prevent=''
|
|
5
|
+
:aria-expanded='isOpen'
|
|
6
|
+
aria-haspopup='true'
|
|
7
|
+
@click.stop='toggle'
|
|
9
8
|
>
|
|
10
9
|
<slot>
|
|
11
10
|
<IconSettings
|
|
12
11
|
:size='32'
|
|
13
|
-
|
|
12
|
+
stroke='1'
|
|
14
13
|
class='cursor-pointer'
|
|
15
14
|
/>
|
|
16
15
|
</slot>
|
|
17
16
|
</div>
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
<Teleport to='body'>
|
|
18
|
+
<ul
|
|
19
|
+
v-if='isOpen'
|
|
20
|
+
ref='menuRef'
|
|
21
|
+
:class='menuClasses'
|
|
22
|
+
:style='floatingStyle'
|
|
23
|
+
:aria-labelledby='id'
|
|
24
|
+
@click='onMenuClick'
|
|
25
|
+
>
|
|
26
|
+
<slot name='dropdown'>
|
|
27
|
+
Dropdown Content
|
|
28
|
+
</slot>
|
|
29
|
+
</ul>
|
|
30
|
+
</Teleport>
|
|
29
31
|
</div>
|
|
30
32
|
</template>
|
|
31
33
|
|
|
32
34
|
<script setup lang="ts">
|
|
33
|
-
import { ref, computed } from 'vue';
|
|
35
|
+
import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
|
|
34
36
|
import {
|
|
35
37
|
IconSettings
|
|
36
38
|
} from '@tabler/icons-vue';
|
|
@@ -44,46 +46,142 @@ export interface DropdownProps {
|
|
|
44
46
|
const props = withDefaults(defineProps<DropdownProps>(), {
|
|
45
47
|
width: 200,
|
|
46
48
|
autoclose: 'true',
|
|
47
|
-
position: 'bottom-end'
|
|
49
|
+
position: 'bottom-end',
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
const id = ref('tabler-dropdown-' + Math.random().toString(36).substr(2, 9) + '-' + Date.now().toString(36));
|
|
53
|
+
const isOpen = ref(false);
|
|
54
|
+
const triggerRef = ref<HTMLElement | null>(null);
|
|
55
|
+
const menuRef = ref<HTMLElement | null>(null);
|
|
56
|
+
const floatingStyle = ref<Record<string, string>>({});
|
|
51
57
|
|
|
52
|
-
const
|
|
53
|
-
|
|
58
|
+
const menuWidth = computed(() =>
|
|
59
|
+
typeof props.width === 'number' ? `${props.width}px` : String(props.width)
|
|
60
|
+
);
|
|
54
61
|
|
|
62
|
+
const menuClasses = computed(() => {
|
|
63
|
+
const base = 'dropdown-menu dropdown-menu-card tabler-dropdown__menu show';
|
|
55
64
|
switch (props.position) {
|
|
56
|
-
case '
|
|
65
|
+
case 'bottom-start':
|
|
57
66
|
case 'top-start':
|
|
67
|
+
return `${base} dropdown-menu-start`;
|
|
68
|
+
case 'bottom-end':
|
|
58
69
|
case 'top-end':
|
|
59
|
-
return `${
|
|
60
|
-
|
|
61
|
-
return
|
|
62
|
-
case 'right':
|
|
63
|
-
return `${baseClass} dropend`;
|
|
64
|
-
default: // bottom, bottom-start, bottom-end
|
|
65
|
-
return baseClass;
|
|
70
|
+
return `${base} dropdown-menu-end`;
|
|
71
|
+
default:
|
|
72
|
+
return base;
|
|
66
73
|
}
|
|
67
74
|
});
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
function calcPosition() {
|
|
77
|
+
if (!triggerRef.value || !menuRef.value) return;
|
|
78
|
+
|
|
79
|
+
const t = triggerRef.value.getBoundingClientRect();
|
|
80
|
+
const m = menuRef.value.getBoundingClientRect();
|
|
81
|
+
const vw = window.innerWidth;
|
|
82
|
+
const vh = window.innerHeight;
|
|
83
|
+
const gap = 4;
|
|
84
|
+
|
|
85
|
+
let top: number;
|
|
86
|
+
let left: number;
|
|
71
87
|
|
|
72
88
|
switch (props.position) {
|
|
89
|
+
case 'bottom':
|
|
90
|
+
top = t.bottom + gap;
|
|
91
|
+
left = t.left + t.width / 2 - m.width / 2;
|
|
92
|
+
break;
|
|
73
93
|
case 'bottom-start':
|
|
94
|
+
top = t.bottom + gap;
|
|
95
|
+
left = t.left;
|
|
96
|
+
break;
|
|
97
|
+
case 'top':
|
|
98
|
+
top = t.top - m.height - gap;
|
|
99
|
+
left = t.left + t.width / 2 - m.width / 2;
|
|
100
|
+
break;
|
|
74
101
|
case 'top-start':
|
|
75
|
-
|
|
76
|
-
|
|
102
|
+
top = t.top - m.height - gap;
|
|
103
|
+
left = t.left;
|
|
104
|
+
break;
|
|
77
105
|
case 'top-end':
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
top = t.top - m.height - gap;
|
|
107
|
+
left = t.right - m.width;
|
|
108
|
+
break;
|
|
109
|
+
case 'left':
|
|
110
|
+
top = t.top;
|
|
111
|
+
left = t.left - m.width - gap;
|
|
112
|
+
break;
|
|
113
|
+
case 'right':
|
|
114
|
+
top = t.top;
|
|
115
|
+
left = t.right + gap;
|
|
116
|
+
break;
|
|
117
|
+
default: // bottom-end
|
|
118
|
+
top = t.bottom + gap;
|
|
119
|
+
left = t.right - m.width;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Keep within viewport
|
|
123
|
+
left = Math.max(gap, Math.min(left, vw - m.width - gap));
|
|
124
|
+
top = Math.max(gap, Math.min(top, vh - gap));
|
|
125
|
+
|
|
126
|
+
// Constrain height to available space below the computed top
|
|
127
|
+
const maxHeight = Math.max(100, vh - top - gap);
|
|
128
|
+
|
|
129
|
+
floatingStyle.value = {
|
|
130
|
+
position: 'fixed',
|
|
131
|
+
zIndex: '9999',
|
|
132
|
+
top: `${top}px`,
|
|
133
|
+
left: `${left}px`,
|
|
134
|
+
minWidth: menuWidth.value,
|
|
135
|
+
maxHeight: `${maxHeight}px`,
|
|
136
|
+
overflowY: 'auto',
|
|
137
|
+
margin: '0',
|
|
138
|
+
visibility: 'visible',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function toggle() {
|
|
143
|
+
if (isOpen.value) {
|
|
144
|
+
isOpen.value = false;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Render hidden first so we can measure before positioning
|
|
148
|
+
floatingStyle.value = { position: 'fixed', zIndex: '9999', visibility: 'hidden', minWidth: menuWidth.value };
|
|
149
|
+
isOpen.value = true;
|
|
150
|
+
await nextTick();
|
|
151
|
+
calcPosition();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onMenuClick() {
|
|
155
|
+
if (props.autoclose === 'true') {
|
|
156
|
+
isOpen.value = false;
|
|
81
157
|
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function onOutsideClick(e: MouseEvent) {
|
|
161
|
+
if (!isOpen.value) return;
|
|
162
|
+
const target = e.target as Node;
|
|
163
|
+
if (triggerRef.value?.contains(target) || menuRef.value?.contains(target)) return;
|
|
164
|
+
isOpen.value = false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function onEscape(e: KeyboardEvent) {
|
|
168
|
+
if (e.key === 'Escape') isOpen.value = false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
onMounted(() => {
|
|
172
|
+
document.addEventListener('click', onOutsideClick, true);
|
|
173
|
+
document.addEventListener('keydown', onEscape);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
onBeforeUnmount(() => {
|
|
177
|
+
document.removeEventListener('click', onOutsideClick, true);
|
|
178
|
+
document.removeEventListener('keydown', onEscape);
|
|
179
|
+
isOpen.value = false;
|
|
82
180
|
});
|
|
83
181
|
</script>
|
|
84
182
|
|
|
85
|
-
<style
|
|
86
|
-
.tabler-dropdown__menu {
|
|
183
|
+
<style>
|
|
184
|
+
.dropdown-menu.tabler-dropdown__menu {
|
|
87
185
|
--tabler-dropdown-color: rgba(255, 255, 255, 0.92);
|
|
88
186
|
--tabler-dropdown-bg: rgba(20, 20, 25, 0.96);
|
|
89
187
|
--tabler-dropdown-border-color: rgba(255, 255, 255, 0.25);
|
|
@@ -93,7 +191,6 @@ const menuClasses = computed(() => {
|
|
|
93
191
|
--tabler-dropdown-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35);
|
|
94
192
|
margin-block: 0.25rem;
|
|
95
193
|
padding: 0.25rem 0;
|
|
96
|
-
overflow: hidden;
|
|
97
194
|
color: var(--tabler-dropdown-color);
|
|
98
195
|
border-color: var(--tabler-dropdown-border-color);
|
|
99
196
|
background: var(--tabler-dropdown-bg);
|
|
@@ -101,7 +198,7 @@ const menuClasses = computed(() => {
|
|
|
101
198
|
box-shadow: var(--tabler-dropdown-shadow);
|
|
102
199
|
}
|
|
103
200
|
|
|
104
|
-
[data-bs-theme='light'] .tabler-dropdown__menu {
|
|
201
|
+
[data-bs-theme='light'] .dropdown-menu.tabler-dropdown__menu {
|
|
105
202
|
--tabler-dropdown-color: var(--tblr-body-color);
|
|
106
203
|
--tabler-dropdown-bg: rgba(255, 255, 255, 0.96);
|
|
107
204
|
--tabler-dropdown-border-color: rgba(var(--tblr-primary-rgb), 0.15);
|
|
@@ -110,49 +207,44 @@ const menuClasses = computed(() => {
|
|
|
110
207
|
--tabler-dropdown-shadow: 0 0.5rem 1rem rgba(15, 23, 42, 0.08);
|
|
111
208
|
}
|
|
112
209
|
|
|
113
|
-
[data-bs-theme='light'] .tabler-dropdown__menu
|
|
114
|
-
[data-bs-theme='light'] .tabler-dropdown__menu
|
|
115
|
-
[data-bs-theme='light'] .tabler-dropdown__menu
|
|
210
|
+
[data-bs-theme='light'] .tabler-dropdown__menu .dropdown-item,
|
|
211
|
+
[data-bs-theme='light'] .tabler-dropdown__menu .tabler-dropdown__item,
|
|
212
|
+
[data-bs-theme='light'] .tabler-dropdown__menu .text-white {
|
|
116
213
|
color: var(--tblr-body-color) !important;
|
|
117
214
|
}
|
|
118
215
|
|
|
119
|
-
[data-bs-theme='light'] .tabler-dropdown__menu
|
|
216
|
+
[data-bs-theme='light'] .tabler-dropdown__menu .text-white-50 {
|
|
120
217
|
color: var(--tblr-secondary-color) !important;
|
|
121
218
|
}
|
|
122
219
|
|
|
123
|
-
.tabler-dropdown__menu.dropdown-
|
|
124
|
-
.tabler-dropdown__menu.
|
|
125
|
-
display: none;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
.tabler-dropdown__menu :deep(.dropdown-item),
|
|
129
|
-
.tabler-dropdown__menu :deep(.tabler-dropdown__item) {
|
|
220
|
+
.tabler-dropdown__menu .dropdown-item,
|
|
221
|
+
.tabler-dropdown__menu .tabler-dropdown__item {
|
|
130
222
|
cursor: pointer;
|
|
131
223
|
color: inherit;
|
|
132
224
|
transition: background 0.1s ease, color 0.1s ease;
|
|
133
225
|
}
|
|
134
226
|
|
|
135
|
-
.tabler-dropdown__menu
|
|
136
|
-
.tabler-dropdown__menu
|
|
227
|
+
.tabler-dropdown__menu .dropdown-item:hover,
|
|
228
|
+
.tabler-dropdown__menu .tabler-dropdown__item:hover {
|
|
137
229
|
background: var(--tabler-dropdown-hover-bg);
|
|
138
230
|
}
|
|
139
231
|
|
|
140
|
-
.tabler-dropdown__menu
|
|
141
|
-
.tabler-dropdown__menu
|
|
142
|
-
.tabler-dropdown__menu
|
|
232
|
+
.tabler-dropdown__menu .dropdown-item.active,
|
|
233
|
+
.tabler-dropdown__menu .dropdown-item:active,
|
|
234
|
+
.tabler-dropdown__menu .tabler-dropdown__item--active {
|
|
143
235
|
background: var(--tabler-dropdown-active-bg);
|
|
144
236
|
color: var(--tabler-dropdown-active-color);
|
|
145
237
|
}
|
|
146
238
|
|
|
147
|
-
.tabler-dropdown__menu
|
|
148
|
-
.tabler-dropdown__menu
|
|
149
|
-
.tabler-dropdown__menu
|
|
239
|
+
.tabler-dropdown__menu .dropdown-item.active .text-white,
|
|
240
|
+
.tabler-dropdown__menu .dropdown-item:active .text-white,
|
|
241
|
+
.tabler-dropdown__menu .tabler-dropdown__item--active .text-white {
|
|
150
242
|
color: var(--tabler-dropdown-active-color) !important;
|
|
151
243
|
}
|
|
152
244
|
|
|
153
|
-
.tabler-dropdown__menu
|
|
154
|
-
.tabler-dropdown__menu
|
|
155
|
-
.tabler-dropdown__menu
|
|
245
|
+
.tabler-dropdown__menu .dropdown-item.active .text-white-50,
|
|
246
|
+
.tabler-dropdown__menu .dropdown-item:active .text-white-50,
|
|
247
|
+
.tabler-dropdown__menu .tabler-dropdown__item--active .text-white-50 {
|
|
156
248
|
color: rgba(var(--tblr-primary-rgb), 0.7) !important;
|
|
157
249
|
}
|
|
158
250
|
</style>
|