267 lines
6.3 KiB
Vue
267 lines
6.3 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, useSlots, onMounted } from 'vue'
|
|
|
|
import { type ColorProps, type VariantProps, type DefaultProps, type RaisedProps, type PastelProps, color } from '~/composables/color';
|
|
import { type WidthProps, width } from '~/composables/width'
|
|
import { type AlignmentProps, align } from '~/composables/alignment'
|
|
|
|
import Loader from '~/components/ui/Loader.vue'
|
|
|
|
const props = defineProps<{
|
|
thinFont?: true
|
|
lowHeight? : true
|
|
|
|
isActive?: boolean
|
|
isLoading?: boolean
|
|
|
|
shadow?: boolean
|
|
round?: boolean
|
|
icon?: string | `right ${string}`
|
|
|
|
onClick?: (...args: any[]) => void | Promise<void> // The default fallback is `submit`
|
|
|
|
split?: boolean // Add this prop for split button support
|
|
splitIcon?: string // Add this prop for the split button icon
|
|
splitTitle?: string // Add this prop
|
|
onSplitClick?: (...args: any[]) => void | Promise<void> // Add click handler for split part
|
|
dropdownOnly?: boolean
|
|
|
|
disabled?: boolean
|
|
|
|
autofocus? : boolean
|
|
ariaPressed? : true
|
|
} & (ColorProps | DefaultProps | PastelProps )
|
|
& VariantProps
|
|
& RaisedProps
|
|
& WidthProps
|
|
& AlignmentProps>()
|
|
|
|
const slots = useSlots()
|
|
|
|
// TODO: Refactor this once upload button progress indicator can be tested (in Sidebar.vue)
|
|
const isIconOnly = computed(() => !!props.icon && (!slots.default || 'square' in props && props.square || 'squareSmall' in props && props.squareSmall ))
|
|
const isSplitIconOnly = computed(() => !!props.splitIcon && !props.splitTitle)
|
|
|
|
const internalLoader = ref(false)
|
|
const isLoading = computed(() => props.isLoading || internalLoader.value)
|
|
|
|
const fontWeight = props.thinFont ? 400 : 900
|
|
|
|
const button = ref()
|
|
|
|
const click = async (...args: any[]) => {
|
|
internalLoader.value = true
|
|
|
|
try {
|
|
await props.onClick?.(...args)
|
|
} finally {
|
|
internalLoader.value = false
|
|
}
|
|
}
|
|
onMounted(() => {
|
|
if (props.autofocus) button.value.focus();
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="split" class="funkwhale split-button">
|
|
<button
|
|
v-if="!dropdownOnly"
|
|
ref="button"
|
|
v-bind="{
|
|
...$attrs,
|
|
...color(props, ['interactive'])(
|
|
width(props, isIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
|
|
align(props, { alignText:'center' })(
|
|
)))}"
|
|
class="funkwhale button split-main"
|
|
:autofocus="autofocus || undefined"
|
|
:disabled="disabled || undefined"
|
|
:aria-pressed="props.ariaPressed"
|
|
:class="{
|
|
'is-loading': isLoading,
|
|
'is-icon-only': isIconOnly,
|
|
'has-icon': !!icon,
|
|
'is-round': round,
|
|
'is-shadow': shadow,
|
|
}"
|
|
@click="click"
|
|
>
|
|
|
|
<slot name="main">
|
|
<i v-if="icon && !icon.startsWith('right ')" :class="['bi', icon]" />
|
|
|
|
<span v-if="!isIconOnly">
|
|
<slot />
|
|
</span>
|
|
|
|
<i v-if="icon && icon.startsWith('right ')" :class="['bi', icon.replace('right ', '')]" />
|
|
</slot>
|
|
<Loader v-if="isLoading" :container="false" />
|
|
</button>
|
|
<button
|
|
v-bind="{
|
|
...$attrs,
|
|
...color(props, ['interactive'])(
|
|
width(props, isSplitIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
|
|
align(props, { alignSelf:'start', alignText:'center' })(
|
|
)))}"
|
|
:disabled="disabled || undefined"
|
|
:autofocus="autofocus || undefined"
|
|
:class="[
|
|
'funkwhale',
|
|
'button',
|
|
{
|
|
'split-toggle': true,
|
|
'is-loading': isLoading,
|
|
'is-icon-only': isSplitIconOnly,
|
|
'has-icon': !!splitIcon,
|
|
'is-round': round,
|
|
'is-shadow': shadow
|
|
}
|
|
]"
|
|
@click="onSplitClick"
|
|
>
|
|
<span v-if="splitTitle">{{ splitTitle }}</span>
|
|
<i :class="['bi', splitIcon]" />
|
|
</button>
|
|
</div>
|
|
<button
|
|
v-else
|
|
ref="button"
|
|
v-bind="color(props, ['interactive'])(
|
|
width(props, isIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
|
|
align(props, { alignSelf:'start', alignText:'center' })(
|
|
)))"
|
|
:disabled="disabled || undefined"
|
|
:autofocus="autofocus || undefined"
|
|
class="funkwhale button"
|
|
:aria-pressed="props.ariaPressed"
|
|
:class="{
|
|
'is-loading': isLoading,
|
|
'is-icon-only': isIconOnly,
|
|
'has-icon': !!icon,
|
|
'is-round': round,
|
|
'is-shadow': shadow,
|
|
}"
|
|
@click="click"
|
|
:type="onClick ? 'button' : 'submit' /* Prevents default `submit` if onCLick is set */"
|
|
>
|
|
<i v-if="icon && !icon.startsWith('right ')" :class="['bi', icon]" />
|
|
<span v-if="!isIconOnly">
|
|
<slot />
|
|
</span>
|
|
<i v-if="icon && icon.startsWith('right ')" :class="['bi', icon.replace('right ', '')]" />
|
|
<Loader v-if="isLoading" :container="false" />
|
|
</button>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
.funkwhale {
|
|
&.split-button {
|
|
|
|
.button {
|
|
display: inline-flex; // Ensure consistent display
|
|
align-items: center;
|
|
|
|
&.split-main {
|
|
border-top-right-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
border-right: 1px solid var(--border-color);
|
|
}
|
|
|
|
&.split-toggle {
|
|
border-top-left-radius: 0;
|
|
border-bottom-left-radius: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.button {
|
|
|
|
// Layout
|
|
|
|
--padding: 16px;
|
|
--shift-by: 0.5px;
|
|
gap: 8px;
|
|
|
|
position: relative;
|
|
display: inline-flex;
|
|
white-space: nowrap;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
|
|
padding: calc(var(--padding) / 2 - var(--shift-by)) var(--padding) calc(var(--padding) / 2 + var(--shift-by)) var(--padding);
|
|
&.is-icon-only {
|
|
padding: var(--padding);
|
|
}
|
|
|
|
// Font
|
|
|
|
font-family: $font-main;
|
|
font-weight: v-bind(fontWeight);
|
|
font-size: 14px;
|
|
|
|
line-height: 14px;
|
|
|
|
// Decoration
|
|
|
|
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
|
|
transition: all .2s ease;
|
|
|
|
&.is-shadow {
|
|
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
border-radius: var(--fw-border-radius);
|
|
&.is-round {
|
|
border-radius: 100vh;
|
|
}
|
|
|
|
// States
|
|
|
|
&[disabled] {
|
|
font-weight: normal;
|
|
pointer-events: none;
|
|
}
|
|
|
|
&.is-loading {
|
|
@extend :active;
|
|
|
|
> span {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
// Content
|
|
|
|
> span {
|
|
position: relative;
|
|
top: calc(0px - var(--shift-by));
|
|
}
|
|
|
|
// Icon
|
|
|
|
> i.bi {
|
|
font-size: 18px;
|
|
margin: -2px 0;
|
|
&.large {
|
|
font-size: 32px;
|
|
margin: -8px 0;
|
|
}
|
|
}
|
|
&.is-icon-only i.bi {
|
|
margin: -6px;
|
|
&.large {
|
|
margin: -8px;
|
|
}
|
|
}
|
|
&:has(>i){
|
|
gap: 10px;
|
|
}
|
|
}
|
|
}
|
|
|
|
</style>
|