@tak-ps/vue-tabler 4.25.1 → 4.28.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/CHANGELOG.md +12 -0
- package/components/Dropdown.vue +153 -64
- package/components/TablerUploadLogo.vue +81 -0
- package/lib.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
## Version History
|
|
12
12
|
|
|
13
|
+
### v4.27.0
|
|
14
|
+
|
|
15
|
+
- :tada: Introduce `TablerUploadLogo` component
|
|
16
|
+
|
|
17
|
+
### v4.26.1
|
|
18
|
+
|
|
19
|
+
- :bug: Fix Lint Errors
|
|
20
|
+
|
|
21
|
+
### v4.26.0
|
|
22
|
+
|
|
23
|
+
- :rocket: Smarter Dropdown Positioning
|
|
24
|
+
|
|
13
25
|
### v4.25.1
|
|
14
26
|
|
|
15
27
|
- :rocket: Allow Dropdown strategy to be specified via prop
|
package/components/Dropdown.vue
CHANGED
|
@@ -1,37 +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
|
-
aria-expanded='false'
|
|
9
|
-
@click.stop.prevent=''
|
|
5
|
+
:aria-expanded='isOpen'
|
|
6
|
+
aria-haspopup='true'
|
|
7
|
+
@click.stop='toggle'
|
|
10
8
|
>
|
|
11
9
|
<slot>
|
|
12
10
|
<IconSettings
|
|
13
11
|
:size='32'
|
|
14
|
-
|
|
12
|
+
stroke='1'
|
|
15
13
|
class='cursor-pointer'
|
|
16
14
|
/>
|
|
17
15
|
</slot>
|
|
18
16
|
</div>
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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>
|
|
30
31
|
</div>
|
|
31
32
|
</template>
|
|
32
33
|
|
|
33
34
|
<script setup lang="ts">
|
|
34
|
-
import { ref, computed } from 'vue';
|
|
35
|
+
import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
|
|
35
36
|
import {
|
|
36
37
|
IconSettings
|
|
37
38
|
} from '@tabler/icons-vue';
|
|
@@ -40,53 +41,147 @@ export interface DropdownProps {
|
|
|
40
41
|
width?: number | string;
|
|
41
42
|
autoclose?: string;
|
|
42
43
|
position?: 'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end' | 'left' | 'right';
|
|
43
|
-
strategy?: 'absolute' | 'fixed';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const props = withDefaults(defineProps<DropdownProps>(), {
|
|
47
47
|
width: 200,
|
|
48
48
|
autoclose: 'true',
|
|
49
49
|
position: 'bottom-end',
|
|
50
|
-
strategy: 'absolute'
|
|
51
50
|
});
|
|
52
51
|
|
|
53
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>>({});
|
|
54
57
|
|
|
55
|
-
const
|
|
56
|
-
|
|
58
|
+
const menuWidth = computed(() =>
|
|
59
|
+
typeof props.width === 'number' ? `${props.width}px` : String(props.width)
|
|
60
|
+
);
|
|
57
61
|
|
|
62
|
+
const menuClasses = computed(() => {
|
|
63
|
+
const base = 'dropdown-menu dropdown-menu-card tabler-dropdown__menu show';
|
|
58
64
|
switch (props.position) {
|
|
59
|
-
case '
|
|
65
|
+
case 'bottom-start':
|
|
60
66
|
case 'top-start':
|
|
67
|
+
return `${base} dropdown-menu-start`;
|
|
68
|
+
case 'bottom-end':
|
|
61
69
|
case 'top-end':
|
|
62
|
-
return `${
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
case 'right':
|
|
66
|
-
return `${baseClass} dropend`;
|
|
67
|
-
default: // bottom, bottom-start, bottom-end
|
|
68
|
-
return baseClass;
|
|
70
|
+
return `${base} dropdown-menu-end`;
|
|
71
|
+
default:
|
|
72
|
+
return base;
|
|
69
73
|
}
|
|
70
74
|
});
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
74
87
|
|
|
75
88
|
switch (props.position) {
|
|
89
|
+
case 'bottom':
|
|
90
|
+
top = t.bottom + gap;
|
|
91
|
+
left = t.left + t.width / 2 - m.width / 2;
|
|
92
|
+
break;
|
|
76
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;
|
|
77
101
|
case 'top-start':
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
top = t.top - m.height - gap;
|
|
103
|
+
left = t.left;
|
|
104
|
+
break;
|
|
80
105
|
case 'top-end':
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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;
|
|
84
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;
|
|
85
180
|
});
|
|
86
181
|
</script>
|
|
87
182
|
|
|
88
|
-
<style
|
|
89
|
-
.tabler-dropdown__menu {
|
|
183
|
+
<style>
|
|
184
|
+
.dropdown-menu.tabler-dropdown__menu {
|
|
90
185
|
--tabler-dropdown-color: rgba(255, 255, 255, 0.92);
|
|
91
186
|
--tabler-dropdown-bg: rgba(20, 20, 25, 0.96);
|
|
92
187
|
--tabler-dropdown-border-color: rgba(255, 255, 255, 0.25);
|
|
@@ -96,7 +191,6 @@ const menuClasses = computed(() => {
|
|
|
96
191
|
--tabler-dropdown-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35);
|
|
97
192
|
margin-block: 0.25rem;
|
|
98
193
|
padding: 0.25rem 0;
|
|
99
|
-
overflow: hidden;
|
|
100
194
|
color: var(--tabler-dropdown-color);
|
|
101
195
|
border-color: var(--tabler-dropdown-border-color);
|
|
102
196
|
background: var(--tabler-dropdown-bg);
|
|
@@ -104,7 +198,7 @@ const menuClasses = computed(() => {
|
|
|
104
198
|
box-shadow: var(--tabler-dropdown-shadow);
|
|
105
199
|
}
|
|
106
200
|
|
|
107
|
-
[data-bs-theme='light'] .tabler-dropdown__menu {
|
|
201
|
+
[data-bs-theme='light'] .dropdown-menu.tabler-dropdown__menu {
|
|
108
202
|
--tabler-dropdown-color: var(--tblr-body-color);
|
|
109
203
|
--tabler-dropdown-bg: rgba(255, 255, 255, 0.96);
|
|
110
204
|
--tabler-dropdown-border-color: rgba(var(--tblr-primary-rgb), 0.15);
|
|
@@ -113,49 +207,44 @@ const menuClasses = computed(() => {
|
|
|
113
207
|
--tabler-dropdown-shadow: 0 0.5rem 1rem rgba(15, 23, 42, 0.08);
|
|
114
208
|
}
|
|
115
209
|
|
|
116
|
-
[data-bs-theme='light'] .tabler-dropdown__menu
|
|
117
|
-
[data-bs-theme='light'] .tabler-dropdown__menu
|
|
118
|
-
[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 {
|
|
119
213
|
color: var(--tblr-body-color) !important;
|
|
120
214
|
}
|
|
121
215
|
|
|
122
|
-
[data-bs-theme='light'] .tabler-dropdown__menu
|
|
216
|
+
[data-bs-theme='light'] .tabler-dropdown__menu .text-white-50 {
|
|
123
217
|
color: var(--tblr-secondary-color) !important;
|
|
124
218
|
}
|
|
125
219
|
|
|
126
|
-
.tabler-dropdown__menu.dropdown-
|
|
127
|
-
.tabler-dropdown__menu.
|
|
128
|
-
display: none;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.tabler-dropdown__menu :deep(.dropdown-item),
|
|
132
|
-
.tabler-dropdown__menu :deep(.tabler-dropdown__item) {
|
|
220
|
+
.tabler-dropdown__menu .dropdown-item,
|
|
221
|
+
.tabler-dropdown__menu .tabler-dropdown__item {
|
|
133
222
|
cursor: pointer;
|
|
134
223
|
color: inherit;
|
|
135
224
|
transition: background 0.1s ease, color 0.1s ease;
|
|
136
225
|
}
|
|
137
226
|
|
|
138
|
-
.tabler-dropdown__menu
|
|
139
|
-
.tabler-dropdown__menu
|
|
227
|
+
.tabler-dropdown__menu .dropdown-item:hover,
|
|
228
|
+
.tabler-dropdown__menu .tabler-dropdown__item:hover {
|
|
140
229
|
background: var(--tabler-dropdown-hover-bg);
|
|
141
230
|
}
|
|
142
231
|
|
|
143
|
-
.tabler-dropdown__menu
|
|
144
|
-
.tabler-dropdown__menu
|
|
145
|
-
.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 {
|
|
146
235
|
background: var(--tabler-dropdown-active-bg);
|
|
147
236
|
color: var(--tabler-dropdown-active-color);
|
|
148
237
|
}
|
|
149
238
|
|
|
150
|
-
.tabler-dropdown__menu
|
|
151
|
-
.tabler-dropdown__menu
|
|
152
|
-
.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 {
|
|
153
242
|
color: var(--tabler-dropdown-active-color) !important;
|
|
154
243
|
}
|
|
155
244
|
|
|
156
|
-
.tabler-dropdown__menu
|
|
157
|
-
.tabler-dropdown__menu
|
|
158
|
-
.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 {
|
|
159
248
|
color: rgba(var(--tblr-primary-rgb), 0.7) !important;
|
|
160
249
|
}
|
|
161
250
|
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class='mb-3'>
|
|
3
|
+
<TablerInput
|
|
4
|
+
:id='props.inputId || "logoUpload"'
|
|
5
|
+
:label='props.label || "Upload PNG/SVG Logo"'
|
|
6
|
+
type='file'
|
|
7
|
+
accept='image/png, image/svg+xml'
|
|
8
|
+
:disabled='props.disabled'
|
|
9
|
+
:error='error'
|
|
10
|
+
@change='onFileChange'
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
13
|
+
<div
|
|
14
|
+
v-if='base64Data'
|
|
15
|
+
class='mt-3 row d-flex align-items-center justify-content-center'
|
|
16
|
+
>
|
|
17
|
+
<img
|
|
18
|
+
:src='base64Data'
|
|
19
|
+
alt='Logo Preview'
|
|
20
|
+
class='img-thumbnail'
|
|
21
|
+
style='max-width: 200px;'
|
|
22
|
+
>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang='ts'>
|
|
27
|
+
import { ref, watch } from 'vue';
|
|
28
|
+
import TablerInput from './input/Input.vue';
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits(['update:modelValue', 'file-name']);
|
|
31
|
+
|
|
32
|
+
const props = defineProps<{
|
|
33
|
+
modelValue?: string | null;
|
|
34
|
+
label?: string;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
inputId?: string;
|
|
37
|
+
}>();
|
|
38
|
+
|
|
39
|
+
const base64Data = ref<string | undefined | null>(props.modelValue);
|
|
40
|
+
const error = ref('');
|
|
41
|
+
|
|
42
|
+
watch(() => props.modelValue, () => {
|
|
43
|
+
base64Data.value = props.modelValue;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
watch(base64Data, () => {
|
|
47
|
+
emit('update:modelValue', base64Data.value);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function onFileChange(event: Event) {
|
|
51
|
+
const target = event.target as HTMLInputElement;
|
|
52
|
+
|
|
53
|
+
error.value = '';
|
|
54
|
+
|
|
55
|
+
if (!target.files || target.files.length === 0) {
|
|
56
|
+
throw new Error('Could not read file from input');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const file = target.files[0];
|
|
60
|
+
if (!file) return;
|
|
61
|
+
|
|
62
|
+
emit('file-name', file.name);
|
|
63
|
+
|
|
64
|
+
if (!['image/png', 'image/svg+xml'].includes(file.type)) {
|
|
65
|
+
error.value = 'Please upload a PNG or SVG file.';
|
|
66
|
+
base64Data.value = '';
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const reader = new FileReader();
|
|
71
|
+
reader.onload = (e) => {
|
|
72
|
+
base64Data.value = (e.target && e.target.result) ? String(e.target.result) : undefined;
|
|
73
|
+
};
|
|
74
|
+
reader.onerror = () => {
|
|
75
|
+
error.value = 'Failed to read file.';
|
|
76
|
+
base64Data.value = '';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
reader.readAsDataURL(file);
|
|
80
|
+
}
|
|
81
|
+
</script>
|
package/lib.ts
CHANGED
|
@@ -34,3 +34,4 @@ export { default as TablerDelete } from './components/Delete.vue';
|
|
|
34
34
|
export { default as TablerPillGroup } from './components/PillGroup.vue';
|
|
35
35
|
export { default as TablerSchema } from './components/Schema.vue';
|
|
36
36
|
export { default as TablerSchemaBuilder } from './components/SchemaBuilder.vue';
|
|
37
|
+
export { default as TablerUploadLogo } from './components/TablerUploadLogo.vue';
|