Extract quick entry category selector
This commit is contained in:
parent
322ee737fd
commit
e7250c4cf7
|
|
@ -0,0 +1,282 @@
|
|||
<script setup lang="ts">
|
||||
import type { EntryType } from "@cents/domain";
|
||||
import { ChevronRight } from "@lucide/vue";
|
||||
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
import {
|
||||
categories,
|
||||
entryTypes,
|
||||
type Category,
|
||||
type EntryTypeOption,
|
||||
} from "../data/categories";
|
||||
|
||||
type SelectorLevel = "type" | "category" | "subcategory";
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
entryType: EntryType;
|
||||
categoryPath: Category[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:entryType": [value: EntryType];
|
||||
"update:categoryPath": [value: Category[]];
|
||||
confirm: [];
|
||||
}>();
|
||||
|
||||
const selectorLevel = ref<SelectorLevel>("type");
|
||||
const axisOffset = ref(0);
|
||||
let axisPointerId: number | null = null;
|
||||
let axisDragStartY = 0;
|
||||
let axisDragStartOffset = 0;
|
||||
let axisDragged = false;
|
||||
let axisLastY = 0;
|
||||
let axisLastTime = 0;
|
||||
let axisVelocity = 0;
|
||||
let axisInertiaFrame: number | null = null;
|
||||
|
||||
const typeOption = computed(() => entryTypes.find((item) => item.id === props.entryType)!);
|
||||
const selectedCategory = computed(() => props.categoryPath.at(-1));
|
||||
const selectedParent = computed(() => props.categoryPath[0]);
|
||||
const selectorOptions = computed<(EntryTypeOption | Category)[]>(() => {
|
||||
if (selectorLevel.value === "type") {
|
||||
const current = entryTypes.find((option) => option.id === props.entryType);
|
||||
return [...entryTypes.filter((option) => option.id !== props.entryType), ...(current ? [current] : [])];
|
||||
}
|
||||
if (selectorLevel.value === "category") return categories[props.entryType];
|
||||
return selectedParent.value?.children ?? [];
|
||||
});
|
||||
const upperSelectorOptions = computed(() => {
|
||||
const count = Math.min(2, Math.floor(selectorOptions.value.length / 2));
|
||||
return selectorOptions.value.slice(0, count);
|
||||
});
|
||||
const upperAxisHeight = computed(() => {
|
||||
const count = upperSelectorOptions.value.length;
|
||||
return count * 72 + Math.max(0, count - 1) * 10 + 34;
|
||||
});
|
||||
const axisBaseOffset = computed(() => upperSelectorOptions.value.length * 82);
|
||||
const axisMaxOffset = computed(() =>
|
||||
Math.max(0, (selectorOptions.value.length - upperSelectorOptions.value.length - 2) * 82),
|
||||
);
|
||||
const selectorPath = computed<(EntryTypeOption | Category)[]>(() => {
|
||||
if (selectorLevel.value === "type") return [];
|
||||
if (selectorLevel.value === "category") return [typeOption.value];
|
||||
return selectedParent.value ? [typeOption.value, selectedParent.value] : [typeOption.value];
|
||||
});
|
||||
|
||||
watch(() => props.open, (open) => {
|
||||
if (open) resetAxisScroll();
|
||||
else stopAxisInertia();
|
||||
});
|
||||
|
||||
onBeforeUnmount(stopAxisInertia);
|
||||
|
||||
function chooseSelectorOption(option: EntryTypeOption | Category) {
|
||||
if (selectorLevel.value === "type") {
|
||||
const nextType = option.id as EntryType;
|
||||
if (nextType !== props.entryType) emit("update:categoryPath", []);
|
||||
emit("update:entryType", nextType);
|
||||
selectorLevel.value = "category";
|
||||
resetAxisScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
const category = option as Category;
|
||||
if (selectorLevel.value === "category") {
|
||||
const currentChild = selectedParent.value?.id === category.id ? props.categoryPath[1] : undefined;
|
||||
emit("update:categoryPath", currentChild ? [category, currentChild] : [category]);
|
||||
if (category.children?.length) {
|
||||
selectorLevel.value = "subcategory";
|
||||
resetAxisScroll();
|
||||
} else {
|
||||
emit("confirm");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedParent.value) return;
|
||||
emit("update:categoryPath", [selectedParent.value, category]);
|
||||
emit("confirm");
|
||||
}
|
||||
|
||||
function choosePathLevel(index: number) {
|
||||
selectorLevel.value = index === 0 ? "type" : "category";
|
||||
resetAxisScroll();
|
||||
}
|
||||
|
||||
function isSelectorOptionSelected(option: EntryTypeOption | Category) {
|
||||
if (selectorLevel.value === "type") return option.id === props.entryType;
|
||||
if (selectorLevel.value === "category") return option.id === selectedParent.value?.id;
|
||||
return props.categoryPath.length > 1 && option.id === selectedCategory.value?.id;
|
||||
}
|
||||
|
||||
function resetAxisScroll() {
|
||||
stopAxisInertia();
|
||||
axisOffset.value = 0;
|
||||
}
|
||||
|
||||
function clampAxisOffset(value: number) {
|
||||
return Math.max(0, Math.min(axisMaxOffset.value, value));
|
||||
}
|
||||
|
||||
function startAxisDrag(event: PointerEvent) {
|
||||
stopAxisInertia();
|
||||
axisPointerId = event.pointerId;
|
||||
axisDragStartY = event.clientY;
|
||||
axisDragStartOffset = axisOffset.value;
|
||||
axisDragged = false;
|
||||
axisLastY = event.clientY;
|
||||
axisLastTime = performance.now();
|
||||
axisVelocity = 0;
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
function moveAxisDrag(event: PointerEvent) {
|
||||
if (axisPointerId !== event.pointerId) return;
|
||||
const distance = axisDragStartY - event.clientY;
|
||||
if (Math.abs(distance) > 5) axisDragged = true;
|
||||
axisOffset.value = clampAxisOffset(axisDragStartOffset + distance);
|
||||
|
||||
const now = performance.now();
|
||||
const elapsed = Math.max(1, now - axisLastTime);
|
||||
const instantaneousVelocity = (axisLastY - event.clientY) / elapsed;
|
||||
axisVelocity = axisVelocity * 0.65 + instantaneousVelocity * 0.35;
|
||||
axisLastY = event.clientY;
|
||||
axisLastTime = now;
|
||||
}
|
||||
|
||||
function endAxisDrag(event: PointerEvent) {
|
||||
if (axisPointerId !== event.pointerId) return;
|
||||
axisPointerId = null;
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
||||
if (axisDragged) startAxisInertia();
|
||||
}
|
||||
|
||||
function handleAxisClick(event: MouseEvent) {
|
||||
if (!axisDragged) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
axisDragged = false;
|
||||
}
|
||||
|
||||
function scrollAxisBy(delta: number) {
|
||||
stopAxisInertia();
|
||||
axisOffset.value = clampAxisOffset(axisOffset.value + delta);
|
||||
}
|
||||
|
||||
function startAxisInertia() {
|
||||
if (Math.abs(axisVelocity) < 0.04) return;
|
||||
let previousTime = performance.now();
|
||||
|
||||
const step = (now: number) => {
|
||||
const elapsed = Math.min(32, now - previousTime);
|
||||
previousTime = now;
|
||||
const current = axisOffset.value;
|
||||
const next = clampAxisOffset(current + axisVelocity * elapsed);
|
||||
axisOffset.value = next;
|
||||
|
||||
if (next === current && (next === 0 || next === axisMaxOffset.value)) {
|
||||
stopAxisInertia();
|
||||
return;
|
||||
}
|
||||
|
||||
axisVelocity *= Math.pow(0.91, elapsed / 16);
|
||||
if (Math.abs(axisVelocity) < 0.025) {
|
||||
stopAxisInertia();
|
||||
return;
|
||||
}
|
||||
axisInertiaFrame = requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
axisInertiaFrame = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
function stopAxisInertia() {
|
||||
if (axisInertiaFrame !== null) cancelAnimationFrame(axisInertiaFrame);
|
||||
axisInertiaFrame = null;
|
||||
axisVelocity = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="coordinate-selector">
|
||||
<div v-if="open" class="category-coordinate" aria-label="二维分类选择器" @click.stop>
|
||||
<div class="selector-path" aria-label="已选分类路径">
|
||||
<template v-for="(option, index) in selectorPath" :key="option.id">
|
||||
<ChevronRight v-if="index > 0" :size="18" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
:style="{ '--path-color': option.color }"
|
||||
:aria-label="`返回${option.label}层重新选择`"
|
||||
@click="choosePathLevel(index)"
|
||||
>
|
||||
<component :is="option.icon" :size="22" />
|
||||
<small>{{ option.label }}</small>
|
||||
</button>
|
||||
</template>
|
||||
<ChevronRight v-if="selectorPath.length" :size="18" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="upperSelectorOptions.length"
|
||||
class="candidate-above-axis"
|
||||
:style="{ height: `${upperAxisHeight}px` }"
|
||||
>
|
||||
<div
|
||||
class="candidate-axis-viewport"
|
||||
@pointerdown="startAxisDrag"
|
||||
@pointermove="moveAxisDrag"
|
||||
@pointerup="endAxisDrag"
|
||||
@pointercancel="endAxisDrag"
|
||||
@click.capture="handleAxisClick"
|
||||
@wheel.prevent="scrollAxisBy($event.deltaY)"
|
||||
>
|
||||
<div class="candidate-axis-list" :style="{ transform: `translate3d(0, -${axisOffset}px, 0)` }">
|
||||
<button
|
||||
v-for="option in selectorOptions"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
class="coordinate-option"
|
||||
:class="{ selected: isSelectorOptionSelected(option) }"
|
||||
:style="{ '--option-color': option.color, '--option-tint': option.tint }"
|
||||
@click="chooseSelectorOption(option)"
|
||||
>
|
||||
<span class="coordinate-icon"><component :is="option.icon" :size="24" /></span>
|
||||
<span>{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="candidate-below-axis">
|
||||
<div
|
||||
class="candidate-axis-viewport"
|
||||
@pointerdown="startAxisDrag"
|
||||
@pointermove="moveAxisDrag"
|
||||
@pointerup="endAxisDrag"
|
||||
@pointercancel="endAxisDrag"
|
||||
@click.capture="handleAxisClick"
|
||||
@wheel.prevent="scrollAxisBy($event.deltaY)"
|
||||
>
|
||||
<div
|
||||
class="candidate-axis-list"
|
||||
:style="{ transform: `translate3d(0, -${axisBaseOffset + axisOffset}px, 0)` }"
|
||||
>
|
||||
<button
|
||||
v-for="option in selectorOptions"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
class="coordinate-option"
|
||||
:class="{ selected: isSelectorOptionSelected(option) }"
|
||||
:style="{ '--option-color': option.color, '--option-tint': option.tint }"
|
||||
@click="chooseSelectorOption(option)"
|
||||
>
|
||||
<span class="coordinate-icon"><component :is="option.icon" :size="24" /></span>
|
||||
<span>{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import type { EntryType } from "@cents/domain";
|
||||
import { BookOpen, CalendarDays, Check, ChevronDown, ChevronRight, Delete as BackspaceIcon, X } from "@lucide/vue";
|
||||
import { BookOpen, CalendarDays, Check, ChevronDown, Delete as BackspaceIcon, X } from "@lucide/vue";
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
categories,
|
||||
entryTypes,
|
||||
type Category,
|
||||
type EntryTypeOption,
|
||||
} from "../data/categories";
|
||||
import { entryTypes, type Category } from "../data/categories";
|
||||
import CategoryCoordinateSelector from "./CategoryCoordinateSelector.vue";
|
||||
|
||||
type SavedEntry = {
|
||||
type: EntryType;
|
||||
|
|
@ -18,8 +14,6 @@ type SavedEntry = {
|
|||
ledgerIds: string[];
|
||||
};
|
||||
|
||||
type SelectorLevel = "type" | "category" | "subcategory";
|
||||
|
||||
const props = defineProps<{
|
||||
ledgerId: string;
|
||||
ledgerName: string;
|
||||
|
|
@ -39,23 +33,12 @@ const note = ref("");
|
|||
const occurredAt = ref(toLocalDateTime(new Date()));
|
||||
const dateInput = ref<HTMLInputElement | null>(null);
|
||||
const selectorOpen = ref(false);
|
||||
const selectorLevel = ref<SelectorLevel>("type");
|
||||
const noteEditing = ref(false);
|
||||
const ledgerPickerOpen = ref(false);
|
||||
const selectedLedgerIds = ref([...new Set([props.ledgerId, props.personalLedgerId].filter(Boolean))]);
|
||||
const axisOffset = ref(0);
|
||||
let axisPointerId: number | null = null;
|
||||
let axisDragStartY = 0;
|
||||
let axisDragStartOffset = 0;
|
||||
let axisDragged = false;
|
||||
let axisLastY = 0;
|
||||
let axisLastTime = 0;
|
||||
let axisVelocity = 0;
|
||||
let axisInertiaFrame: number | null = null;
|
||||
|
||||
const typeOption = computed(() => entryTypes.find((item) => item.id === entryType.value)!);
|
||||
const selectedCategory = computed(() => categoryPath.value.at(-1));
|
||||
const selectedParent = computed(() => categoryPath.value[0]);
|
||||
const categoryPathLabel = computed(() =>
|
||||
[typeOption.value.label, ...categoryPath.value.map((category) => category.label)].join(">"),
|
||||
);
|
||||
|
|
@ -91,31 +74,6 @@ const canSave = computed(() => {
|
|||
const amount = Math.round(calculatedAmount.value * 100);
|
||||
return Number.isFinite(amount) && amount > 0 && occurredAtValid.value;
|
||||
});
|
||||
const selectorOptions = computed<(EntryTypeOption | Category)[]>(() => {
|
||||
if (selectorLevel.value === "type") {
|
||||
const current = entryTypes.find((option) => option.id === entryType.value);
|
||||
return [...entryTypes.filter((option) => option.id !== entryType.value), ...(current ? [current] : [])];
|
||||
}
|
||||
if (selectorLevel.value === "category") return categories[entryType.value];
|
||||
return selectedParent.value?.children ?? [];
|
||||
});
|
||||
const upperSelectorOptions = computed(() => {
|
||||
const count = Math.min(2, Math.floor(selectorOptions.value.length / 2));
|
||||
return selectorOptions.value.slice(0, count);
|
||||
});
|
||||
const upperAxisHeight = computed(() => {
|
||||
const count = upperSelectorOptions.value.length;
|
||||
return count * 72 + Math.max(0, count - 1) * 10 + 34;
|
||||
});
|
||||
const axisBaseOffset = computed(() => upperSelectorOptions.value.length * 82);
|
||||
const axisMaxOffset = computed(() =>
|
||||
Math.max(0, (selectorOptions.value.length - upperSelectorOptions.value.length - 2) * 82),
|
||||
);
|
||||
const selectorPath = computed<(EntryTypeOption | Category)[]>(() => {
|
||||
if (selectorLevel.value === "type") return [];
|
||||
if (selectorLevel.value === "category") return [typeOption.value];
|
||||
return selectedParent.value ? [typeOption.value, selectedParent.value] : [typeOption.value];
|
||||
});
|
||||
|
||||
function toLocalDateTime(date: Date) {
|
||||
const year = date.getFullYear();
|
||||
|
|
@ -187,7 +145,6 @@ function selectOperator(operator: "+" | "-") {
|
|||
function openSelector() {
|
||||
if (selectorOpen.value) return;
|
||||
selectorOpen.value = true;
|
||||
resetAxisScroll();
|
||||
}
|
||||
|
||||
function openDatePicker() {
|
||||
|
|
@ -198,132 +155,6 @@ function openDatePicker() {
|
|||
else input.click();
|
||||
}
|
||||
|
||||
function chooseSelectorOption(option: EntryTypeOption | Category) {
|
||||
if (selectorLevel.value === "type") {
|
||||
const typeChanged = option.id !== entryType.value;
|
||||
entryType.value = option.id as EntryType;
|
||||
if (typeChanged) categoryPath.value = [];
|
||||
selectorLevel.value = "category";
|
||||
resetAxisScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
const category = option as Category;
|
||||
if (selectorLevel.value === "category") {
|
||||
const currentChild = selectedParent.value?.id === category.id ? categoryPath.value[1] : undefined;
|
||||
categoryPath.value = currentChild ? [category, currentChild] : [category];
|
||||
if (category.children?.length) {
|
||||
selectorLevel.value = "subcategory";
|
||||
resetAxisScroll();
|
||||
}
|
||||
else confirmCategory();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedParent.value) return;
|
||||
categoryPath.value = [selectedParent.value, category];
|
||||
confirmCategory();
|
||||
}
|
||||
|
||||
function choosePathLevel(index: number) {
|
||||
selectorLevel.value = index === 0 ? "type" : "category";
|
||||
resetAxisScroll();
|
||||
}
|
||||
|
||||
function isSelectorOptionSelected(option: EntryTypeOption | Category) {
|
||||
if (selectorLevel.value === "type") return option.id === entryType.value;
|
||||
if (selectorLevel.value === "category") return option.id === selectedParent.value?.id;
|
||||
return categoryPath.value.length > 1 && option.id === selectedCategory.value?.id;
|
||||
}
|
||||
|
||||
function resetAxisScroll() {
|
||||
stopAxisInertia();
|
||||
axisOffset.value = 0;
|
||||
}
|
||||
|
||||
function clampAxisOffset(value: number) {
|
||||
return Math.max(0, Math.min(axisMaxOffset.value, value));
|
||||
}
|
||||
|
||||
function startAxisDrag(event: PointerEvent) {
|
||||
stopAxisInertia();
|
||||
axisPointerId = event.pointerId;
|
||||
axisDragStartY = event.clientY;
|
||||
axisDragStartOffset = axisOffset.value;
|
||||
axisDragged = false;
|
||||
axisLastY = event.clientY;
|
||||
axisLastTime = performance.now();
|
||||
axisVelocity = 0;
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
function moveAxisDrag(event: PointerEvent) {
|
||||
if (axisPointerId !== event.pointerId) return;
|
||||
const distance = axisDragStartY - event.clientY;
|
||||
if (Math.abs(distance) > 5) axisDragged = true;
|
||||
axisOffset.value = clampAxisOffset(axisDragStartOffset + distance);
|
||||
|
||||
const now = performance.now();
|
||||
const elapsed = Math.max(1, now - axisLastTime);
|
||||
const instantaneousVelocity = (axisLastY - event.clientY) / elapsed;
|
||||
axisVelocity = axisVelocity * 0.65 + instantaneousVelocity * 0.35;
|
||||
axisLastY = event.clientY;
|
||||
axisLastTime = now;
|
||||
}
|
||||
|
||||
function endAxisDrag(event: PointerEvent) {
|
||||
if (axisPointerId !== event.pointerId) return;
|
||||
axisPointerId = null;
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
||||
if (axisDragged) startAxisInertia();
|
||||
}
|
||||
|
||||
function handleAxisClick(event: MouseEvent) {
|
||||
if (!axisDragged) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
axisDragged = false;
|
||||
}
|
||||
|
||||
function scrollAxisBy(delta: number) {
|
||||
stopAxisInertia();
|
||||
axisOffset.value = clampAxisOffset(axisOffset.value + delta);
|
||||
}
|
||||
|
||||
function startAxisInertia() {
|
||||
if (Math.abs(axisVelocity) < 0.04) return;
|
||||
let previousTime = performance.now();
|
||||
|
||||
const step = (now: number) => {
|
||||
const elapsed = Math.min(32, now - previousTime);
|
||||
previousTime = now;
|
||||
const current = axisOffset.value;
|
||||
const next = clampAxisOffset(current + axisVelocity * elapsed);
|
||||
axisOffset.value = next;
|
||||
|
||||
if (next === current && (next === 0 || next === axisMaxOffset.value)) {
|
||||
stopAxisInertia();
|
||||
return;
|
||||
}
|
||||
|
||||
axisVelocity *= Math.pow(0.91, elapsed / 16);
|
||||
if (Math.abs(axisVelocity) < 0.025) {
|
||||
stopAxisInertia();
|
||||
return;
|
||||
}
|
||||
axisInertiaFrame = requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
axisInertiaFrame = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
function stopAxisInertia() {
|
||||
if (axisInertiaFrame !== null) cancelAnimationFrame(axisInertiaFrame);
|
||||
axisInertiaFrame = null;
|
||||
axisVelocity = 0;
|
||||
}
|
||||
|
||||
function confirmCategory() {
|
||||
selectorOpen.value = false;
|
||||
}
|
||||
|
|
@ -425,86 +256,12 @@ function saveEntry() {
|
|||
<span>{{ selectorOpen ? "确定" : categoryDisplayLabel }}</span>
|
||||
</button>
|
||||
|
||||
<Transition name="coordinate-selector">
|
||||
<div v-if="selectorOpen" class="category-coordinate" aria-label="二维分类选择器" @click.stop>
|
||||
<div class="selector-path" aria-label="已选分类路径">
|
||||
<template v-for="(option, index) in selectorPath" :key="option.id">
|
||||
<ChevronRight v-if="index > 0" :size="18" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
:style="{ '--path-color': option.color }"
|
||||
:aria-label="`返回${option.label}层重新选择`"
|
||||
@click="choosePathLevel(index)"
|
||||
>
|
||||
<component :is="option.icon" :size="22" />
|
||||
<small>{{ option.label }}</small>
|
||||
</button>
|
||||
</template>
|
||||
<ChevronRight v-if="selectorPath.length" :size="18" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="upperSelectorOptions.length"
|
||||
class="candidate-above-axis"
|
||||
:style="{ height: `${upperAxisHeight}px` }"
|
||||
>
|
||||
<div
|
||||
class="candidate-axis-viewport"
|
||||
@pointerdown="startAxisDrag"
|
||||
@pointermove="moveAxisDrag"
|
||||
@pointerup="endAxisDrag"
|
||||
@pointercancel="endAxisDrag"
|
||||
@click.capture="handleAxisClick"
|
||||
@wheel.prevent="scrollAxisBy($event.deltaY)"
|
||||
>
|
||||
<div class="candidate-axis-list" :style="{ transform: `translate3d(0, -${axisOffset}px, 0)` }">
|
||||
<button
|
||||
v-for="option in selectorOptions"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
class="coordinate-option"
|
||||
:class="{ selected: isSelectorOptionSelected(option) }"
|
||||
:style="{ '--option-color': option.color, '--option-tint': option.tint }"
|
||||
@click="chooseSelectorOption(option)"
|
||||
>
|
||||
<span class="coordinate-icon"><component :is="option.icon" :size="24" /></span>
|
||||
<span>{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="candidate-below-axis">
|
||||
<div
|
||||
class="candidate-axis-viewport"
|
||||
@pointerdown="startAxisDrag"
|
||||
@pointermove="moveAxisDrag"
|
||||
@pointerup="endAxisDrag"
|
||||
@pointercancel="endAxisDrag"
|
||||
@click.capture="handleAxisClick"
|
||||
@wheel.prevent="scrollAxisBy($event.deltaY)"
|
||||
>
|
||||
<div
|
||||
class="candidate-axis-list"
|
||||
:style="{ transform: `translate3d(0, -${axisBaseOffset + axisOffset}px, 0)` }"
|
||||
>
|
||||
<button
|
||||
v-for="option in selectorOptions"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
class="coordinate-option"
|
||||
:class="{ selected: isSelectorOptionSelected(option) }"
|
||||
:style="{ '--option-color': option.color, '--option-tint': option.tint }"
|
||||
@click="chooseSelectorOption(option)"
|
||||
>
|
||||
<span class="coordinate-icon"><component :is="option.icon" :size="24" /></span>
|
||||
<span>{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<CategoryCoordinateSelector
|
||||
v-model:entry-type="entryType"
|
||||
v-model:category-path="categoryPath"
|
||||
:open="selectorOpen"
|
||||
@confirm="confirmCategory"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue