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)
|
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>
|
||||||
|
|
|
@ -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,14 +43,19 @@ whenInteractive(()=>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const deselectPill = (value:string) =>
|
const pillClicked = (value: string) =>
|
||||||
whenInteractive(()=>
|
model.value.custom?.includes(value) ? edit(value) : unselect(value)
|
||||||
model.value =
|
|
||||||
{...model.value,
|
const unselect = (value: string) =>
|
||||||
current : model.value.current.filter(v=>v!==value),
|
whenInteractive(() => model.value =
|
||||||
others : [value, ...(model.value.others || [])]
|
{...model.value,
|
||||||
}
|
current : model.value.current.filter(v=>v!==value),
|
||||||
)
|
others : [value, ...(model.value.others || [])]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const edit = (value: string) =>
|
||||||
|
whenInteractive(()=> editingValue.value = value)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -7,25 +7,63 @@ 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
|
||||||
|
|
||||||
|
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
|
```vue-html
|
||||||
<Pills v-model="staticModel" label="Tags" />
|
<Pills v-model="staticModel" label="Tags" />
|
||||||
```
|
```
|
||||||
|
@ -34,24 +72,39 @@ import Pills from "~/components/ui/Pills.vue"
|
||||||
<Pills v-model="staticModel" label="Tags" />
|
<Pills v-model="staticModel" label="Tags" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
## Let users select and unselect pills
|
||||||
|
|
||||||
Select a set of pills from presets, and add and remove custom ones
|
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
|
```vue-html
|
||||||
<Pills v-model="interactiveModel" label="Tags" />
|
<Pills v-model="interactiveModel" label="Tags" />
|
||||||
<Input label="Label" placeholder="Placeholder"></Input>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<Layout class="preview" style="padding:16px">
|
<Layout class="preview" style="padding:16px">
|
||||||
<Pills v-model="interactiveModel" label="Tags" />
|
<Pills v-model="interactiveModel" label="Tags" />
|
||||||
<Input label="Label" placeholder="Placeholder"></Input>
|
|
||||||
</Layout>
|
</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
|
```vue-html
|
||||||
<Pills v-model="nullModel" />
|
<Pills v-model="customModel" label="Custom Tags" can-add-pills />
|
||||||
```
|
```
|
||||||
|
|
||||||
<Layout class="preview" style="padding:16px">
|
<Layout class="preview" style="padding:16px">
|
||||||
<Pills v-model="nullModel" />
|
<Pills v-model="customModel" label="Custom Tags" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
Loading…
Reference in New Issue