feat: use OnClickOutside from vueuse

This commit is contained in:
Phan An 2024-05-08 17:11:32 +08:00
parent d4be2e3b22
commit 911410bdfd
22 changed files with 139 additions and 127 deletions

View file

@ -51,6 +51,7 @@
"@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^4.11.1",
"@vitejs/plugin-vue": "^5.0.4",
"@vueuse/components": "^10.9.0",
"@vueuse/core": "^10.9.0",
"@vueuse/integrations": "^10.9.0",
"add": "^2.0.6",

View file

@ -103,7 +103,6 @@ export default abstract class UnitTestCase {
return render(component, deepMerge({
global: {
directives: {
'koel-clickaway': {},
'koel-focus': {},
'koel-tooltip': {},
'koel-hide-broken-icon': {},

View file

@ -1,5 +1,5 @@
import { createApp } from 'vue'
import { clickaway, focus, hideBrokenIcon, overflowFade, tooltip } from '@/directives'
import { focus, hideBrokenIcon, overflowFade, tooltip } from '@/directives'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { RouterKey } from '@/symbols'
import { routes } from '@/config'
@ -12,7 +12,6 @@ createApp(App)
.component('Icon', FontAwesomeIcon)
.component('IconLayers', FontAwesomeLayers)
.directive('koel-focus', focus)
.directive('koel-clickaway', clickaway)
.directive('koel-tooltip', tooltip)
.directive('koel-hide-broken-icon', hideBrokenIcon)
.directive('koel-overflow-fade', overflowFade)
@ -24,4 +23,4 @@ createApp(App)
*/
.mount('#app')
navigator.serviceWorker?.register('./sw.js')
navigator.serviceWorker.register('./sw.js')

View file

@ -1,5 +1,5 @@
<template>
<ContextMenuBase ref="base" data-testid="album-context-menu" extra-class="album-menu">
<ContextMenu ref="base" data-testid="album-context-menu" extra-class="album-menu">
<template v-if="album">
<li @click="play">Play All</li>
<li @click="shuffle">Shuffle All</li>
@ -11,7 +11,7 @@
<li @click="download">Download</li>
</template>
</template>
</ContextMenuBase>
</ContextMenu>
</template>
<script lang="ts" setup>
@ -22,7 +22,7 @@ import { useContextMenu, useRouter } from '@/composables'
import { eventBus } from '@/utils'
const { go } = useRouter()
const { base, ContextMenuBase, open, trigger } = useContextMenu()
const { base, ContextMenu, open, trigger } = useContextMenu()
const album = ref<Album>()
const allowDownload = toRef(commonStore.state, 'allows_download')

View file

@ -1,5 +1,5 @@
<template>
<ContextMenuBase ref="base" data-testid="artist-context-menu" extra-class="artist-menu">
<ContextMenu ref="base" data-testid="artist-context-menu" extra-class="artist-menu">
<template v-if="artist">
<li @click="play">Play All</li>
<li @click="shuffle">Shuffle All</li>
@ -12,7 +12,7 @@
<li @click="download">Download</li>
</template>
</template>
</ContextMenuBase>
</ContextMenu>
</template>
<script lang="ts" setup>
@ -23,7 +23,7 @@ import { useContextMenu, useRouter } from '@/composables'
import { eventBus } from '@/utils'
const { go } = useRouter()
const { base, ContextMenuBase, open, trigger } = useContextMenu()
const { base, ContextMenu, open, trigger } = useContextMenu()
const artist = ref<Artist>()
const allowDownload = toRef(commonStore.state, 'allows_download')

View file

@ -1,31 +1,33 @@
<template>
<nav
v-koel-clickaway="closeIfMobile"
:class="{ collapsed: !expanded, 'tmp-showing': tmpShowing, showing: mobileShowing }"
class="flex flex-col fixed md:relative w-full md:w-k-sidebar-width z-10"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<section class="search-wrapper p-6">
<SearchForm />
</section>
<OnClickOutside @trigger="closeIfMobile">
<nav
:class="{ collapsed: !expanded, 'tmp-showing': tmpShowing, showing: mobileShowing }"
class="flex flex-col fixed md:relative w-full md:w-k-sidebar-width z-10"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<section class="search-wrapper p-6">
<SearchForm />
</section>
<section v-koel-overflow-fade class="pt-2 pb-10 overflow-y-auto space-y-8">
<SidebarYourMusicSection />
<SidebarPlaylistsSection />
<SidebarManageSection v-if="showManageSection" />
</section>
<section v-koel-overflow-fade class="pt-2 pb-10 overflow-y-auto space-y-8">
<SidebarYourMusicSection />
<SidebarPlaylistsSection />
<SidebarManageSection v-if="showManageSection" />
</section>
<section v-if="!isPlus && isAdmin" class="p-6">
<BtnUpgradeToPlus />
</section>
<section v-if="!isPlus && isAdmin" class="p-6">
<BtnUpgradeToPlus />
</section>
<SidebarToggleButton v-model="expanded" />
</nav>
<SidebarToggleButton v-model="expanded" />
</nav>
</OnClickOutside>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { OnClickOutside } from '@vueuse/components'
import { eventBus } from '@/utils'
import { useAuthorization, useKoelPlus, useLocalStorage, useRouter, useUpload } from '@/composables'

View file

@ -1,11 +1,11 @@
<template>
<ContextMenuBase ref="base">
<ContextMenu ref="base">
<li data-testid="playlist-context-menu-create-simple" @click="onItemClicked('new-playlist')">New Playlist</li>
<li data-testid="playlist-context-menu-create-smart" @click="onItemClicked('new-smart-playlist')">
New Smart Playlist
</li>
<li data-testid="playlist-context-menu-create-folder" @click="onItemClicked('new-folder')">New Folder</li>
</ContextMenuBase>
</ContextMenu>
</template>
<script lang="ts" setup>
@ -13,9 +13,11 @@ import { useContextMenu } from '@/composables'
import { eventBus } from '@/utils'
import { Events } from '@/config'
const { base, ContextMenuBase, open, trigger } = useContextMenu()
const { base, ContextMenu, open, trigger } = useContextMenu()
const actionToEventMap: Record<string, keyof Events> = {
type Action = 'new-playlist' | 'new-smart-playlist' | 'new-folder'
const actionToEventMap: Record<Action, keyof Events> = {
'new-playlist': 'MODAL_SHOW_CREATE_PLAYLIST_FORM',
'new-smart-playlist': 'MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM',
'new-folder': 'MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM'

View file

@ -1,5 +1,5 @@
<template>
<ContextMenuBase ref="base">
<ContextMenu ref="base">
<li @click="play">Play</li>
<li @click="shuffle">Shuffle</li>
<li @click="addToQueue">Add to Queue</li>
@ -10,7 +10,7 @@
</template>
<li v-if="canEditPlaylist" @click="edit">Edit</li>
<li v-if="canEditPlaylist" @click="destroy">Delete</li>
</ContextMenuBase>
</ContextMenu>
</template>
<script lang="ts" setup>
@ -20,7 +20,7 @@ import { useContextMenu, useKoelPlus, useMessageToaster, usePolicies, useRouter
import { playbackService } from '@/services'
import { queueStore, songStore } from '@/stores'
const { base, ContextMenuBase, open, trigger } = useContextMenu()
const { base, ContextMenu, open, trigger } = useContextMenu()
const { go } = useRouter()
const { toastWarning, toastSuccess } = useMessageToaster()
const { isPlus } = useKoelPlus()

View file

@ -1,5 +1,5 @@
<template>
<ContextMenuBase ref="base">
<ContextMenu ref="base">
<template v-if="folder">
<template v-if="playable">
<li @click="play">Play All</li>
@ -12,7 +12,7 @@
<li @click="rename">Rename</li>
<li @click="destroy">Delete</li>
</template>
</ContextMenuBase>
</ContextMenu>
</template>
<script lang="ts" setup>
@ -22,7 +22,7 @@ import { playlistStore, songStore } from '@/stores'
import { playbackService } from '@/services'
import { useContextMenu, useMessageToaster, useRouter } from '@/composables'
const { base, ContextMenuBase, open, trigger } = useContextMenu()
const { base, ContextMenu, open, trigger } = useContextMenu()
const { go } = useRouter()
const { toastWarning } = useMessageToaster()

View file

@ -64,7 +64,7 @@ import { useSongMenuMethods } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
const props = defineProps<{ songs: Song[], config: AddToMenuConfig }>()
const props = defineProps<{ songs: Readonly<Song[]>, config: AddToMenuConfig }>()
const { songs, config } = toRefs(props)
const queue = toRef(queueStore.state, 'songs')

View file

@ -1,5 +1,5 @@
<template>
<ContextMenuBase ref="base" data-testid="song-context-menu" extra-class="song-menu">
<ContextMenu ref="base" data-testid="song-context-menu" extra-class="song-menu">
<template v-if="onlyOneSongSelected">
<li @click.stop.prevent="doPlayback">
<span v-if="firstSongPlaying">Pause</span>
@ -61,7 +61,7 @@
<li class="separator" />
<li @click="deleteFromFilesystem">Delete from Filesystem</li>
</template>
</ContextMenuBase>
</ContextMenu>
</template>
<script lang="ts" setup>
@ -83,7 +83,7 @@ import {
const { toastSuccess, toastError, toastWarning } = useMessageToaster()
const { showConfirmDialog } = useDialogBox()
const { go, getRouteParam, isCurrentScreen } = useRouter()
const { base, ContextMenuBase, open, close, trigger } = useContextMenu()
const { base, ContextMenu, open, close, trigger } = useContextMenu()
const { removeSongsFromPlaylist } = usePlaylistManagement()
const { isPlus } = useKoelPlus()

View file

@ -90,15 +90,18 @@
</BtnGroup>
</div>
<div ref="addToMenu" v-koel-clickaway="closeAddToMenu" class="context-menu p-0 hidden">
<AddToMenu :config="config.addTo" :songs="selectedSongs" @closing="closeAddToMenu" />
</div>
<OnClickOutside @trigger="closeAddToMenu">
<div ref="addToMenu" class="context-menu p-0 hidden">
<AddToMenu :config="config.addTo" :songs="selectedSongs" @closing="closeAddToMenu" />
</div>
</OnClickOutside>
</div>
</template>
<script lang="ts" setup>
import { faPlay, faRandom, faRotateRight, faTrashCan } from '@fortawesome/free-solid-svg-icons'
import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, Ref, ref, toRef, watch } from 'vue'
import { OnClickOutside } from '@vueuse/components'
import { SelectedSongsKey, SongsKey } from '@/symbols'
import { requireInjection } from '@/utils'
import { useFloatingUi } from '@/composables'

View file

@ -1,25 +1,27 @@
<template>
<form
v-koel-clickaway="maybeClose"
class="flex border rounded-md overflow-hidden border-solid border-white/10 focus-within:bg-black/10 focus-within:border-white/40"
@submit.prevent
>
<Btn v-koel-tooltip title="Filter" transparent unrounded @click.prevent="toggleInput">
<Icon :icon="faFilter" fixed-width />
</Btn>
<TextInput
v-show="showingInput"
ref="input"
v-model="keywords"
class="!text-k-text-primary !bg-transparent !rounded-none !pl-0 !h-[unset] placeholder:text-white/50 focus-visible:outline-0"
placeholder="Keywords"
type="search"
/>
</form>
<OnClickOutside @trigger="maybeClose">
<form
class="flex border rounded-md overflow-hidden border-solid border-white/10 focus-within:bg-black/10 focus-within:border-white/40"
@submit.prevent
>
<Btn v-koel-tooltip title="Filter" transparent unrounded @click.prevent="toggleInput">
<Icon :icon="faFilter" fixed-width />
</Btn>
<TextInput
v-show="showingInput"
ref="input"
v-model="keywords"
class="!text-k-text-primary !bg-transparent !rounded-none !pl-0 !h-[unset] placeholder:text-white/50 focus-visible:outline-0"
placeholder="Keywords"
type="search"
/>
</form>
</OnClickOutside>
</template>
<script lang="ts" setup>
import { faFilter } from '@fortawesome/free-solid-svg-icons'
import { OnClickOutside } from '@vueuse/components'
import { nextTick, ref, watch } from 'vue'
import Btn from '@/components/ui/form/Btn.vue'

View file

@ -3,27 +3,30 @@
<button ref="button" class="w-full focus:text-k-highlight" title="Sort" @click.stop="trigger">
<Icon :icon="faSort" />
</button>
<menu ref="menu" v-koel-clickaway="hide" class="context-menu normal-case tracking-normal">
<li
v-for="item in menuItems"
:key="item.label"
:class="item.field === field && 'active'"
class="cursor-pointer flex justify-between"
@click="sort(item.field)"
>
<span>{{ item.label }}</span>
<span class="icon hidden">
<OnClickOutside @trigger="hide">
<menu ref="menu" class="context-menu normal-case tracking-normal">
<li
v-for="item in menuItems"
:key="item.label"
:class="item.field === field && 'active'"
class="cursor-pointer flex justify-between"
@click="sort(item.field)"
>
<span>{{ item.label }}</span>
<span class="icon hidden">
<Icon v-if="field === 'position'" :icon="faCheck" />
<Icon v-else-if="order === 'asc'" :icon="faArrowDown" />
<Icon v-else :icon="faArrowUp" />
</span>
</li>
</menu>
</li>
</menu>
</OnClickOutside>
</div>
</template>
<script lang="ts" setup>
import { faArrowDown, faArrowUp, faCheck, faSort } from '@fortawesome/free-solid-svg-icons'
import { OnClickOutside } from '@vueuse/components'
import { computed, onBeforeUnmount, onMounted, ref, toRefs } from 'vue'
import { useFloatingUi } from '@/composables'

View file

@ -1,22 +1,24 @@
<template>
<nav
v-if="shown"
ref="el"
v-koel-clickaway="close"
v-koel-focus
:class="extraClass"
class="menu context-menu select-none"
tabindex="0"
@contextmenu.prevent
@keydown.esc="close"
>
<ul>
<slot>Menu items go here.</slot>
</ul>
</nav>
<OnClickOutside @trigger="close">
<nav
v-if="shown"
ref="el"
v-koel-focus
:class="extraClass"
class="menu context-menu select-none"
tabindex="0"
@contextmenu.prevent
@keydown.esc="close"
>
<ul>
<slot>Menu items go here.</slot>
</ul>
</nav>
</OnClickOutside>
</template>
<script lang="ts" setup>
import { OnClickOutside } from '@vueuse/components'
import { nextTick, ref, toRefs } from 'vue'
import { eventBus, logger } from '@/utils'

View file

@ -1,23 +1,25 @@
<template>
<div
v-if="allowsUpload && mediaPathSetUp"
v-koel-clickaway="close"
:class="{ droppable }"
class="drop-zone h-[256px] max-h-[66vh] aspect-square outline-4 outline-dashed outline-gray-300
fixed z-10 top-0 left-0 rounded-3xl bg-black/40 flex flex-col-reverse items-center justify-center
overflow-hidden duration-200"
@dragleave="onDropLeave"
@dragover="onDragOver"
@drop="onDrop"
>
<h3 class="text-3xl mt-4 font-extralight">Drop to upload</h3>
<Icon :icon="faUpload" size="6x" />
</div>
<OnClickOutside @trigger="close">
<div
v-if="allowsUpload && mediaPathSetUp"
:class="{ droppable }"
class="drop-zone h-[256px] max-h-[66vh] aspect-square outline-4 outline-dashed outline-gray-300
fixed z-10 top-0 left-0 rounded-3xl bg-black/40 flex flex-col-reverse items-center justify-center
overflow-hidden duration-200"
@dragleave="onDropLeave"
@dragover="onDragOver"
@drop="onDrop"
>
<h3 class="text-3xl mt-4 font-extralight">Drop to upload</h3>
<Icon :icon="faUpload" size="6x" />
</div>
</OnClickOutside>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { faUpload } from '@fortawesome/free-solid-svg-icons'
import { ref } from 'vue'
import { OnClickOutside } from '@vueuse/components'
import { useUpload } from '@/composables'
const { allowsUpload, mediaPathSetUp, handleDropEvent } = useUpload()

View file

@ -1,8 +1,8 @@
import { ref } from 'vue'
import ContextMenuBase from '@/components/ui/ContextMenuBase.vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
export const useContextMenu = () => {
const base = ref<InstanceType<typeof ContextMenuBase>>()
const base = ref<InstanceType<typeof ContextMenu>>()
const open = async (top: number, left: number) => await base.value?.open(top, left)
const close = () => base.value?.close()
@ -13,7 +13,7 @@ export const useContextMenu = () => {
}
return {
ContextMenuBase,
ContextMenu,
base,
open,
close,

View file

@ -1,7 +0,0 @@
import { Directive } from 'vue'
export const clickaway: Directive = {
created (el: HTMLElement, binding) {
document.addEventListener('click', (e: MouseEvent) => el.contains(e.target as Node) || binding.value())
}
}

View file

@ -1,4 +1,3 @@
export * from './clickaway'
export * from './focus'
export * from './tooltip'
export * from './hideBrokenIcon'

View file

@ -1,10 +1,8 @@
import { createApp } from 'vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { clickaway } from '@/directives'
import '@/../css/remote.pcss'
import App from './App.vue'
createApp(App)
.component('Icon', FontAwesomeIcon)
.directive('koel-clickaway', clickaway)
.mount('#app')

View file

@ -1,11 +1,8 @@
<template>
<span class="volume">
<span
v-show="showingVolumeSlider"
id="volumeSlider"
ref="volumeSlider"
v-koel-clickaway="closeVolumeSlider"
/>
<OnClickOutside @trigger="closeVolumeSlider">
<span v-show="showingVolumeSlider" id="volumeSlider" ref="volumeSlider" />
</OnClickOutside>
<span class="icon" @click.stop="toggleVolumeSlider">
<Icon :icon="muted ? faVolumeMute : faVolumeHigh" fixed-width />
</span>
@ -13,10 +10,11 @@
</template>
<script lang="ts" setup>
import noUISlider from 'nouislider'
import { socketService } from '@/services'
import { faVolumeHigh, faVolumeMute } from '@fortawesome/free-solid-svg-icons'
import noUISlider from 'nouislider'
import { OnClickOutside } from '@vueuse/components'
import { inject, onMounted, ref, watch } from 'vue'
import { socketService } from '@/services'
import { RemoteState } from '@/remote/types'
const DEFAULT_VOLUME = 7

View file

@ -1108,6 +1108,15 @@
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.2.4.tgz#2aa8159cc1f55e5e14d9c1642818e28d6f9d636b"
integrity sha512-1JjLduJ84bFcuCt/1YLTNyktYeUHS/zA0u8iTmF6w6ul1K/nSvyKu/MC47YjdpZ4lI/hn7FH31B22kfz62e9wA==
"@vueuse/components@^10.9.0":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@vueuse/components/-/components-10.9.0.tgz#5c1011e0511b68e4d94f5d545343f86d2a7e3044"
integrity sha512-BHQpA0yIi3y7zKa1gYD0FUzLLkcRTqVhP8smnvsCK6GFpd94Nziq1XVPD7YpFeho0k5BzbBiNZF7V/DpkJ967A==
dependencies:
"@vueuse/core" "10.9.0"
"@vueuse/shared" "10.9.0"
vue-demi ">=0.14.7"
"@vueuse/core@10.9.0", "@vueuse/core@^10.7.2", "@vueuse/core@^10.9.0":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.9.0.tgz#7d779a95cf0189de176fee63cee4ba44b3c85d64"