refactor(ui): [WIP] allow user to add, edit and remove custom pills

This commit is contained in:
upsiflu 2025-01-29 09:52:52 +01:00
parent 97ff6c56aa
commit a54a80fd02
4 changed files with 180 additions and 50 deletions

View File

@ -8,24 +8,34 @@ const handleClick = (event: MouseEvent) => {
emit('click', event) emit('click', event)
} }
const props = defineProps<{ noUnderline?:true } & (PastelProps | ColorProps) & VariantProps & RaisedProps>() const props = defineProps<{ noUnderline?:true } & (PastelProps | ColorProps) & VariantProps & RaisedProps>()
const model = defineModel<string>()
</script> </script>
<template> <template>
<button <component
class="funkwhale pill" class="funkwhale pill"
:class="props.noUnderline && 'no-underline'" :class="props.noUnderline && 'no-underline'"
:is="model ? 'label' : 'button'"
v-bind="color(props, ['interactive', 'outline'])()" v-bind="color(props, ['interactive', 'outline'])()"
@click.stop="handleClick" @click.stop="handleClick"
> >
<div v-if="!!$slots.image" class="pill-image"> <div v-if="!!$slots.image" class="pill-image">
<slot name="image" /> <slot name="image" />
</div> </div>
<div class="pill-content"> <input v-if="model" class="pill-content" v-model="model" style="border-radius:16px; min-width: max-content; width:min-content;" />
<div class="pill-content" v-if="!!$slots.default">
<slot /> <slot />
</div> </div>
</button> </component>
</template> </template>
<style lang="scss"> <style lang="scss">
@import './pill.scss'; @import './pill.scss';
label.funkwhale.pill.pill {
height: 28px;
>input {
// Note that <Input>s can't be styled with `min-width` or `width` directly.
width: 120px;
}
}
</style> </style>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch, watchEffect } from 'vue' import { ref, watchEffect, computed } from 'vue'
import { color } from '~/composables/color'; import { color } from '~/composables/color';
@ -10,23 +10,27 @@ const props = defineProps<{
icon?: string, icon?: string,
placeholder?: string, placeholder?: string,
label?: string, label?: string,
customOptions?: true,
autofocus?: boolean, autofocus?: boolean,
}>(); }>();
const customOptions = ref<string[]>([]);
const model = defineModel<{ const model = defineModel<{
current: string[], current: string[],
others?: string[] others?: string[],
}>({required:true}) custom?: string[],
}>({ required: true });
const whenInteractive = (then:() => void) => { const whenInteractive = (then:() => void) => {
if(!model.value.others) return; then(); if(!model.value.others) return; then();
} }
const editingValue = ref('      ');
const isCustom = (value:string) =>
model.value.current.includes(value)
const selectedLabel = ref("+"); const selectedLabel = ref("+");
whenInteractive(()=> whenInteractive(()=>
watchEffect(() => { watchEffect(() => {
if (!model.value.others) return if (!model.value.others) return
@ -39,15 +43,20 @@ whenInteractive(()=>
} }
}) })
) )
const deselectPill = (value:string) => const pillClicked = (value: string) =>
whenInteractive(()=> model.value.custom?.includes(value) ? edit(value) : unselect(value)
model.value =
const unselect = (value: string) =>
whenInteractive(() => model.value =
{...model.value, {...model.value,
current : model.value.current.filter(v=>v!==value), current : model.value.current.filter(v=>v!==value),
others : [value, ...(model.value.others || [])] others : [value, ...(model.value.others || [])]
} }
) )
const edit = (value: string) =>
whenInteractive(()=> editingValue.value = value)
</script> </script>
<template> <template>
@ -67,23 +76,8 @@ const deselectPill = (value:string) =>
v-bind="color({}, ['solid', 'default', 'secondary'])()" v-bind="color({}, ['solid', 'default', 'secondary'])()"
:class="$style.list" :class="$style.list"
> >
<Pill outline raised
v-for="value in model.current"
v-if="!model.others"
:class="$style.pill"
>
<span>{{ value }}</span>
</Pill>
<Pill outline raised
no-underline @click="deselectPill(value)"
v-for="value in model.current"
v-if="model.others"
:class="$style.pill"
>
<span :class="$style['pill-content']">{{ value }}</span>   ×
</Pill>
<!-- Add more pills --> <!-- Add predefined or previously unselected pill -->
<select <select
v-if="model.others" v-if="model.others"
@ -97,6 +91,45 @@ const deselectPill = (value:string) =>
{{ value }} {{ value }}
</option> </option>
</select> </select>
<!-- Static pills -->
<Pill outline raised
v-if="!model.others"
v-for="value in model.current"
:class="$style.pill"
>
<span>{{ value }}</span>
</Pill>
<!-- Dynamic pills -->
<template v-for="value in model.current">
<Pill outline raised no-underline
v-if="value !== editingValue"
@click="pillClicked(value)"
:class="$style.pill"
>
<span :class="$style['pill-content']">{{ value }}</span>
{{ isCustom(value) ? '   ×' : ''}}
</Pill>
<Pill outline raised no-underline
v-else
v-model="editingValue"
@click="pillClicked(value)"
:class="$style.pill"
/>
</template>
<!-- Add custom pill -->
<Pill outline raised no-underline
v-if="model.custom"
:class="$style.pill"
v-model="editingValue"
style="margin-right: 40px; height:32px;"
>
</Pill>
</Layout> </Layout>
</Layout> </Layout>
</template> </template>
@ -110,6 +143,8 @@ const deselectPill = (value:string) =>
font-weight: 600; font-weight: 600;
} }
>.list { >.list {
position: relative;
padding:4px; padding:4px;
border-radius: 24px; border-radius: 24px;
@ -124,9 +159,11 @@ const deselectPill = (value:string) =>
padding: 2px; padding: 2px;
} }
>.dropdown{ >.dropdown{
position: absolute;
inset: 0;
border-radius: 15px; border-radius: 15px;
padding: 2px 11.25px; padding: 2px 11.25px;
flex-grow: 1;
text-align: right; text-align: right;
background: transparent; background: transparent;
appearance: auto; appearance: auto;
@ -137,10 +174,10 @@ const deselectPill = (value:string) =>
color: inherit; color: inherit;
} }
} }
&:hover>.list { &:hover:has(select)>.list {
box-shadow: inset 0 0 0 4px var(--border-color) box-shadow: inset 0 0 0 4px var(--border-color)
} }
:has(>.dropdown:focus) { :has(>select:focus) {
box-shadow: inset 0 0 0 4px var(--focus-ring-color) box-shadow: inset 0 0 0 4px var(--focus-ring-color)
} }
} }

View File

@ -1,16 +1,22 @@
<script setup> <script setup>
import { computed, ref } from 'vue'
import Pill from '~/components/ui/Pill.vue' import Pill from '~/components/ui/Pill.vue'
import Button from '~/components/ui/Button.vue'
const customTag=ref('Custom Tag')
const isDeleted = computed(() => customTag.value==='' ? 'Pill was deleted' : '')
</script> </script>
```ts ```ts
import Pill from "~/components/ui/Pill.vue" import Pill from "~/components/ui/Pill.vue";
``` ```
# Pill # Pill
Pills are decorative elements that display information about content they attach to. They can be links to other content or simple colored labels. Pills are decorative elements that display information about content they attach to. They can be links to other content or simple colored labels.
You can add text to pills by adding it between the `<Pill>` tags. You can add text to pills by adding it between the `<Pill>` tags. Alternatively, you can set `v-model` and [make the pill editable](#editable-pill).
| Prop | Data type | Required? | Default | Description | | Prop | Data type | Required? | Default | Description |
| ------- | ----------------------------------------------------------------------------------------------- | --------- | ----------- | ---------------------- | | ------- | ----------------------------------------------------------------------------------------------- | --------- | ----------- | ---------------------- |
@ -128,14 +134,14 @@ Funkwhale pills support a range of pastel colors to create visually appealing in
Yellow pill Yellow pill
</Pill> </Pill>
## Image pills ## Image pill
Image pills contain a small circular image on their left. These can be used for decorative links such as artist links. To created an image pill, insert a link to the image between the pill tags as a `<template>`. Image pills contain a small circular image on their left. These can be used for decorative links such as artist links. To created an image pill, insert a link to the image between the pill tags as a `<template>`.
```vue-html{2-4} ```vue-html{2-4}
<Pill> <Pill>
<template #image> <template #image>
<img src="/images/awesome-artist.png" /> <div style="background-color: #0004" />
</template> </template>
Awesome artist Awesome artist
</Pill> </Pill>
@ -147,3 +153,27 @@ Image pills contain a small circular image on their left. These can be used for
</template> </template>
Awesome artist Awesome artist
</Pill> </Pill>
## Editable pill
Add `v-model="..."` to link the pill content to a `ref`. Note that the pill is not rendered if v-model is ''.
```ts
import { computed, ref } from "vue";
const customTag = ref(" ");
const isDeleted = computed(() => customTag.value === "");
```
```vue-html
<Pill v-model="customTag" />
<Button primary v-if="isDeleted" :onClick="() => customTag=' '">New Pill</Button>
```
<Pill v-model="customTag">
<template #image>
<div style="background-color: #0004" />
</template>
</Pill>
<Button primary v-if="isDeleted" :onClick="() => customTag=' '">New Pill</Button>

View File

@ -7,47 +7,47 @@ import Input from '~/components/ui/Input.vue';
const nullModel = ref({ const nullModel = ref({
current: [], current: [],
others: []
}); });
const staticModel = ref({ const staticModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"] current: ["#Noise", "#FieldRecording", "#Experiment"],
}); });
const interactiveModel = ref({ const interactiveModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"], current: ["#Noise", "#FieldRecording", "#Experiment"],
others: ["#Melody", "#Rhythm"] others: ["#Melody", "#Rhythm"],
});
const customModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"],
others: ["#Melody", "#Rhythm"],
custom: ["#Noise"],
}); });
</script> </script>
```ts ```ts
import Pills from "~/components/ui/Pills.vue" import Pills from "~/components/ui/Pills.vue";
``` ```
# Pills # Pills
```vue-html Show a dense list of pills representing tags, categories or options.
<Pills v-model="staticModel" label="Tags" /> Users can select a subset of given options and create new ones.
```
<Layout class="preview" style="padding:16px"> The model you provide will be mutated by this component:
<Pills v-model="staticModel" label="Tags" />
</Layout>
Select a set of pills from presets, and add and remove custom ones - `current`: these pills are currently selected
- `others`: these pills are currently not selected (but can be selected by the user). This prop is optional. By adding it, you allow users to change the selection.
```vue-html - `custom`: these pills were created by the user. This prop is optional. Users can edit, add and remove any pill defined in this array. Note that the `custom` array should only contain pills that are either in `current` or in `others`.
<Pills v-model="interactiveModel" label="Tags" />
<Input label="Label" placeholder="Placeholder"></Input>
```
<Layout class="preview" style="padding:16px">
<Pills v-model="interactiveModel" label="Tags" />
<Input label="Label" placeholder="Placeholder"></Input>
</Layout>
## No pills ## No pills
```ts
const nullModel = ref({
current: [],
});
```
```vue-html ```vue-html
<Pills v-model="nullModel" /> <Pills v-model="nullModel" />
``` ```
@ -55,3 +55,56 @@ Select a set of pills from presets, and add and remove custom ones
<Layout class="preview" style="padding:16px"> <Layout class="preview" style="padding:16px">
<Pills v-model="nullModel" /> <Pills v-model="nullModel" />
</Layout> </Layout>
## Predefined list of pills
```ts
const staticModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"],
});
```
```vue-html
<Pills v-model="staticModel" label="Tags" />
```
<Layout class="preview" style="padding:16px">
<Pills v-model="staticModel" label="Tags" />
</Layout>
## Let users select and unselect pills
Select a set of pills from presets, and add and remove custom ones
```ts
const interactiveModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"],
others: ["#Melody", "#Rhythm"],
});
```
```vue-html
<Pills v-model="interactiveModel" label="Tags" />
```
<Layout class="preview" style="padding:16px">
<Pills v-model="interactiveModel" label="Tags" />
</Layout>
## Let users add, remove and edit custom pills
```ts
const customModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"],
others: ["#Melody", "#Rhythm"],
custom: ["#Noise"],
});
```
```vue-html
<Pills v-model="customModel" label="Custom Tags" can-add-pills />
```
<Layout class="preview" style="padding:16px">
<Pills v-model="customModel" label="Custom Tags" />
</Layout>