mirror of
https://github.com/koel/koel
synced 2024-11-10 14:44:13 +00:00
feat: use Floating UI for "Add To" menu (#1584)
This commit is contained in:
parent
af30e632fd
commit
2ea9f582a5
34 changed files with 129 additions and 133 deletions
|
@ -26,7 +26,7 @@ const email = ref(isDemo() ? DEMO_ACCOUNT.email : '')
|
|||
const password = ref(isDemo() ? DEMO_ACCOUNT.password : '')
|
||||
const failed = ref(false)
|
||||
|
||||
const emit = defineEmits(['loggedin'])
|
||||
const emit = defineEmits<{ (e: 'loggedin'): void }>()
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
|
|
|
@ -70,7 +70,7 @@ const {
|
|||
latestVersionReleaseUrl
|
||||
} = useNewVersionNotification()
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
|
@ -16,8 +16,6 @@ import { onMounted } from 'vue'
|
|||
|
||||
const { base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
|
||||
const emit = defineEmits(['itemClicked'])
|
||||
|
||||
const actionToEventMap: Record<string, EventName> = {
|
||||
'new-playlist': 'MODAL_SHOW_CREATE_PLAYLIST_FORM',
|
||||
'new-smart-playlist': 'MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM',
|
||||
|
|
|
@ -42,8 +42,7 @@ const dialog = requireInjection(DialogBoxKey)
|
|||
const loading = ref(false)
|
||||
const name = ref('')
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const submit = async () => {
|
||||
|
|
|
@ -43,8 +43,7 @@ const dialog = requireInjection(DialogBoxKey)
|
|||
const loading = ref(false)
|
||||
const name = ref('')
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const submit = async () => {
|
||||
|
|
|
@ -60,8 +60,7 @@ const submit = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const maybeClose = async () => {
|
||||
|
|
|
@ -60,8 +60,7 @@ const submit = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const maybeClose = async () => {
|
||||
|
|
|
@ -60,7 +60,7 @@ const dialog = requireInjection(DialogBoxKey)
|
|||
const router = requireInjection(RouterKey)
|
||||
const name = ref('')
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const maybeClose = async () => {
|
||||
|
|
|
@ -73,7 +73,7 @@ const {
|
|||
onGroupChanged
|
||||
} = useSmartPlaylistForm(mutablePlaylist.rules)
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const maybeClose = async () => {
|
||||
|
|
|
@ -96,7 +96,10 @@ watch(availableOperators, () => {
|
|||
|
||||
const valueSuffix = computed(() => selectedOperator.value?.unit || selectedModel.value?.unit)
|
||||
|
||||
const emit = defineEmits(['input', 'remove'])
|
||||
const emit = defineEmits<{
|
||||
(e: 'input', rule: SmartPlaylistRule): void,
|
||||
(e: 'remove'): void
|
||||
}>()
|
||||
|
||||
const onInput = () => {
|
||||
emit('input', {
|
||||
|
@ -104,7 +107,7 @@ const onInput = () => {
|
|||
model: selectedModel.value,
|
||||
operator: selectedOperator.value?.operator,
|
||||
value: availableInputs.value.map(input => input.value)
|
||||
} as SmartPlaylistRule)
|
||||
})
|
||||
}
|
||||
|
||||
const removeRule = () => emit('remove')
|
||||
|
|
|
@ -37,7 +37,7 @@ const Rule = defineAsyncComponent(() => import('@/components/playlist/smart-play
|
|||
|
||||
const mutatedGroup = reactive<SmartPlaylistRuleGroup>(JSON.parse(JSON.stringify(group.value)))
|
||||
|
||||
const emit = defineEmits(['input'])
|
||||
const emit = defineEmits<{ (e: 'input', group: SmartPlaylistRuleGroup): void }>()
|
||||
|
||||
const notifyParentForUpdate = () => emit('input', mutatedGroup)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import inputTypes from '@/config/smart-playlist/inputTypes'
|
|||
const props = withDefaults(defineProps<{ type?: keyof typeof inputTypes, value?: any }>(), { value: undefined })
|
||||
const { type } = toRefs(props)
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: any): void }>()
|
||||
|
||||
const value = computed({
|
||||
get: () => props.value,
|
||||
|
|
|
@ -14,7 +14,8 @@ exports[`renders 1`] = `
|
|||
</div>
|
||||
<div class="song-list-controls" data-testid="song-list-controls" data-v-d396e0d2="" data-v-5691beb5-s="">
|
||||
<div class="wrapper" data-v-d396e0d2=""><span class="btn-group" uppercased="" data-v-e884c19a="" data-v-d396e0d2=""><button type="button" class="btn-shuffle-all" data-testid="btn-shuffle-all" orange="" title="Shuffle all songs" data-v-e368fe26="" data-v-d396e0d2=""><br data-testid="icon" icon="[object Object]" fixed-width="" data-v-d396e0d2=""> All </button><!--v-if--><!--v-if--><!--v-if--></span><span class="btn-group" data-v-e884c19a="" data-v-d396e0d2=""></span></div>
|
||||
<div class="add-to" data-testid="add-to-menu" tabindex="0" data-v-42061e3e="" data-v-d396e0d2="" style="display: none;">
|
||||
<div class="menu-wrapper" data-v-d396e0d2="">
|
||||
<div class="add-to" data-testid="add-to-menu" tabindex="0" data-v-42061e3e="" data-v-d396e0d2="">
|
||||
<section class="existing-playlists" data-v-42061e3e="">
|
||||
<p data-v-42061e3e="">Add 0 songs to</p>
|
||||
<ul data-v-42061e3e="">
|
||||
|
@ -28,6 +29,7 @@ exports[`renders 1`] = `
|
|||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</header><br data-testid="song-list">
|
||||
</section>
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="showing"
|
||||
v-koel-clickaway="close"
|
||||
v-koel-focus
|
||||
class="add-to"
|
||||
data-testid="add-to-menu"
|
||||
tabindex="0"
|
||||
@keydown.esc="close"
|
||||
>
|
||||
<div class="add-to" data-testid="add-to-menu" tabindex="0">
|
||||
<section class="existing-playlists">
|
||||
<p>Add {{ pluralize(songs, 'song') }} to</p>
|
||||
|
||||
|
@ -86,8 +78,8 @@ import Btn from '@/components/ui/Btn.vue'
|
|||
const toaster = requireInjection(MessageToasterKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const props = defineProps<{ songs: Song[], showing: Boolean, config: AddToMenuConfig }>()
|
||||
const { songs, showing, config } = toRefs(props)
|
||||
const props = defineProps<{ songs: Song[], config: AddToMenuConfig }>()
|
||||
const { songs, config } = toRefs(props)
|
||||
|
||||
const newPlaylistName = ref('')
|
||||
const queue = toRef(queueStore.state, 'songs')
|
||||
|
@ -96,7 +88,7 @@ const currentSong = queueStore.current
|
|||
const allPlaylists = toRef(playlistStore.state, 'playlists')
|
||||
const playlists = computed(() => allPlaylists.value.filter(playlist => !playlist.is_smart))
|
||||
|
||||
const emit = defineEmits(['closing'])
|
||||
const emit = defineEmits<{ (e: 'closing'): void }>()
|
||||
const close = () => emit('closing')
|
||||
|
||||
const {
|
||||
|
@ -135,8 +127,6 @@ const createNewPlaylistFromSongs = async () => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.add-to {
|
||||
@include context-menu();
|
||||
|
||||
width: 100%;
|
||||
max-width: 225px;
|
||||
padding: .75rem;
|
||||
|
@ -181,34 +171,23 @@ const createNewPlaylistFromSongs = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-bottom: 10px solid var(--color-bg-primary);
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: calc(50% - 10px);
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
border-radius: 5px 0 0 5px;
|
||||
height: 28px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
margin-top: 0;
|
||||
border-radius: 0 5px 5px 0 !important;
|
||||
border-radius: 0;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding-top: 0;
|
||||
|
|
|
@ -299,7 +299,7 @@ const open = async () => {
|
|||
Object.assign(initialFormData, formData)
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const maybeClose = async () => {
|
||||
|
|
|
@ -104,7 +104,14 @@ import SongListSorter from '@/components/song/SongListSorter.vue'
|
|||
const { startDragging } = useDraggable('songs')
|
||||
const { getDroppedData, acceptsDrop } = useDroppable(['songs'])
|
||||
|
||||
const emit = defineEmits(['press:enter', 'press:delete', 'reorder', 'sort', 'scroll-breakpoint', 'scrolled-to-end'])
|
||||
const emit = defineEmits<{
|
||||
(e: 'press:enter', event: KeyboardEvent): void,
|
||||
(e: 'press:delete'): void,
|
||||
(e: 'reorder', song: Song): void,
|
||||
(e: 'sort', field: SongListSortField, order: SortOrder): void,
|
||||
(e: 'scroll-breakpoint', direction: 'up' | 'down'): void,
|
||||
(e: 'scrolled-to-end'): void,
|
||||
}>()
|
||||
|
||||
const [items] = requireInjection<[Ref<Song[]>]>(SongsKey)
|
||||
const [selectedSongs, setSelectedSongs] = requireInjection<[Ref<Song[]>, Closure]>(SelectedSongsKey)
|
||||
|
|
|
@ -59,16 +59,6 @@ new class extends UnitTestCase {
|
|||
expect(emitted().playSelected[0]).toEqual([false])
|
||||
})
|
||||
|
||||
it('toggles Add To menu', async () => {
|
||||
const { getByTitle, getByTestId } = this.renderComponent()
|
||||
|
||||
await fireEvent.click(getByTitle('Add selected songs to…'))
|
||||
expect(getByTestId('add-to-menu').style.display).toBe('')
|
||||
|
||||
await fireEvent.click(getByTitle('Cancel'))
|
||||
expect(getByTestId('add-to-menu').style.display).toBe('none')
|
||||
})
|
||||
|
||||
it('clears queue', async () => {
|
||||
const { emitted, getByTitle } = this.renderComponent(0, { clearQueue: true })
|
||||
|
||||
|
|
|
@ -54,14 +54,7 @@
|
|||
</template>
|
||||
</template>
|
||||
|
||||
<Btn
|
||||
v-if="selectedSongs.length"
|
||||
:title="`${showingAddToMenu ? 'Cancel' : 'Add selected songs to…'}`"
|
||||
class="btn-add-to"
|
||||
data-testid="add-to-btn"
|
||||
green
|
||||
@click.prevent.stop="toggleAddToMenu"
|
||||
>
|
||||
<Btn v-if="showAddToButton" ref="addToButton" green @click.prevent.stop="toggleAddToMenu">
|
||||
{{ showingAddToMenu ? 'Cancel' : 'Add To…' }}
|
||||
</Btn>
|
||||
|
||||
|
@ -86,21 +79,18 @@
|
|||
</BtnGroup>
|
||||
</div>
|
||||
|
||||
<AddToMenu
|
||||
v-koel-clickaway="closeAddToMenu"
|
||||
:config="mergedConfig.addTo"
|
||||
:showing="showingAddToMenu"
|
||||
:songs="selectedSongs"
|
||||
@closing="closeAddToMenu"
|
||||
/>
|
||||
<div ref="addToMenu" v-koel-clickaway="closeAddToMenu" class="menu-wrapper">
|
||||
<AddToMenu :config="mergedConfig.addTo" :songs="selectedSongs" @closing="closeAddToMenu"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faPlay, faRandom, faRotateRight, faTrashCan } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, toRefs } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, Ref, ref, toRefs, watch } from 'vue'
|
||||
import { SelectedSongsKey, SongsKey } from '@/symbols'
|
||||
import { requireInjection } from '@/utils'
|
||||
import { useFloatingUi } from '@/composables'
|
||||
|
||||
import AddToMenu from '@/components/song/AddToMenu.vue'
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
|
@ -109,10 +99,12 @@ import BtnGroup from '@/components/ui/BtnGroup.vue'
|
|||
const props = withDefaults(defineProps<{ config?: Partial<SongListControlsConfig> }>(), { config: () => ({}) })
|
||||
const { config } = toRefs(props)
|
||||
|
||||
const [songs] = requireInjection(SongsKey)
|
||||
const [songs] = requireInjection<[Ref<Song[]>]>(SongsKey)
|
||||
const [selectedSongs] = requireInjection(SelectedSongsKey)
|
||||
|
||||
const el = ref<HTMLElement>()
|
||||
const addToButton = ref<InstanceType<Btn>>()
|
||||
const addToMenu = ref<HTMLDivElement>()
|
||||
const showingAddToMenu = ref(false)
|
||||
const altPressed = ref(false)
|
||||
|
||||
|
@ -130,10 +122,14 @@ const mergedConfig = computed((): SongListControlsConfig => Object.assign({
|
|||
}, config.value)
|
||||
)
|
||||
|
||||
const showAddToButton = computed(() => Boolean(selectedSongs.value.length))
|
||||
const showClearQueueButton = computed(() => mergedConfig.value.clearQueue)
|
||||
const showDeletePlaylistButton = computed(() => mergedConfig.value.deletePlaylist)
|
||||
|
||||
const emit = defineEmits(['playAll', 'playSelected', 'clearQueue', 'deletePlaylist', 'refresh'])
|
||||
const emit = defineEmits<{
|
||||
(e: 'playAll' | 'playSelected', shuffle: boolean): void,
|
||||
(e: 'clearQueue' | 'deletePlaylist' | 'refresh'): void,
|
||||
}>()
|
||||
|
||||
const shuffle = () => emit('playAll', true)
|
||||
const shuffleSelected = () => emit('playSelected', true)
|
||||
|
@ -142,25 +138,30 @@ const playSelected = () => emit('playSelected', false)
|
|||
const clearQueue = () => emit('clearQueue')
|
||||
const deletePlaylist = () => emit('deletePlaylist')
|
||||
const refresh = () => emit('refresh')
|
||||
const closeAddToMenu = () => (showingAddToMenu.value = false)
|
||||
const registerKeydown = (event: KeyboardEvent) => event.key === 'Alt' && (altPressed.value = true)
|
||||
const registerKeyup = (event: KeyboardEvent) => event.key === 'Alt' && (altPressed.value = false)
|
||||
|
||||
const toggleAddToMenu = async () => {
|
||||
showingAddToMenu.value = !showingAddToMenu.value
|
||||
|
||||
if (!showingAddToMenu.value) {
|
||||
return
|
||||
}
|
||||
let usedFloatingUi: ReturnType<typeof useFloatingUi>
|
||||
|
||||
watch(showAddToButton, async showingButton => {
|
||||
await nextTick()
|
||||
|
||||
const btnAddTo = el.value?.querySelector<HTMLButtonElement>('.btn-add-to')!
|
||||
const { left: btnLeft, bottom: btnBottom, width: btnWidth } = btnAddTo.getBoundingClientRect()
|
||||
const contextMenu = el.value?.querySelector<HTMLElement>('.add-to')!
|
||||
const menuWidth = contextMenu.getBoundingClientRect().width
|
||||
contextMenu.style.top = `${btnBottom + 10}px`
|
||||
contextMenu.style.left = `${btnLeft + btnWidth / 2 - menuWidth / 2}px`
|
||||
if (showingButton) {
|
||||
usedFloatingUi = useFloatingUi(addToButton.value.button, addToMenu, { autoTrigger: false })
|
||||
usedFloatingUi.setup()
|
||||
} else {
|
||||
usedFloatingUi?.teardown()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const closeAddToMenu = () => {
|
||||
usedFloatingUi?.hide()
|
||||
showingAddToMenu.value = false
|
||||
}
|
||||
|
||||
const toggleAddToMenu = () => {
|
||||
showingAddToMenu.value ? usedFloatingUi?.hide() : usedFloatingUi?.show()
|
||||
showingAddToMenu.value = !showingAddToMenu.value
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -168,9 +169,11 @@ onMounted(() => {
|
|||
window.addEventListener('keyup', registerKeyup)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', registerKeydown)
|
||||
window.removeEventListener('keyup', registerKeyup)
|
||||
|
||||
usedFloatingUi?.teardown()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -182,5 +185,12 @@ onUnmounted(() => {
|
|||
display: flex;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.menu-wrapper {
|
||||
@include context-menu();
|
||||
|
||||
padding: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,7 +23,7 @@ import { useFloatingUi } from '@/composables'
|
|||
const props = defineProps<{ field?: SongListSortField, order?: SortOrder }>()
|
||||
const { field, order } = toRefs(props)
|
||||
|
||||
const emit = defineEmits<{ (e: 'sort', payload: SongListSortField): void }>()
|
||||
const emit = defineEmits<{ (e: 'sort', field: SongListSortField): void }>()
|
||||
|
||||
const button = ref<HTMLButtonElement>()
|
||||
const menu = ref<HTMLDivElement>()
|
||||
|
@ -76,7 +76,6 @@ button {
|
|||
}
|
||||
|
||||
menu {
|
||||
width: max-content;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `
|
||||
<div class="add-to" data-testid="add-to-menu" tabindex="0" data-v-42061e3e="">
|
||||
<div class="add-to" data-testid="add-to-menu" tabindex="0" showing="true" data-v-42061e3e="">
|
||||
<section class="existing-playlists" data-v-42061e3e="">
|
||||
<p data-v-42061e3e="">Add 5 songs to</p>
|
||||
<ul data-v-42061e3e="">
|
||||
|
|
|
@ -28,7 +28,11 @@ const props = withDefaults(
|
|||
{ layout: 'full' }
|
||||
)
|
||||
|
||||
const emit = defineEmits(['dblclick', 'contextmenu', 'dragstart'])
|
||||
const emit = defineEmits<{
|
||||
(e: 'dblclick'): void,
|
||||
(e: 'dragstart', event: DragEvent): void,
|
||||
(e: 'contextmenu', event: MouseEvent): void
|
||||
}>()
|
||||
|
||||
const onDblClick = () => emit('dblclick')
|
||||
const onDragStart = (e: DragEvent) => emit('dragstart', e)
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
<template>
|
||||
<button type="button">
|
||||
<button type="button" ref="button">
|
||||
<slot>Click me</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const button = ref<HTMLButtonElement>()
|
||||
|
||||
defineExpose({
|
||||
button
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
button {
|
||||
background: var(--color-blue);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
defineEmits(['click'])
|
||||
defineEmits<{ (e: 'click'): void }>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -15,7 +15,7 @@ const props = withDefaults(defineProps<{ modelValue?: any }>(), {
|
|||
|
||||
const checked = ref(props.modelValue)
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||
|
||||
const onInput = (event: InputEvent) => {
|
||||
checked.value = (event.target as HTMLInputElement).checked
|
||||
|
|
|
@ -46,7 +46,7 @@ import { equalizerPresets as presets } from '@/config'
|
|||
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
|
||||
const bands = audioService.bands
|
||||
const root = ref<HTMLElement>()
|
||||
|
|
|
@ -50,7 +50,7 @@ import { useThirdPartyServices } from '@/composables'
|
|||
|
||||
const props = defineProps<{ modelValue?: ExtraPanelTab }>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: ExtraPanelTab): void }>()
|
||||
|
||||
const { useYouTube } = useThirdPartyServices()
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { faSearchMinus, faSearchPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const emit = defineEmits(['in', 'out'])
|
||||
const emit = defineEmits<{ (e: 'in' | 'out'): void }>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -41,7 +41,7 @@ const typeIcon = computed(() => {
|
|||
|
||||
let timeoutHandler: number
|
||||
|
||||
const emit = defineEmits(['dismiss'])
|
||||
const emit = defineEmits<{ (e: 'dismiss', message: ToastMessage): void }>()
|
||||
|
||||
const dismiss = () => {
|
||||
emit('dismiss', message.value)
|
||||
|
|
|
@ -13,7 +13,7 @@ import { computed } from 'vue'
|
|||
|
||||
const props = withDefaults(defineProps<{ modelValue?: boolean }>(), { modelValue: false })
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
|
|
|
@ -32,7 +32,8 @@ import { faList } from '@fortawesome/free-solid-svg-icons'
|
|||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{ modelValue?: ArtistAlbumViewMode }>(), { modelValue: 'thumbnails' })
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: ArtistAlbumViewMode): void }>()
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
|
|
|
@ -21,7 +21,10 @@ const scrollerHeight = ref(0)
|
|||
const renderAhead = 5
|
||||
const scrollTop = ref(0)
|
||||
|
||||
const emit = defineEmits(['scrolled-to-end', 'scroll'])
|
||||
const emit = defineEmits<{
|
||||
(e: 'scrolled-to-end'): void,
|
||||
(e: 'scroll', event: MouseEvent): void
|
||||
}>()
|
||||
|
||||
const totalHeight = computed(() => items.value.length * itemHeight.value)
|
||||
const startPosition = computed(() => Math.max(0, Math.floor(scrollTop.value / itemHeight.value) - renderAhead))
|
||||
|
|
|
@ -91,8 +91,7 @@ const submit = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const maybeClose = async () => {
|
||||
|
|
|
@ -96,8 +96,7 @@ const submit = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const maybeClose = async () => {
|
||||
|
|
|
@ -159,24 +159,20 @@
|
|||
|
||||
@mixin context-menu() {
|
||||
padding: .4rem 0;
|
||||
width: max-content;
|
||||
min-width: 144px;
|
||||
background-color: var(--color-bg-context-menu);
|
||||
position: fixed;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
z-index: 1001;
|
||||
align-items: stretch;
|
||||
text-align: left;
|
||||
box-shadow: inset 0 0 0 rgba(255, 255, 255, 0.3), 0 2px 15px 4px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
filter: drop-shadow(0 5px 15px rgba(0, 0, 0, .5));
|
||||
|
||||
input[type="search"], input[type="text"], input[type="email"], input[type="url"] {
|
||||
background: var(--color-text-primary);
|
||||
|
||||
&:focus {
|
||||
background: var(--color-text-primary);
|
||||
}
|
||||
:deep(.arrow) {
|
||||
background-color: var(--color-bg-context-menu);
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue