|
<script lang="ts"> |
|
import { computed, defineComponent, ref } from 'vue' |
|
import { SharedData } from '@vue-devtools/shared-utils' |
|
import { useOrientation } from '@front/features/layout/orientation' |
|
|
|
export default defineComponent({ |
|
props: { |
|
items: { |
|
type: Array, |
|
required: true, |
|
}, |
|
|
|
selectedItem: { |
|
type: Object, |
|
default: () => ({}), |
|
}, |
|
|
|
optionIcon: { |
|
type: String, |
|
default: null, |
|
}, |
|
}, |
|
emits: ['select'], |
|
setup(props, { emit }) { |
|
|
|
|
|
const isShown = ref(false) |
|
const isShowApplied = ref(false) |
|
|
|
|
|
|
|
|
|
|
|
const showDelayEnabled = ref(true) |
|
|
|
let disabled = false |
|
let toggleCloseEnabled = true |
|
let toggleCloseTimer = null |
|
|
|
let pendingOperation = null |
|
let operationTimer = null |
|
|
|
function queueOperation(type, delay) { |
|
if (disabled) { |
|
return |
|
} |
|
pendingOperation = type |
|
clearTimeout(operationTimer) |
|
operationTimer = setTimeout(() => applyOperation(), delay) |
|
} |
|
|
|
function applyOperation() { |
|
if (pendingOperation) { |
|
pendingOperation() |
|
} |
|
pendingOperation = null |
|
clearTimeout(operationTimer) |
|
} |
|
|
|
function queueOpen(delay = true) { |
|
queueOperation(() => { |
|
isShown.value = true |
|
|
|
toggleCloseEnabled = false |
|
clearTimeout(toggleCloseTimer) |
|
toggleCloseTimer = setTimeout(() => { |
|
toggleCloseEnabled = true |
|
}, 500) |
|
}, delay ? 250 : 1) |
|
} |
|
|
|
function queueClose(delay = true) { |
|
toggleCloseEnabled = false |
|
clearTimeout(toggleCloseTimer) |
|
queueOperation(() => { |
|
isShown.value = false |
|
}, delay ? 300 : 1) |
|
} |
|
|
|
function toggle() { |
|
if (isShown.value) { |
|
if (toggleCloseEnabled) { |
|
queueClose(false) |
|
} |
|
} |
|
else { |
|
|
|
|
|
|
|
|
|
queueOpen(false) |
|
} |
|
} |
|
|
|
|
|
|
|
function select(item) { |
|
disabled = true |
|
emit('select', item) |
|
|
|
setTimeout(() => { |
|
disabled = false |
|
}, 500) |
|
} |
|
|
|
const selectedIndex = computed(() => props.items.indexOf(props.selectedItem)) |
|
|
|
function selectNext() { |
|
const index = selectedIndex.value + 1 |
|
if (index < props.items.length) { |
|
select(props.items[index]) |
|
} |
|
} |
|
|
|
function selectPrevious() { |
|
const index = selectedIndex.value - 1 |
|
if (index >= 0) { |
|
select(props.items[index]) |
|
} |
|
} |
|
|
|
let wheelEnabled = true |
|
|
|
function onMouseWheel(e: WheelEvent) { |
|
if (!wheelEnabled) { |
|
return |
|
} |
|
|
|
if (e.deltaY > 0) { |
|
selectNext() |
|
} |
|
else { |
|
selectPrevious() |
|
} |
|
|
|
if (SharedData.menuStepScrolling) { |
|
wheelEnabled = false |
|
setTimeout(() => { |
|
wheelEnabled = true |
|
}, 300) |
|
} |
|
} |
|
|
|
|
|
|
|
const { orientation } = useOrientation() |
|
|
|
return { |
|
isShown, |
|
isShowApplied, |
|
showDelayEnabled, |
|
queueOpen, |
|
queueClose, |
|
toggle, |
|
select, |
|
orientation, |
|
onMouseWheel, |
|
} |
|
}, |
|
}) |
|
</script> |
|
|
|
<template> |
|
<VueDropdown |
|
v-model="isShown" |
|
:placement="orientation === 'landscape' ? 'right-start' : 'bottom-start'" |
|
:triggers="[]" |
|
:offset="[0, 0]" |
|
:delay="0" |
|
:auto-hide="false" |
|
@apply-show="isShowApplied = true" |
|
@apply-hide="isShowApplied = false" |
|
> |
|
<template #trigger> |
|
<div |
|
@mouseenter="queueOpen()" |
|
@mouseleave="queueClose()" |
|
@wheel="onMouseWheel" |
|
@click.capture="toggle()" |
|
> |
|
<slot name="trigger"> |
|
<VueButton |
|
class="flat" |
|
:icon-left="selectedItem.icon" |
|
:icon-right="orientation === 'landscape' ? 'arrow_drop_down' : null" |
|
:class="{ |
|
'icon-button': orientation === 'portrait', |
|
}" |
|
> |
|
<template v-if="orientation === 'landscape'"> |
|
{{ selectedItem.label }} |
|
</template> |
|
</VueButton> |
|
</slot> |
|
</div> |
|
</template> |
|
|
|
<div> |
|
<div |
|
class="flex flex-col" |
|
@mouseenter="queueOpen()" |
|
@mouseleave="queueClose()" |
|
@wheel="onMouseWheel" |
|
> |
|
<slot name="before" /> |
|
|
|
<VueDropdownButton |
|
v-for="(item, index) of items" |
|
:key="index" |
|
:icon-left="item.icon || optionIcon" |
|
:class="{ |
|
selected: selectedItem === item, |
|
}" |
|
@click="select(item)" |
|
> |
|
<slot :item="item"> |
|
{{ item.label }} |
|
</slot> |
|
</VueDropdownButton> |
|
|
|
<div |
|
v-if="$shared.showMenuScrollTip" |
|
class="text-xs flex items-center space-x-2 text-gray-500 pl-4 pr-1 py-1 border-t border-gray-200 dark:border-gray-700 group" |
|
> |
|
<span>Scroll to switch</span> |
|
<VueIcon icon="mouse" /> |
|
|
|
<span class="flex-1" /> |
|
|
|
<VueButton |
|
class="flex-none icon-button flat invisible group-hover:visible" |
|
icon-left="close" |
|
@click="$shared.showMenuScrollTip = false" |
|
/> |
|
</div> |
|
</div> |
|
</div> |
|
</VueDropdown> |
|
</template> |
|
|
|
<style lang="postcss" scoped> |
|
.selected { |
|
@apply bg-green-100 text-green-700 !important; |
|
|
|
.vue-ui-dark-mode & { |
|
@apply bg-gray-700 text-gray-100 !important; |
|
} |
|
|
|
:deep(svg) { |
|
fill: currentColor !important; |
|
} |
|
} |
|
|
|
.vue-ui-dropdown-button { |
|
min-height: 32px; |
|
height: auto; |
|
padding-top: 6px; |
|
padding-bottom: 6px; |
|
|
|
:deep(.default-slot) { |
|
flex: 1; |
|
} |
|
} |
|
</style> |
|
|