refactor(ui): [WIP] allow user to add, edit and remove custom pills
This commit is contained in:
parent
97ff6c56aa
commit
a54a80fd02
|
@ -8,24 +8,34 @@ const handleClick = (event: MouseEvent) => {
|
|||
emit('click', event)
|
||||
}
|
||||
const props = defineProps<{ noUnderline?:true } & (PastelProps | ColorProps) & VariantProps & RaisedProps>()
|
||||
const model = defineModel<string>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
<component
|
||||
class="funkwhale pill"
|
||||
:class="props.noUnderline && 'no-underline'"
|
||||
:is="model ? 'label' : 'button'"
|
||||
v-bind="color(props, ['interactive', 'outline'])()"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<div v-if="!!$slots.image" class="pill-image">
|
||||
<slot name="image" />
|
||||
</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 />
|
||||
</div>
|
||||
</button>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style lang="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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch, watchEffect } from 'vue'
|
||||
import { ref, watchEffect, computed } from 'vue'
|
||||
|
||||
import { color } from '~/composables/color';
|
||||
|
||||
|
@ -10,23 +10,27 @@ const props = defineProps<{
|
|||
icon?: string,
|
||||
placeholder?: string,
|
||||
label?: string,
|
||||
customOptions?: true,
|
||||
|
||||
autofocus?: boolean,
|
||||
}>();
|
||||
|
||||
const customOptions = ref<string[]>([]);
|
||||
|
||||
const model = defineModel<{
|
||||
current : string[],
|
||||
others?: string[]
|
||||
}>({required:true})
|
||||
current: string[],
|
||||
others?: string[],
|
||||
custom?: string[],
|
||||
}>({ required: true });
|
||||
|
||||
const whenInteractive = (then:() => void) => {
|
||||
if(!model.value.others) return; then();
|
||||
}
|
||||
|
||||
const editingValue = ref(' ');
|
||||
|
||||
const isCustom = (value:string) =>
|
||||
model.value.current.includes(value)
|
||||
|
||||
const selectedLabel = ref("+");
|
||||
|
||||
whenInteractive(()=>
|
||||
watchEffect(() => {
|
||||
if (!model.value.others) return
|
||||
|
@ -39,14 +43,19 @@ whenInteractive(()=>
|
|||
}
|
||||
})
|
||||
)
|
||||
const deselectPill = (value:string) =>
|
||||
whenInteractive(()=>
|
||||
model.value =
|
||||
{...model.value,
|
||||
current : model.value.current.filter(v=>v!==value),
|
||||
others : [value, ...(model.value.others || [])]
|
||||
}
|
||||
)
|
||||
const pillClicked = (value: string) =>
|
||||
model.value.custom?.includes(value) ? edit(value) : unselect(value)
|
||||
|
||||
const unselect = (value: string) =>
|
||||
whenInteractive(() => model.value =
|
||||
{...model.value,
|
||||
current : model.value.current.filter(v=>v!==value),
|
||||
others : [value, ...(model.value.others || [])]
|
||||
}
|
||||
)
|
||||
|
||||
const edit = (value: string) =>
|
||||
whenInteractive(()=> editingValue.value = value)
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -67,23 +76,8 @@ const deselectPill = (value:string) =>
|
|||
v-bind="color({}, ['solid', 'default', 'secondary'])()"
|
||||
: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
|
||||
v-if="model.others"
|
||||
|
@ -97,6 +91,45 @@ const deselectPill = (value:string) =>
|
|||
{{ value }}
|
||||
</option>
|
||||
</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>
|
||||
</template>
|
||||
|
@ -110,6 +143,8 @@ const deselectPill = (value:string) =>
|
|||
font-weight: 600;
|
||||
}
|
||||
>.list {
|
||||
position: relative;
|
||||
|
||||
padding:4px;
|
||||
border-radius: 24px;
|
||||
|
||||
|
@ -124,9 +159,11 @@ const deselectPill = (value:string) =>
|
|||
padding: 2px;
|
||||
}
|
||||
>.dropdown{
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
border-radius: 15px;
|
||||
padding: 2px 11.25px;
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
background: transparent;
|
||||
appearance: auto;
|
||||
|
@ -137,10 +174,10 @@ const deselectPill = (value:string) =>
|
|||
color: inherit;
|
||||
}
|
||||
}
|
||||
&:hover>.list {
|
||||
&:hover:has(select)>.list {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
<script setup>
|
||||
import { computed, ref } from '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>
|
||||
|
||||
```ts
|
||||
import Pill from "~/components/ui/Pill.vue"
|
||||
import Pill from "~/components/ui/Pill.vue";
|
||||
```
|
||||
|
||||
# Pill
|
||||
|
||||
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 |
|
||||
| ------- | ----------------------------------------------------------------------------------------------- | --------- | ----------- | ---------------------- |
|
||||
|
@ -128,14 +134,14 @@ Funkwhale pills support a range of pastel colors to create visually appealing in
|
|||
Yellow 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>`.
|
||||
|
||||
```vue-html{2-4}
|
||||
<Pill>
|
||||
<template #image>
|
||||
<img src="/images/awesome-artist.png" />
|
||||
<div style="background-color: #0004" />
|
||||
</template>
|
||||
Awesome artist
|
||||
</Pill>
|
||||
|
@ -147,3 +153,27 @@ Image pills contain a small circular image on their left. These can be used for
|
|||
</template>
|
||||
Awesome artist
|
||||
</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>
|
||||
|
|
|
@ -7,25 +7,63 @@ import Input from '~/components/ui/Input.vue';
|
|||
|
||||
const nullModel = ref({
|
||||
current: [],
|
||||
others: []
|
||||
});
|
||||
|
||||
const staticModel = ref({
|
||||
current: ["#Noise", "#FieldRecording", "#Experiment"]
|
||||
current: ["#Noise", "#FieldRecording", "#Experiment"],
|
||||
});
|
||||
|
||||
const interactiveModel = ref({
|
||||
current: ["#Noise", "#FieldRecording", "#Experiment"],
|
||||
others: ["#Melody", "#Rhythm"]
|
||||
others: ["#Melody", "#Rhythm"],
|
||||
});
|
||||
|
||||
const customModel = ref({
|
||||
current: ["#Noise", "#FieldRecording", "#Experiment"],
|
||||
others: ["#Melody", "#Rhythm"],
|
||||
custom: ["#Noise"],
|
||||
});
|
||||
</script>
|
||||
|
||||
```ts
|
||||
import Pills from "~/components/ui/Pills.vue"
|
||||
import Pills from "~/components/ui/Pills.vue";
|
||||
```
|
||||
|
||||
# Pills
|
||||
|
||||
Show a dense list of pills representing tags, categories or options.
|
||||
Users can select a subset of given options and create new ones.
|
||||
|
||||
The model you provide will be mutated by this component:
|
||||
|
||||
- `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.
|
||||
- `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`.
|
||||
|
||||
## No pills
|
||||
|
||||
```ts
|
||||
const nullModel = ref({
|
||||
current: [],
|
||||
});
|
||||
```
|
||||
|
||||
```vue-html
|
||||
<Pills v-model="nullModel" />
|
||||
```
|
||||
|
||||
<Layout class="preview" style="padding:16px">
|
||||
<Pills v-model="nullModel" />
|
||||
</Layout>
|
||||
|
||||
## Predefined list of pills
|
||||
|
||||
```ts
|
||||
const staticModel = ref({
|
||||
current: ["#Noise", "#FieldRecording", "#Experiment"],
|
||||
});
|
||||
```
|
||||
|
||||
```vue-html
|
||||
<Pills v-model="staticModel" label="Tags" />
|
||||
```
|
||||
|
@ -34,24 +72,39 @@ import Pills from "~/components/ui/Pills.vue"
|
|||
<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" />
|
||||
<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
|
||||
## 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="nullModel" />
|
||||
<Pills v-model="customModel" label="Custom Tags" can-add-pills />
|
||||
```
|
||||
|
||||
<Layout class="preview" style="padding:16px">
|
||||
<Pills v-model="nullModel" />
|
||||
<Pills v-model="customModel" label="Custom Tags" />
|
||||
</Layout>
|
||||
|
|
Loading…
Reference in New Issue