feat: add shared select dialog

This commit is contained in:
Methapon2001 2024-12-20 11:13:45 +07:00 committed by Thanaphon Frappet
parent cd1848c5fb
commit 894900bd71

View file

@ -0,0 +1,193 @@
<script setup lang="ts" generic="T extends { id: string }">
import { Ref } from 'vue';
import { reactive, ref, watch } from 'vue';
import { CancelButton, MainButton } from 'components/button';
import DialogContainer from './DialogContainer.vue';
import DialogHeader from './DialogHeader.vue';
const props = defineProps<{
getList: (search?: string) => T[] | Promise<T[]>;
dialog: {
title: string;
};
preselectedItem?: T[];
disabledItemId?: T['id'][];
/** @returns true - to auto close dialog */
onSubmit: (selected: T[]) => void | boolean | Promise<void | boolean>;
}>();
const open = defineModel<boolean>('open', { default: false });
const list: Ref<T[]> = ref([]);
const selected: Ref<T[]> = ref([]);
const state = reactive({
search: '',
});
function clean() {
list.value = [];
selected.value = [];
open.value = false;
}
function selectedIndex(item: T) {
return selected.value.findIndex((v) => v.id === item.id);
}
function toggleSelect(item: T) {
if (props.disabledItemId?.some((id) => id === item.id)) return;
const index = selectedIndex(item);
if (index === -1) {
selected.value.push(item);
} else {
selected.value.splice(index, 1);
}
}
function init() {
if (props.preselectedItem) {
selected.value = JSON.parse(JSON.stringify(props.preselectedItem));
}
getList();
}
async function getList() {
list.value = await props.getList(state.search);
}
async function submit() {
const result = await props.onSubmit(selected.value);
if (typeof result === 'boolean') open.value = !result;
}
watch(() => state.search, getList);
</script>
<template>
<DialogContainer v-model="open" @open="init" @close="clean">
<template #header>
<DialogHeader :title="dialog.title">
<template #title-before>
<span class="q-mr-auto"></span>
</template>
<template #title-after>
<span class="q-ml-auto" v-if="!$slots['title-after']" />
<div class="q-ml-auto" v-else><slot name="title-after" /></div>
</template>
</DialogHeader>
</template>
<div class="column q-pa-md full-height">
<section class="row justify-end q-mb-md">
<q-input
for="input-search"
outlined
dense
:label="$t('general.search')"
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
v-model="state.search"
debounce="500"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
</section>
<!-- wrapper -->
<div class="col scroll">
<section
:class="{ ['items-center']: list.length === 0 }"
class="row q-col-gutter-md"
>
<div
style="display: inline-block; margin-inline: auto"
v-if="list.length === 0"
>
<slot name="empty" :data="{ notFound: !!state.search }" />
</div>
<div
:key="item.id"
v-for="(item, index) in list.map((data) => ({
...data,
_selectedIndex: selectedIndex(data),
}))"
class="col-2"
>
<button
class="selectable-item full-width"
:class="{
['selectable-item__selected']: item._selectedIndex !== -1,
['selectable-item__disabled']: disabledItemId?.some(
(id) => id === item.id,
),
}"
@click="toggleSelect(item)"
>
<span class="selectable-item__pos">
{{ item._selectedIndex + 1 }}
</span>
<slot name="card" :data="{ index, item }" />
</button>
</div>
</section>
</div>
</div>
<template #footer>
<div class="q-gutter-x-xs q-ml-auto">
<CancelButton outlined @click="clean" />
<MainButton icon="mdi-check" color="207 96% 32%" solid @click="submit">
{{ $t('general.select') }}
</MainButton>
</div>
</template>
</DialogContainer>
</template>
<style scoped>
.selectable-item {
padding: 0;
appearance: none;
border: none;
background: transparent;
position: relative;
color: inherit;
& > .selectable-item__pos {
display: none;
}
}
.selectable-item__selected {
& > :deep(*) {
border: 1px solid var(--_color, var(--brand-1)) !important;
}
& > .selectable-item__pos {
display: block;
position: absolute;
margin: var(--size-2);
right: 0;
top: 0;
border-radius: 50%;
width: 20px;
height: 20px;
color: var(--surface-1);
background: var(--brand-1);
color: white;
}
}
.selectable-item__disabled {
filter: grayscale(1);
opacity: 0.5;
& :deep(*) {
cursor: not-allowed;
}
}
</style>