two column list item selection supports filtering content (#38)

This commit is contained in:
MaysWind
2025-02-06 22:37:02 +08:00
parent 29d7ee09c8
commit b6efa91879
14 changed files with 177 additions and 36 deletions
@@ -1,4 +1,6 @@
import { type Ref } from 'vue';
import { type Ref, ref, computed } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { getItemByKeyValue, getPrimaryValueBySecondaryValue } from '@/lib/common.ts';
@@ -29,12 +31,94 @@ export interface CommonTwoColumnListItemSelectionProps {
secondaryIconType?: string;
secondaryColorField?: string;
secondaryHiddenField?: string;
items: unknown[];
enableFilter?: boolean;
filterPlaceholder?: string;
filterNoItemsText?: string;
items: Record<string, unknown>[];
}
export function useTwoColumnListItemSelectionBase(props: CommonTwoColumnListItemSelectionProps) {
const { ti } = useI18n();
const filterContent = ref<string>('');
const filteredItems = computed<Record<string, unknown>[]>(() => {
const finalItems: Record<string, unknown>[] = [];
const items = props.items;
for (const item of items) {
if (props.primaryHiddenField && item[props.primaryHiddenField]) {
continue;
}
if (!props.enableFilter || !filterContent.value) {
finalItems.push(item);
continue;
}
if (props.primaryTitleField) {
const title = ti(item[props.primaryTitleField] as string, !!props.primaryTitleI18n);
if (title.toLowerCase().indexOf(filterContent.value.toLowerCase()) >= 0) {
finalItems.push(item);
continue;
}
}
if (props.primarySubItemsField) {
if (getFilteredSubItems(item).length > 0) {
finalItems.push(item);
}
}
}
return finalItems;
});
function getFilteredSubItems(selectedPrimaryItem: unknown): Record<string, unknown>[] {
const finalItems: Record<string, unknown>[] = [];
if (!selectedPrimaryItem || !props.primarySubItemsField) {
return finalItems;
}
const subItems = (selectedPrimaryItem as Record<string, unknown>)[props.primarySubItemsField] as Record<string, unknown>[];
let primaryTitleHasFilterContent = false;
if (props.primaryTitleField) {
const title = ti((selectedPrimaryItem as Record<string, unknown>)[props.primaryTitleField] as string, !!props.primaryTitleI18n);
primaryTitleHasFilterContent = title.toLowerCase().indexOf(filterContent.value.toLowerCase()) >= 0;
}
for (const subItem of subItems) {
if (props.secondaryHiddenField && subItem[props.secondaryHiddenField]) {
continue;
}
if (!props.enableFilter || !filterContent.value) {
finalItems.push(subItem);
continue;
}
if (primaryTitleHasFilterContent) {
finalItems.push(subItem);
continue;
}
if (props.secondaryTitleField && filterContent.value) {
const title = ti(subItem[props.secondaryTitleField] as string, !!props.secondaryTitleI18n);
if (title.toLowerCase().indexOf(filterContent.value.toLowerCase()) >= 0) {
finalItems.push(subItem);
}
}
}
return finalItems;
}
function getCurrentPrimaryValueBySecondaryValue(secondaryValue: unknown): unknown {
return getPrimaryValueBySecondaryValue(props.items as Record<string, Record<string, unknown>[]>[] | Record<string, Record<string, Record<string, unknown>[]>>, props.primarySubItemsField, props.primaryValueField, props.primaryHiddenField, props.secondaryValueField, props.secondaryHiddenField, secondaryValue);
return getPrimaryValueBySecondaryValue(props.items as Record<string, Record<string, unknown>[]>[], props.primarySubItemsField, props.primaryValueField, props.primaryHiddenField, props.secondaryValueField, props.secondaryHiddenField, secondaryValue);
}
function isSecondaryValueSelected(currentSecondaryValue: unknown, subItem: unknown): boolean {
@@ -45,17 +129,17 @@ export function useTwoColumnListItemSelectionBase(props: CommonTwoColumnListItem
}
}
function getSelectedPrimaryItem(currentPrimaryValue: unknown) {
function getSelectedPrimaryItem(currentPrimaryValue: unknown): unknown {
if (props.primaryValueField) {
return getItemByKeyValue(props.items as Record<string, unknown>[] | Record<string, Record<string, unknown>>, currentPrimaryValue, props.primaryValueField);
return getItemByKeyValue(props.items, currentPrimaryValue, props.primaryValueField);
} else {
return currentPrimaryValue;
}
}
function getSelectedSecondaryItem(currentSecondaryValue: unknown, selectedPrimaryItem: unknown) {
function getSelectedSecondaryItem(currentSecondaryValue: unknown, selectedPrimaryItem: unknown): unknown {
if (currentSecondaryValue && selectedPrimaryItem && (selectedPrimaryItem as Record<string, unknown>)[props.primarySubItemsField]) {
return getItemByKeyValue((selectedPrimaryItem as Record<string, unknown>)[props.primarySubItemsField] as Record<string, unknown>[] | Record<string, Record<string, unknown>>, currentSecondaryValue, props.secondaryValueField as string);
return getItemByKeyValue((selectedPrimaryItem as Record<string, unknown>)[props.primarySubItemsField] as Record<string, unknown>[], currentSecondaryValue, props.secondaryValueField as string);
} else {
return null;
}
@@ -78,7 +162,12 @@ export function useTwoColumnListItemSelectionBase(props: CommonTwoColumnListItem
}
return {
// states
filterContent,
// computed states
filteredItems,
// functions
getFilteredSubItems,
getCurrentPrimaryValueBySecondaryValue,
isSecondaryValueSelected,
getSelectedPrimaryItem,
+25 -16
View File
@@ -14,11 +14,11 @@
<template #selection>
<div class="d-flex align-center text-truncate cursor-pointer">
<span class="text-truncate" v-if="customSelectionPrimaryText">{{ customSelectionPrimaryText }}</span>
<v-icon class="disabled" :icon="icons.chevronRight" size="23" v-if="customSelectionPrimaryText && customSelectionSecondaryText" />
<v-icon class="disabled" :icon="mdiChevronRight" size="23" v-if="customSelectionPrimaryText && customSelectionSecondaryText" />
<span class="text-truncate" v-if="customSelectionPrimaryText && customSelectionSecondaryText">{{ customSelectionSecondaryText }}</span>
<span class="text-truncate" v-if="!customSelectionPrimaryText && !selectedPrimaryItem && !selectedSecondaryItem">{{ noSelectionText }}</span>
<span class="text-truncate" v-if="!customSelectionPrimaryText && showSelectionPrimaryText && selectedPrimaryItem">{{ selectionPrimaryItemText }}</span>
<v-icon class="disabled" :icon="icons.chevronRight" size="23" v-if="!customSelectionPrimaryText && showSelectionPrimaryText && selectedPrimaryItem && selectedSecondaryItem" />
<v-icon class="disabled" :icon="mdiChevronRight" size="23" v-if="!customSelectionPrimaryText && showSelectionPrimaryText && selectedPrimaryItem && selectedSecondaryItem" />
<ItemIcon class="mr-2" icon-type="account" size="21.5px"
:icon-id="selectedSecondaryItem && secondaryIconField ? (selectedSecondaryItem as Record<string, unknown>)[secondaryIconField] : null"
:color="selectedSecondaryItem && secondaryColorField ? (selectedSecondaryItem as Record<string, unknown>)[secondaryColorField] : null"
@@ -28,13 +28,21 @@
</template>
<template #no-data>
<div ref="dropdownMenu" class="two-column-list-container">
<div class="mx-2 mt-2" v-if="enableFilter">
<v-text-field density="compact"
:prepend-inner-icon="mdiMagnify"
:placeholder="filterPlaceholder"
v-model="filterContent"></v-text-field>
</div>
<div class="mx-4 my-3" v-show="!filteredItems || !filteredItems.length">
{{ filterNoItemsText }}
</div>
<div ref="dropdownMenu" class="two-column-list-container" v-show="filteredItems && filteredItems.length">
<div class="primary-list-container">
<v-list :class="{ 'list-item-with-header': !!primaryHeaderField, 'list-item-with-footer': !!primaryFooterField }">
<v-list-item :class="{ 'primary-list-item-selected v-list-item--active text-primary': item === selectedPrimaryItem }"
:key="primaryKeyField ? (item as Record<string, unknown>)[primaryKeyField] : item"
v-for="item in items"
v-show="item && (!primaryHiddenField || !(item as Record<string, unknown>)[primaryHiddenField])"
v-for="item in filteredItems"
@click="onPrimaryItemClicked(item)">
<template #prepend>
<ItemIcon class="mr-2" :icon-type="primaryIconType"
@@ -53,8 +61,7 @@
v-if="selectedPrimaryItem && primarySubItemsField && (selectedPrimaryItem as Record<string, unknown>)[primarySubItemsField]">
<v-list-item :class="{ 'secondary-list-item-selected v-list-item--active text-primary': isSecondarySelected(subItem) }"
:key="secondaryKeyField ? subItem[secondaryKeyField] : subItem"
v-for="subItem in (selectedPrimaryItem as Record<string, unknown>)[primarySubItemsField]"
v-show="subItem && (!secondaryHiddenField || !subItem[secondaryHiddenField])"
v-for="subItem in filteredSubItems"
@click="onSecondaryItemClicked(subItem)">
<template #prepend>
<ItemIcon class="mr-2" :icon-type="secondaryIconType"
@@ -87,7 +94,8 @@ import {
import { scrollToSelectedItem } from '@/lib/ui/desktop.ts';
import {
mdiChevronRight
mdiChevronRight,
mdiMagnify
} from '@mdi/js';
interface DesktopTwoColumnListItemSelectionProps extends CommonTwoColumnListItemSelectionProps {
@@ -112,6 +120,9 @@ const emit = defineEmits<{
const { tt, ti } = useI18n();
const {
filterContent,
filteredItems,
getFilteredSubItems,
getCurrentPrimaryValueBySecondaryValue,
isSecondaryValueSelected,
getSelectedPrimaryItem,
@@ -120,26 +131,24 @@ const {
updateCurrentSecondaryValue
} = useTwoColumnListItemSelectionBase(props);
const icons = {
chevronRight: mdiChevronRight
};
const dropdownMenu = useTemplateRef<HTMLElement>('dropdownMenu');
const menuState = ref<boolean>(false);
const filteredSubItems = computed<Record<string, unknown>[]>(() => getFilteredSubItems(selectedPrimaryItem.value));
const currentPrimaryValue = computed<unknown>({
get: () => {
return getCurrentPrimaryValueBySecondaryValue(props.modelValue);
},
set: (value) => {
const primaryItem = getItemByKeyValue(props.items as Record<string, unknown>[] | Record<string, Record<string, unknown>>, value, props.primaryValueField as string);
const primaryItem = getItemByKeyValue(filteredItems.value, value, props.primaryValueField as string);
if (!primaryItem) {
return;
}
const secondaryItem = getFirstVisibleItem(primaryItem[props.primarySubItemsField] as Record<string, unknown>[] | Record<string, Record<string, unknown>>, props.primaryHiddenField as string);
const secondaryItem = getFirstVisibleItem(getFilteredSubItems(primaryItem), props.primaryHiddenField as string);
if (secondaryItem) {
if (props.secondaryValueField) {
@@ -167,7 +176,7 @@ const noSelectionText = computed<string>(() => props.noItemText ? props.noItemTe
const selectionPrimaryItemText = computed<string>(() => {
if (props.primaryValueField && props.primaryTitleField) {
if (currentPrimaryValue.value) {
return getNameByKeyValue(props.items as Record<string, string>[] | Record<string, Record<string, string>>, currentPrimaryValue.value, props.primaryValueField, props.primaryTitleField, noSelectionText.value) as string;
return getNameByKeyValue(props.items as Record<string, string>[], currentPrimaryValue.value, props.primaryValueField, props.primaryTitleField, noSelectionText.value) as string;
} else {
return noSelectionText.value;
}
@@ -179,7 +188,7 @@ const selectionPrimaryItemText = computed<string>(() => {
const selectionSecondaryItemText = computed<string>(() => {
if (props.secondaryValueField && props.secondaryTitleField) {
if (currentSecondaryValue.value && selectedPrimaryItem.value && (selectedPrimaryItem.value as Record<string, unknown>)[props.primarySubItemsField]) {
return getNameByKeyValue((selectedPrimaryItem.value as Record<string, unknown>)[props.primarySubItemsField] as Record<string, string>[] | Record<string, Record<string, string>>, currentSecondaryValue.value, props.secondaryValueField, props.secondaryTitleField, noSelectionText.value) as string;
return getNameByKeyValue((selectedPrimaryItem.value as Record<string, unknown>)[props.primarySubItemsField] as Record<string, string>[], currentSecondaryValue.value, props.secondaryValueField, props.secondaryTitleField, noSelectionText.value) as string;
} else {
return noSelectionText.value;
}
@@ -9,31 +9,39 @@
</div>
</f7-toolbar>
<f7-page-content>
<div class="grid grid-cols-2 grid-gap">
<div v-if="enableFilter">
<f7-searchbar ref="searchbar" custom-searchs
:value="filterContent"
:placeholder="filterPlaceholder"
:disable-button="false"
@input="filterContent = $event.target.value"></f7-searchbar>
</div>
<div class="grid grid-gap" :class="{ 'grid-cols-2': filteredItems && filteredItems.length }">
<div>
<div class="primary-list-container">
<f7-list dividers class="primary-list no-margin-vertical">
<f7-list-item link="#" no-chevron
:class="{ 'primary-list-item-selected': item === selectedPrimaryItem }"
:value="primaryValueField ? (item as Record<string, unknown>)[primaryValueField] : item"
:title="primaryTitleField ? ti((item as Record<string, unknown>)[primaryTitleField] as string, !!primaryTitleI18n) : ''"
:header="primaryHeaderField ? ti((item as Record<string, unknown>)[primaryHeaderField] as string, !!primaryHeaderI18n) : ''"
:footer="primaryFooterField ? ti((item as Record<string, unknown>)[primaryFooterField] as string, !!primaryFooterI18n) : ''"
:key="primaryKeyField ? (item as Record<string, unknown>)[primaryKeyField] : item"
v-for="item in items"
v-show="item && (!primaryHiddenField || !(item as Record<string, unknown>)[primaryHiddenField])"
:value="primaryValueField ? item[primaryValueField] : item"
:title="primaryTitleField ? ti(item[primaryTitleField] as string, !!primaryTitleI18n) : ''"
:header="primaryHeaderField ? ti(item[primaryHeaderField] as string, !!primaryHeaderI18n) : ''"
:footer="primaryFooterField ? ti(item[primaryFooterField] as string, !!primaryFooterI18n) : ''"
:key="primaryKeyField ? item[primaryKeyField] : item"
v-for="item in filteredItems"
@click="onPrimaryItemClicked(item)">
<template #media>
<ItemIcon :icon-type="primaryIconType" :icon-id="primaryIconField ? (item as Record<string, unknown>)[primaryIconField] : undefined" :color="primaryColorField ? (item as Record<string, unknown>)[primaryColorField] : undefined"></ItemIcon>
<ItemIcon :icon-type="primaryIconType" :icon-id="primaryIconField ? item[primaryIconField] : undefined" :color="primaryColorField ? item[primaryColorField] : undefined"></ItemIcon>
</template>
<template #after>
<f7-icon class="list-item-showing" f7="chevron_right" v-if="item === selectedPrimaryItem"></f7-icon>
</template>
</f7-list-item>
<f7-list-item v-if="!filteredItems || !filteredItems.length"
:title="filterNoItemsText"></f7-list-item>
</f7-list>
</div>
</div>
<div>
<div v-show="filteredItems && filteredItems.length">
<div class="secondary-list-container">
<f7-list dividers class="secondary-list no-margin-vertical" v-if="selectedPrimaryItem && primarySubItemsField && (selectedPrimaryItem as Record<string, unknown>)[primarySubItemsField]">
<f7-list-item link="#" no-chevron
@@ -43,8 +51,7 @@
:header="secondaryHeaderField ? ti(subItem[secondaryHeaderField] as string, !!secondaryHeaderI18n) : ''"
:footer="secondaryFooterField ? ti(subItem[secondaryFooterField] as string, !!secondaryFooterI18n) : ''"
:key="secondaryKeyField ? subItem[secondaryKeyField] : subItem"
v-for="subItem in (selectedPrimaryItem as Record<string, unknown>)[primarySubItemsField]"
v-show="subItem && (!secondaryHiddenField || !subItem[secondaryHiddenField])"
v-for="subItem in filteredSubItems"
@click="onSecondaryItemClicked(subItem)">
<template #media>
<ItemIcon :icon-type="secondaryIconType" :icon-id="secondaryIconField ? subItem[secondaryIconField] : undefined" :color="secondaryColorField ? subItem[secondaryColorField] : undefined"></ItemIcon>
@@ -62,7 +69,8 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ref, computed, useTemplateRef } from 'vue';
import type { Searchbar } from 'framework7/types';
import { useI18n } from '@/locales/helpers.ts';
import { type CommonTwoColumnListItemSelectionProps, useTwoColumnListItemSelectionBase } from '@/components/base/TwoColumnListItemSelectionBase.ts';
@@ -83,6 +91,9 @@ const emit = defineEmits<{
const { tt, ti } = useI18n();
const {
filterContent,
filteredItems,
getFilteredSubItems,
getCurrentPrimaryValueBySecondaryValue,
isSecondaryValueSelected,
getSelectedPrimaryItem,
@@ -90,10 +101,12 @@ const {
updateCurrentSecondaryValue
} = useTwoColumnListItemSelectionBase(props);
const searchbar = useTemplateRef<Searchbar.Searchbar>('searchbar');
const currentPrimaryValue = ref<unknown>(getCurrentPrimaryValueBySecondaryValue(props.modelValue));
const currentSecondaryValue = ref<unknown>(props.modelValue);
const filteredSubItems = computed<Record<string, unknown>[]>(() => getFilteredSubItems(selectedPrimaryItem.value));
const selectedPrimaryItem = computed<unknown>(() => getSelectedPrimaryItem(currentPrimaryValue.value));
function isSecondarySelected(subItem: unknown): boolean {
@@ -123,6 +136,8 @@ function onSheetOpen(event: { $el: Framework7Dom }): void {
function onSheetClosed(): void {
close();
filterContent.value = '';
searchbar.value?.clear();
}
</script>