@m3ui-vue/m3ui-vue 0.3.1 → 0.4.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/README.md +219 -59
- package/dist/{MMenuItem-C6RwHl-l.js → MMenuItem-FyttjOj_.js} +399 -207
- package/dist/MMenuItem-FyttjOj_.js.map +1 -0
- package/dist/components/MAutocomplete.vue.d.ts +2 -0
- package/dist/components/MBox.vue.d.ts +2 -2
- package/dist/components/MButton.vue.d.ts +4 -0
- package/dist/components/MButtonGroup.vue.d.ts +28 -0
- package/dist/components/MCarousel.vue.d.ts +21 -12
- package/dist/components/MColorPickerModal.vue.d.ts +18 -0
- package/dist/components/MDataTable.vue.d.ts +1 -1
- package/dist/components/MDatePickerModal.vue.d.ts +21 -0
- package/dist/components/MDateRangePickerModal.vue.d.ts +24 -0
- package/dist/components/MDialog.vue.d.ts +9 -1
- package/dist/components/MExpansionPanel.vue.d.ts +3 -2
- package/dist/components/MFab.vue.d.ts +14 -2
- package/dist/components/MFlex.vue.d.ts +2 -2
- package/dist/components/MIconButton.vue.d.ts +4 -2
- package/dist/components/MInfiniteScroll.vue.d.ts +1 -1
- package/dist/components/MList.vue.d.ts +2 -0
- package/dist/components/MLoadingOverlay.vue.d.ts +1 -1
- package/dist/components/MMenu.vue.d.ts +3 -1
- package/dist/components/MMenuItem.vue.d.ts +14 -3
- package/dist/components/MMultiAutocomplete.vue.d.ts +2 -0
- package/dist/components/MMultiSelect.vue.d.ts +2 -0
- package/dist/components/MNavigationDrawer.vue.d.ts +1 -1
- package/dist/components/MNotificationHost.vue.d.ts +3 -0
- package/dist/components/MSelect.vue.d.ts +2 -0
- package/dist/components/MSlider.vue.d.ts +15 -4
- package/dist/components/MSplitButton.vue.d.ts +39 -0
- package/dist/components/MSplitter.vue.d.ts +1 -1
- package/dist/components/MStack.vue.d.ts +2 -2
- package/dist/components/MTable.vue.d.ts +1 -1
- package/dist/components/MTimePickerModal.vue.d.ts +21 -0
- package/dist/components/MToolbar.vue.d.ts +30 -0
- package/dist/components/MTooltip.vue.d.ts +8 -2
- package/dist/components/MWindow.vue.d.ts +2 -2
- package/dist/composables/useDevice.d.ts +3 -0
- package/dist/composables/useNotification.d.ts +64 -0
- package/dist/composables/useToast.d.ts +8 -0
- package/dist/index.d.ts +12 -0
- package/dist/m3ui-vue.css +1 -1
- package/dist/m3ui.js +5222 -2994
- package/dist/m3ui.js.map +1 -1
- package/dist/rich-text-editor.js +10 -10
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/MAutocomplete.vue +109 -13
- package/src/components/MButton.vue +38 -16
- package/src/components/MButtonGroup.vue +158 -0
- package/src/components/MCarousel.vue +223 -101
- package/src/components/MColorPicker.vue +1 -4
- package/src/components/MColorPickerModal.vue +276 -0
- package/src/components/MDatePicker.vue +1 -4
- package/src/components/MDatePickerModal.vue +265 -0
- package/src/components/MDateRangePicker.vue +1 -4
- package/src/components/MDateRangePickerModal.vue +264 -0
- package/src/components/MDialog.vue +38 -1
- package/src/components/MExpansionPanel.vue +10 -32
- package/src/components/MFab.vue +92 -11
- package/src/components/MIconButton.vue +36 -6
- package/src/components/MList.vue +6 -3
- package/src/components/MListItem.vue +9 -3
- package/src/components/MMenu.vue +10 -5
- package/src/components/MMenuItem.vue +93 -10
- package/src/components/MMultiAutocomplete.vue +105 -10
- package/src/components/MMultiSelect.vue +106 -13
- package/src/components/MNotificationHost.vue +163 -0
- package/src/components/MSelect.vue +99 -16
- package/src/components/MSlider.vue +367 -134
- package/src/components/MSnackbar.vue +3 -0
- package/src/components/MSplitButton.vue +240 -0
- package/src/components/MTabs.vue +82 -39
- package/src/components/MTagInput.vue +11 -1
- package/src/components/MTimePicker.vue +1 -4
- package/src/components/MTimePickerModal.vue +458 -0
- package/src/components/MToolbar.vue +75 -0
- package/src/components/MTooltip.vue +60 -10
- package/src/components/_MContextMenuPanel.vue +2 -2
- package/src/composables/useDevice.ts +29 -0
- package/src/composables/useNotification.ts +93 -0
- package/src/composables/useToast.ts +42 -5
- package/src/index.ts +12 -0
- package/dist/MMenuItem-C6RwHl-l.js.map +0 -1
|
@@ -51,6 +51,7 @@ const isExpanded = computed(() => internalExpanded.value)
|
|
|
51
51
|
const list = inject<{
|
|
52
52
|
dense: ComputedRef<boolean>
|
|
53
53
|
nav: ComputedRef<boolean>
|
|
54
|
+
segmented: ComputedRef<boolean>
|
|
54
55
|
selectable: ComputedRef<boolean>
|
|
55
56
|
selected: ComputedRef<string | number | null | undefined>
|
|
56
57
|
dividers: ComputedRef<boolean | 'inset'>
|
|
@@ -102,18 +103,23 @@ const iconContainerClasses: Record<string, string> = {
|
|
|
102
103
|
|
|
103
104
|
const isDense = computed(() => list?.dense.value ?? false)
|
|
104
105
|
const isNav = computed(() => list?.nav.value ?? false)
|
|
106
|
+
const isSegmented = computed(() => list?.segmented.value ?? false)
|
|
105
107
|
const hasDivider = computed(() => list?.dividers.value ?? false)
|
|
106
108
|
|
|
107
109
|
const containerClasses = computed(() => [
|
|
108
110
|
'mli-row relative flex w-full items-center gap-4 text-left',
|
|
109
|
-
|
|
111
|
+
isSegmented.value
|
|
112
|
+
? 'rounded-xl bg-surface-container px-4'
|
|
113
|
+
: isNav.value ? 'rounded-full px-4' : 'px-4',
|
|
110
114
|
isDense.value
|
|
111
115
|
? 'py-1'
|
|
112
116
|
: resolvedLines.value === 3 ? 'py-3' : 'py-2',
|
|
113
117
|
isClickable.value && !props.disabled && 'cursor-pointer transition-colors duration-150 hover:bg-on-surface/8 active:bg-on-surface/12',
|
|
114
|
-
isActive.value && (
|
|
118
|
+
isActive.value && (isSegmented.value
|
|
115
119
|
? 'bg-secondary-container text-on-secondary-container'
|
|
116
|
-
:
|
|
120
|
+
: isNav.value
|
|
121
|
+
? 'bg-secondary-container text-on-secondary-container'
|
|
122
|
+
: 'bg-on-surface/8'),
|
|
117
123
|
props.disabled && 'opacity-[0.38] pointer-events-none',
|
|
118
124
|
])
|
|
119
125
|
|
package/src/components/MMenu.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
2
|
+
import { computed, onMounted, onUnmounted, provide, ref } from 'vue'
|
|
3
3
|
|
|
4
4
|
const props = withDefaults(
|
|
5
5
|
defineProps<{
|
|
@@ -48,11 +48,15 @@ function close() {
|
|
|
48
48
|
open.value = false
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
provide('m-menu-close', close)
|
|
51
52
|
defineExpose({ close, open })
|
|
52
53
|
|
|
53
54
|
function onOutsideClick(e: MouseEvent) {
|
|
54
55
|
const t = e.target as Node
|
|
55
|
-
if (
|
|
56
|
+
if (triggerEl.value?.contains(t)) return
|
|
57
|
+
if (dropdownEl.value?.contains(t)) return
|
|
58
|
+
if ((t as Element).closest?.('.m3-submenu')) return
|
|
59
|
+
close()
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function onScroll(e: Event) {
|
|
@@ -102,11 +106,12 @@ const origin = computed(() =>
|
|
|
102
106
|
<div
|
|
103
107
|
v-if="open"
|
|
104
108
|
ref="dropdownEl"
|
|
105
|
-
class="fixed z-500 min-w-48 overflow-
|
|
109
|
+
class="fixed z-500 min-w-48 overflow-hidden rounded-lg bg-surface-container shadow-elevation-2"
|
|
106
110
|
:style="{ ...dropStyle, transformOrigin: origin }"
|
|
107
|
-
@click="close"
|
|
108
111
|
>
|
|
109
|
-
<
|
|
112
|
+
<div class="overflow-y-auto py-1" :style="{ maxHeight: dropStyle.maxHeight }">
|
|
113
|
+
<slot :close="close" />
|
|
114
|
+
</div>
|
|
110
115
|
</div>
|
|
111
116
|
</Transition>
|
|
112
117
|
</Teleport>
|
|
@@ -1,20 +1,103 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, inject, ref, useSlots } from 'vue'
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
4
|
|
|
5
|
-
const props = withDefaults(defineProps<{
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
icon?: string
|
|
7
|
+
to?: string | Record<string, any>
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
danger?: boolean
|
|
10
|
+
}>(), {
|
|
11
|
+
disabled: false,
|
|
12
|
+
danger: false,
|
|
13
|
+
})
|
|
6
14
|
|
|
15
|
+
const emit = defineEmits<{ click: [] }>()
|
|
16
|
+
|
|
17
|
+
const slots = useSlots()
|
|
7
18
|
const tag = computed(() => props.to ? 'RouterLink' : 'button')
|
|
19
|
+
const hasChildren = computed(() => !!slots.children)
|
|
20
|
+
const menuClose = inject<(() => void) | null>('m-menu-close', null)
|
|
21
|
+
|
|
22
|
+
function handleClick() {
|
|
23
|
+
if (props.disabled || hasChildren.value) return
|
|
24
|
+
emit('click')
|
|
25
|
+
menuClose?.()
|
|
26
|
+
}
|
|
27
|
+
const showSub = ref(false)
|
|
28
|
+
const itemEl = ref<HTMLElement>()
|
|
29
|
+
const subStyle = ref<Record<string, string>>({})
|
|
30
|
+
|
|
31
|
+
function onMouseEnter() {
|
|
32
|
+
if (!hasChildren.value || props.disabled) return
|
|
33
|
+
showSub.value = true
|
|
34
|
+
if (!itemEl.value) return
|
|
35
|
+
const rect = itemEl.value.getBoundingClientRect()
|
|
36
|
+
const right = rect.right + 200 > window.innerWidth
|
|
37
|
+
subStyle.value = {
|
|
38
|
+
position: 'fixed',
|
|
39
|
+
top: `${rect.top}px`,
|
|
40
|
+
...(right
|
|
41
|
+
? { right: `${window.innerWidth - rect.left}px` }
|
|
42
|
+
: { left: `${rect.right}px` }),
|
|
43
|
+
zIndex: '501',
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function onMouseLeave(e: MouseEvent) {
|
|
48
|
+
const related = e.relatedTarget as Element | null
|
|
49
|
+
if (related?.closest('.m3-submenu')) return
|
|
50
|
+
showSub.value = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function onSubLeave(e: MouseEvent) {
|
|
54
|
+
const related = e.relatedTarget as Element | null
|
|
55
|
+
if (related?.closest('.m3-submenu') || itemEl.value?.contains(related as Node)) return
|
|
56
|
+
showSub.value = false
|
|
57
|
+
}
|
|
8
58
|
</script>
|
|
9
59
|
|
|
10
60
|
<template>
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class="flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-left text-body-large text-on-surface hover:bg-on-surface/8"
|
|
61
|
+
<div
|
|
62
|
+
ref="itemEl"
|
|
63
|
+
@mouseenter="onMouseEnter"
|
|
64
|
+
@mouseleave="onMouseLeave"
|
|
16
65
|
>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
66
|
+
<component
|
|
67
|
+
:is="tag"
|
|
68
|
+
:to="to || undefined"
|
|
69
|
+
:type="to ? undefined : 'button'"
|
|
70
|
+
class="flex w-full items-center gap-3 px-4 py-2.5 text-left text-body-large transition-colors"
|
|
71
|
+
:class="[
|
|
72
|
+
disabled ? 'cursor-not-allowed opacity-[0.38] text-on-surface' : 'cursor-pointer',
|
|
73
|
+
!disabled && danger ? 'text-error hover:bg-error/8' : '',
|
|
74
|
+
!disabled && !danger ? 'text-on-surface hover:bg-on-surface/8' : '',
|
|
75
|
+
]"
|
|
76
|
+
:disabled="disabled || undefined"
|
|
77
|
+
@click="handleClick"
|
|
78
|
+
>
|
|
79
|
+
<MIcon v-if="icon" :name="icon" :size="20" class="shrink-0" :class="danger ? 'text-error' : 'text-on-surface-variant'" />
|
|
80
|
+
<span class="flex-1"><slot /></span>
|
|
81
|
+
<MIcon v-if="hasChildren" name="chevron_right" :size="18" class="shrink-0 text-on-surface-variant" />
|
|
82
|
+
</component>
|
|
83
|
+
|
|
84
|
+
<!-- Submenu -->
|
|
85
|
+
<Teleport v-if="hasChildren" to="body">
|
|
86
|
+
<Transition
|
|
87
|
+
enter-active-class="transition-opacity duration-100"
|
|
88
|
+
enter-from-class="opacity-0"
|
|
89
|
+
leave-active-class="transition-opacity duration-75"
|
|
90
|
+
leave-to-class="opacity-0"
|
|
91
|
+
>
|
|
92
|
+
<div
|
|
93
|
+
v-if="showSub"
|
|
94
|
+
class="m3-submenu min-w-44 overflow-hidden rounded-lg bg-surface-container py-1 shadow-elevation-2"
|
|
95
|
+
:style="subStyle"
|
|
96
|
+
@mouseleave="onSubLeave"
|
|
97
|
+
>
|
|
98
|
+
<slot name="children" />
|
|
99
|
+
</div>
|
|
100
|
+
</Transition>
|
|
101
|
+
</Teleport>
|
|
102
|
+
</div>
|
|
20
103
|
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, useId, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
|
+
import MIconButton from './MIconButton.vue'
|
|
4
5
|
import MCheckbox from './MCheckbox.vue'
|
|
5
6
|
import { useFieldBg } from '../composables/useFieldBg'
|
|
6
7
|
import { useLocale } from '../composables/useLocale'
|
|
@@ -13,6 +14,7 @@ const props = withDefaults(
|
|
|
13
14
|
label?: string
|
|
14
15
|
placeholder?: string
|
|
15
16
|
variant?: 'filled' | 'outlined'
|
|
17
|
+
mode?: 'docked' | 'modal'
|
|
16
18
|
disabled?: boolean
|
|
17
19
|
error?: string
|
|
18
20
|
hint?: string
|
|
@@ -27,6 +29,7 @@ const props = withDefaults(
|
|
|
27
29
|
{
|
|
28
30
|
modelValue: () => [],
|
|
29
31
|
variant: 'filled',
|
|
32
|
+
mode: 'docked',
|
|
30
33
|
disabled: false,
|
|
31
34
|
required: false,
|
|
32
35
|
clearable: false,
|
|
@@ -40,6 +43,8 @@ const emit = defineEmits<{ 'update:modelValue': [unknown[]] }>()
|
|
|
40
43
|
const locale = useLocale()
|
|
41
44
|
const id = useId()
|
|
42
45
|
const open = ref(false)
|
|
46
|
+
const modalOpen = ref(false)
|
|
47
|
+
const modalSearch = ref('')
|
|
43
48
|
const search = ref('')
|
|
44
49
|
const highlightIndex = ref(-1)
|
|
45
50
|
const fieldEl = ref<HTMLElement | null>(null)
|
|
@@ -126,8 +131,23 @@ function computeDropPos() {
|
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
const modalFilteredOptions = computed(() => {
|
|
135
|
+
let opts = props.options
|
|
136
|
+
if (props.hideSelected) {
|
|
137
|
+
opts = opts.filter((o) => !includes(props.modelValue, o.value))
|
|
138
|
+
}
|
|
139
|
+
if (!modalSearch.value) return opts
|
|
140
|
+
const q = modalSearch.value.toLowerCase()
|
|
141
|
+
return opts.filter((o) => o.label.toLowerCase().includes(q))
|
|
142
|
+
})
|
|
143
|
+
|
|
129
144
|
function openDropdown() {
|
|
130
145
|
if (props.disabled || open.value) return
|
|
146
|
+
if (props.mode === 'modal') {
|
|
147
|
+
modalOpen.value = true
|
|
148
|
+
modalSearch.value = ''
|
|
149
|
+
return
|
|
150
|
+
}
|
|
131
151
|
computeDropPos()
|
|
132
152
|
open.value = true
|
|
133
153
|
highlightIndex.value = -1
|
|
@@ -135,15 +155,22 @@ function openDropdown() {
|
|
|
135
155
|
|
|
136
156
|
function closeDropdown() {
|
|
137
157
|
open.value = false
|
|
158
|
+
modalOpen.value = false
|
|
138
159
|
search.value = ''
|
|
160
|
+
modalSearch.value = ''
|
|
139
161
|
highlightIndex.value = -1
|
|
140
162
|
}
|
|
141
163
|
|
|
164
|
+
watch(modalOpen, (v) => {
|
|
165
|
+
document.body.style.overflow = v ? 'hidden' : ''
|
|
166
|
+
})
|
|
167
|
+
|
|
142
168
|
function onInputFocus() {
|
|
143
169
|
openDropdown()
|
|
144
170
|
}
|
|
145
171
|
|
|
146
172
|
function onInputBlur(e: FocusEvent) {
|
|
173
|
+
if (modalOpen.value) return
|
|
147
174
|
const related = e.relatedTarget as Node | null
|
|
148
175
|
if (dropdownEl.value?.contains(related) || fieldEl.value?.contains(related)) return
|
|
149
176
|
closeDropdown()
|
|
@@ -157,6 +184,7 @@ function onInput(e: Event) {
|
|
|
157
184
|
}
|
|
158
185
|
|
|
159
186
|
function onOutsideClick(e: MouseEvent) {
|
|
187
|
+
if (modalOpen.value) return
|
|
160
188
|
const t = e.target as Node
|
|
161
189
|
if (!fieldEl.value?.contains(t) && !dropdownEl.value?.contains(t)) closeDropdown()
|
|
162
190
|
}
|
|
@@ -164,13 +192,7 @@ function onOutsideClick(e: MouseEvent) {
|
|
|
164
192
|
function onScroll(e: Event) {
|
|
165
193
|
if (!open.value) return
|
|
166
194
|
if (dropdownEl.value?.contains(e.target as Node)) return
|
|
167
|
-
|
|
168
|
-
const rect = fieldEl.value.getBoundingClientRect()
|
|
169
|
-
if (rect.bottom < 0 || rect.top > window.innerHeight) {
|
|
170
|
-
closeDropdown()
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
computeDropPos()
|
|
195
|
+
closeDropdown()
|
|
174
196
|
}
|
|
175
197
|
|
|
176
198
|
function onKeydown(e: KeyboardEvent) {
|
|
@@ -235,6 +257,7 @@ onMounted(() => {
|
|
|
235
257
|
onUnmounted(() => {
|
|
236
258
|
document.removeEventListener('mousedown', onOutsideClick)
|
|
237
259
|
window.removeEventListener('scroll', onScroll, true)
|
|
260
|
+
document.body.style.overflow = ''
|
|
238
261
|
})
|
|
239
262
|
|
|
240
263
|
watch(filteredOptions, () => {
|
|
@@ -388,9 +411,10 @@ const labelClasses = computed(() => {
|
|
|
388
411
|
<!-- Arrow icon -->
|
|
389
412
|
<div class="pointer-events-none absolute right-2 top-4">
|
|
390
413
|
<MIcon
|
|
391
|
-
|
|
414
|
+
name="arrow_drop_down"
|
|
392
415
|
:size="24"
|
|
393
416
|
class="text-on-surface-variant transition-transform duration-200"
|
|
417
|
+
:class="open || modalOpen ? 'rotate-180' : ''"
|
|
394
418
|
/>
|
|
395
419
|
</div>
|
|
396
420
|
</div>
|
|
@@ -399,7 +423,7 @@ const labelClasses = computed(() => {
|
|
|
399
423
|
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
400
424
|
</div>
|
|
401
425
|
|
|
402
|
-
<!--
|
|
426
|
+
<!-- Docked dropdown -->
|
|
403
427
|
<Teleport to="body">
|
|
404
428
|
<Transition
|
|
405
429
|
enter-active-class="transition-[opacity,transform] duration-150"
|
|
@@ -410,7 +434,7 @@ const labelClasses = computed(() => {
|
|
|
410
434
|
leave-to-class="opacity-0 -translate-y-1 scale-[0.98]"
|
|
411
435
|
>
|
|
412
436
|
<div
|
|
413
|
-
v-if="open"
|
|
437
|
+
v-if="open && mode === 'docked'"
|
|
414
438
|
ref="dropdownEl"
|
|
415
439
|
class="fixed z-500 max-h-60 overflow-auto rounded-sm bg-surface-container shadow-elevation-2"
|
|
416
440
|
:style="dropPos"
|
|
@@ -442,5 +466,76 @@ const labelClasses = computed(() => {
|
|
|
442
466
|
</div>
|
|
443
467
|
</div>
|
|
444
468
|
</Transition>
|
|
469
|
+
|
|
470
|
+
<!-- Modal -->
|
|
471
|
+
<Transition name="m3-mac-modal">
|
|
472
|
+
<div
|
|
473
|
+
v-if="modalOpen && mode === 'modal'"
|
|
474
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
|
|
475
|
+
@click.self="closeDropdown"
|
|
476
|
+
>
|
|
477
|
+
<div class="mac-modal-box flex max-h-[80vh] w-full max-w-sm flex-col overflow-hidden rounded-[28px] bg-surface-container-high shadow-elevation-3">
|
|
478
|
+
<div class="flex items-center justify-between px-6 pt-6 pb-4">
|
|
479
|
+
<h2 class="text-headline-small text-on-surface">{{ label || placeholder }}</h2>
|
|
480
|
+
<MIconButton icon="close" :label="locale.close" @click="closeDropdown" />
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
<div class="px-6 pb-3">
|
|
484
|
+
<div class="flex items-center gap-2 rounded-full bg-surface-container-highest px-3 py-2">
|
|
485
|
+
<MIcon name="search" :size="16" class="shrink-0 text-on-surface-variant" />
|
|
486
|
+
<input
|
|
487
|
+
v-model="modalSearch"
|
|
488
|
+
type="text"
|
|
489
|
+
:placeholder="locale.search"
|
|
490
|
+
class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant"
|
|
491
|
+
/>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
<div class="h-px bg-outline-variant" />
|
|
496
|
+
|
|
497
|
+
<div class="flex-1 overflow-y-auto py-2">
|
|
498
|
+
<label
|
|
499
|
+
v-for="(opt, i) in modalFilteredOptions"
|
|
500
|
+
:key="i"
|
|
501
|
+
class="flex cursor-pointer items-center gap-4 px-6 py-3 transition-colors hover:bg-on-surface/8"
|
|
502
|
+
:class="opt.disabled ? 'cursor-not-allowed opacity-38' : ''"
|
|
503
|
+
>
|
|
504
|
+
<MCheckbox
|
|
505
|
+
:model-value="includes(modelValue, opt.value)"
|
|
506
|
+
:disabled="opt.disabled"
|
|
507
|
+
@update:model-value="!opt.disabled && toggle(opt.value)"
|
|
508
|
+
/>
|
|
509
|
+
<span class="text-body-large text-on-surface">{{ opt.label }}</span>
|
|
510
|
+
</label>
|
|
511
|
+
<p
|
|
512
|
+
v-if="!modalFilteredOptions.length"
|
|
513
|
+
class="px-6 py-4 text-center text-body-medium text-on-surface-variant"
|
|
514
|
+
>
|
|
515
|
+
{{ resolvedNoResultsText }}
|
|
516
|
+
</p>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</Transition>
|
|
445
521
|
</Teleport>
|
|
446
522
|
</template>
|
|
523
|
+
|
|
524
|
+
<style scoped>
|
|
525
|
+
.m3-mac-modal-enter-active,
|
|
526
|
+
.m3-mac-modal-leave-active {
|
|
527
|
+
transition: opacity 0.15s ease;
|
|
528
|
+
}
|
|
529
|
+
.m3-mac-modal-enter-from,
|
|
530
|
+
.m3-mac-modal-leave-to {
|
|
531
|
+
opacity: 0;
|
|
532
|
+
}
|
|
533
|
+
.m3-mac-modal-enter-active .mac-modal-box,
|
|
534
|
+
.m3-mac-modal-leave-active .mac-modal-box {
|
|
535
|
+
transition: transform 0.15s ease;
|
|
536
|
+
}
|
|
537
|
+
.m3-mac-modal-enter-from .mac-modal-box,
|
|
538
|
+
.m3-mac-modal-leave-to .mac-modal-box {
|
|
539
|
+
transform: scale(0.95);
|
|
540
|
+
}
|
|
541
|
+
</style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, useId, onMounted, onUnmounted, nextTick } from 'vue'
|
|
2
|
+
import { computed, ref, useId, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
|
+
import MIconButton from './MIconButton.vue'
|
|
4
5
|
import MCheckbox from './MCheckbox.vue'
|
|
5
6
|
import { useFieldBg } from '../composables/useFieldBg'
|
|
6
7
|
import { useLocale } from '../composables/useLocale'
|
|
@@ -20,6 +21,7 @@ const props = withDefaults(
|
|
|
20
21
|
label?: string
|
|
21
22
|
placeholder?: string
|
|
22
23
|
variant?: 'filled' | 'outlined'
|
|
24
|
+
mode?: 'docked' | 'modal'
|
|
23
25
|
disabled?: boolean
|
|
24
26
|
error?: string
|
|
25
27
|
hint?: string
|
|
@@ -36,6 +38,7 @@ const props = withDefaults(
|
|
|
36
38
|
{
|
|
37
39
|
modelValue: () => [],
|
|
38
40
|
variant: 'filled',
|
|
41
|
+
mode: 'docked',
|
|
39
42
|
disabled: false,
|
|
40
43
|
required: false,
|
|
41
44
|
searchable: true,
|
|
@@ -49,6 +52,8 @@ const emit = defineEmits<{ 'update:modelValue': [unknown[]] }>()
|
|
|
49
52
|
|
|
50
53
|
const id = useId()
|
|
51
54
|
const open = ref(false)
|
|
55
|
+
const modalOpen = ref(false)
|
|
56
|
+
const modalSearch = ref('')
|
|
52
57
|
const search = ref('')
|
|
53
58
|
const fieldEl = ref<HTMLElement | null>(null)
|
|
54
59
|
const { resolvedFieldBg } = useFieldBg(fieldEl, () => props.fieldBg)
|
|
@@ -128,6 +133,11 @@ function computeDropPos() {
|
|
|
128
133
|
|
|
129
134
|
async function openDropdown() {
|
|
130
135
|
if (props.disabled) return
|
|
136
|
+
if (props.mode === 'modal') {
|
|
137
|
+
modalOpen.value = true
|
|
138
|
+
modalSearch.value = ''
|
|
139
|
+
return
|
|
140
|
+
}
|
|
131
141
|
computeDropPos()
|
|
132
142
|
open.value = true
|
|
133
143
|
search.value = ''
|
|
@@ -137,10 +147,27 @@ async function openDropdown() {
|
|
|
137
147
|
|
|
138
148
|
function close() {
|
|
139
149
|
open.value = false
|
|
150
|
+
modalOpen.value = false
|
|
140
151
|
search.value = ''
|
|
152
|
+
modalSearch.value = ''
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
const modalFilteredOptions = computed(() => {
|
|
156
|
+
let opts = props.options
|
|
157
|
+
if (props.hideSelected) {
|
|
158
|
+
opts = opts.filter((o) => !includes(props.modelValue, o.value))
|
|
159
|
+
}
|
|
160
|
+
if (!modalSearch.value) return opts
|
|
161
|
+
const q = modalSearch.value.toLowerCase()
|
|
162
|
+
return opts.filter((o) => o.label.toLowerCase().includes(q))
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
watch(modalOpen, (v) => {
|
|
166
|
+
document.body.style.overflow = v ? 'hidden' : ''
|
|
167
|
+
})
|
|
168
|
+
|
|
143
169
|
function onOutsideClick(e: MouseEvent) {
|
|
170
|
+
if (modalOpen.value) return
|
|
144
171
|
const t = e.target as Node
|
|
145
172
|
if (!fieldEl.value?.contains(t) && !dropdownEl.value?.contains(t)) close()
|
|
146
173
|
}
|
|
@@ -148,13 +175,7 @@ function onOutsideClick(e: MouseEvent) {
|
|
|
148
175
|
function onScroll(e: Event) {
|
|
149
176
|
if (!open.value) return
|
|
150
177
|
if (dropdownEl.value?.contains(e.target as Node)) return
|
|
151
|
-
|
|
152
|
-
const rect = fieldEl.value.getBoundingClientRect()
|
|
153
|
-
if (rect.bottom < 0 || rect.top > window.innerHeight) {
|
|
154
|
-
close()
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
computeDropPos()
|
|
178
|
+
close()
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
onMounted(() => {
|
|
@@ -164,6 +185,7 @@ onMounted(() => {
|
|
|
164
185
|
onUnmounted(() => {
|
|
165
186
|
document.removeEventListener('mousedown', onOutsideClick)
|
|
166
187
|
window.removeEventListener('scroll', onScroll, true)
|
|
188
|
+
document.body.style.overflow = ''
|
|
167
189
|
})
|
|
168
190
|
|
|
169
191
|
const triggerClasses = computed(() => {
|
|
@@ -298,9 +320,10 @@ const labelClasses = computed(() => {
|
|
|
298
320
|
|
|
299
321
|
<div class="pointer-events-none absolute right-2 top-4">
|
|
300
322
|
<MIcon
|
|
301
|
-
|
|
323
|
+
name="arrow_drop_down"
|
|
302
324
|
:size="24"
|
|
303
|
-
class="text-on-surface-variant"
|
|
325
|
+
class="text-on-surface-variant transition-transform duration-200"
|
|
326
|
+
:class="open || modalOpen ? 'rotate-180' : ''"
|
|
304
327
|
/>
|
|
305
328
|
</div>
|
|
306
329
|
</div>
|
|
@@ -309,7 +332,7 @@ const labelClasses = computed(() => {
|
|
|
309
332
|
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
310
333
|
</div>
|
|
311
334
|
|
|
312
|
-
<!--
|
|
335
|
+
<!-- Docked dropdown -->
|
|
313
336
|
<Teleport to="body">
|
|
314
337
|
<Transition
|
|
315
338
|
enter-active-class="transition-[opacity,transform] duration-150"
|
|
@@ -320,12 +343,11 @@ const labelClasses = computed(() => {
|
|
|
320
343
|
leave-to-class="opacity-0 -translate-y-1 scale-[0.98]"
|
|
321
344
|
>
|
|
322
345
|
<div
|
|
323
|
-
v-if="open"
|
|
346
|
+
v-if="open && mode === 'docked'"
|
|
324
347
|
ref="dropdownEl"
|
|
325
348
|
class="fixed z-500 max-h-60 overflow-auto rounded-sm bg-surface-container shadow-elevation-2"
|
|
326
349
|
:style="dropPos"
|
|
327
350
|
>
|
|
328
|
-
<!-- Search -->
|
|
329
351
|
<div v-if="searchable" class="sticky top-0 bg-surface-container px-3 py-2">
|
|
330
352
|
<div class="flex items-center gap-2 rounded-full bg-surface-container-high px-3 py-1.5">
|
|
331
353
|
<MIcon name="search" :size="16" class="shrink-0 text-on-surface-variant" />
|
|
@@ -361,5 +383,76 @@ const labelClasses = computed(() => {
|
|
|
361
383
|
</div>
|
|
362
384
|
</div>
|
|
363
385
|
</Transition>
|
|
386
|
+
|
|
387
|
+
<!-- Modal -->
|
|
388
|
+
<Transition name="m3-ms-modal">
|
|
389
|
+
<div
|
|
390
|
+
v-if="modalOpen && mode === 'modal'"
|
|
391
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
|
|
392
|
+
@click.self="close"
|
|
393
|
+
>
|
|
394
|
+
<div class="ms-modal-box flex max-h-[80vh] w-full max-w-sm flex-col overflow-hidden rounded-[28px] bg-surface-container-high shadow-elevation-3">
|
|
395
|
+
<div class="flex items-center justify-between px-6 pt-6 pb-4">
|
|
396
|
+
<h2 class="text-headline-small text-on-surface">{{ label || placeholder }}</h2>
|
|
397
|
+
<MIconButton icon="close" :label="locale.close" @click="close" />
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div v-if="searchable" class="px-6 pb-3">
|
|
401
|
+
<div class="flex items-center gap-2 rounded-full bg-surface-container-highest px-3 py-2">
|
|
402
|
+
<MIcon name="search" :size="16" class="shrink-0 text-on-surface-variant" />
|
|
403
|
+
<input
|
|
404
|
+
v-model="modalSearch"
|
|
405
|
+
type="text"
|
|
406
|
+
:placeholder="searchPlaceholder ?? locale.search"
|
|
407
|
+
class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant"
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<div class="h-px bg-outline-variant" />
|
|
413
|
+
|
|
414
|
+
<div class="flex-1 overflow-y-auto py-2">
|
|
415
|
+
<label
|
|
416
|
+
v-for="(opt, i) in modalFilteredOptions"
|
|
417
|
+
:key="i"
|
|
418
|
+
class="flex cursor-pointer items-center gap-4 px-6 py-3 transition-colors hover:bg-on-surface/8"
|
|
419
|
+
:class="opt.disabled ? 'cursor-not-allowed opacity-38' : ''"
|
|
420
|
+
>
|
|
421
|
+
<MCheckbox
|
|
422
|
+
:model-value="includes(modelValue, opt.value)"
|
|
423
|
+
:disabled="opt.disabled"
|
|
424
|
+
@update:model-value="!opt.disabled && toggle(opt.value)"
|
|
425
|
+
/>
|
|
426
|
+
<span class="text-body-large text-on-surface">{{ opt.label }}</span>
|
|
427
|
+
</label>
|
|
428
|
+
<p
|
|
429
|
+
v-if="!modalFilteredOptions.length"
|
|
430
|
+
class="px-6 py-4 text-center text-body-medium text-on-surface-variant"
|
|
431
|
+
>
|
|
432
|
+
{{ noResultsText ?? locale.noResults }}
|
|
433
|
+
</p>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</Transition>
|
|
364
438
|
</Teleport>
|
|
365
439
|
</template>
|
|
440
|
+
|
|
441
|
+
<style scoped>
|
|
442
|
+
.m3-ms-modal-enter-active,
|
|
443
|
+
.m3-ms-modal-leave-active {
|
|
444
|
+
transition: opacity 0.15s ease;
|
|
445
|
+
}
|
|
446
|
+
.m3-ms-modal-enter-from,
|
|
447
|
+
.m3-ms-modal-leave-to {
|
|
448
|
+
opacity: 0;
|
|
449
|
+
}
|
|
450
|
+
.m3-ms-modal-enter-active .ms-modal-box,
|
|
451
|
+
.m3-ms-modal-leave-active .ms-modal-box {
|
|
452
|
+
transition: transform 0.15s ease;
|
|
453
|
+
}
|
|
454
|
+
.m3-ms-modal-enter-from .ms-modal-box,
|
|
455
|
+
.m3-ms-modal-leave-to .ms-modal-box {
|
|
456
|
+
transform: scale(0.95);
|
|
457
|
+
}
|
|
458
|
+
</style>
|