<template> <div class="filter channels-container"> <div class="filters-sort"> <input v-model="search" type="search" :placeholder="`Filter ${channels.length} channels`" class="input input-inline filters-search" > <div v-show="order === 'name'" class="filter-sort order noselect" @click="order = 'count'" > <Icon icon="sort-alpha-asc" /> </div> <div v-show="order === 'count'" class="filter-sort order noselect" @click="order = 'name'" > <Icon icon="sort-numeric-desc" /> </div> </div> <div v-if="entities.length === 0 && !filters.entity" class="filter-empty" >No channels</div> <template v-else> <ul class="filter-items nolist" > <li v-for="entity in entities" :key="`filter-channel-${entity.id}`" class="filter-item" :class="{ channel: !entity.isIndependent && entity.type !== 'network', selected: filters.entity?.id === entity.id }" @click="emit('update', 'entity', entity)" > <span class="filter-name"> <span class="filter-text" :title="entity.name" > <img v-if="entity.isIndependent || entity.type === 'network'" :src="`/logos/${entity.slug}/favicon_dark.png`" class="favicon" > <Icon v-else icon="arrow-up4" /> {{ entity.name }} </span> <span class="filter-details"> <span v-if="entity.count" class="filter-count" >{{ entity.count }}</span> <Icon v-if="filters.entity?.id === entity.id" icon="cross2" class="filter-remove" @click.native.stop="emit('update', 'entity', null)" /> </span> </span> </li> </ul> </template> </div> </template> <script setup> import { ref, computed, inject } from 'vue'; const props = defineProps({ filters: { type: Object, default: null, }, channels: { type: Array, default: () => [], }, }); const emit = defineEmits(['update']); const search = ref(''); const searchRegexp = computed(() => new RegExp(search.value, 'i')); const order = ref('name'); const { pageProps } = inject('pageContext'); const { channel: pageChannel } = pageProps; function sort(channelA, channelB) { if (order.value === 'count') { return channelB.count - channelA.count; } return channelA.name.localeCompare(channelB.name); } const entities = computed(() => { const filteredChannels = props.channels.filter((channel) => channel.id !== pageChannel?.id && (searchRegexp.value.test(channel.name) || searchRegexp.value.test(channel.slug) || (channel.parent && searchRegexp.value.test(channel.parent.name)) || (channel.parent && searchRegexp.value.test(channel.parent.slug)))); const networks = Object.values(filteredChannels.reduce((acc, channel) => { if (!acc[channel.id] && (channel.type === 'network' || !channel.parent || channel.isIndependent)) { // network may have already been created by a child acc[channel.id] = { ...channel, children: [], }; return acc; } if (channel.parent && !acc[channel.parent.id] && channel.type === 'channel') { acc[channel.parent.id] = { ...channel.parent, children: [], }; } if (channel.parent && channel.type === 'channel') { acc[channel.parent.id].children.push(channel); } return acc; }, {})) .map((network) => ({ ...network, children: network.children?.sort(sort), count: network.count || network.children?.reduce((acc, channel) => acc + channel.count, 0), })) .sort(sort) .flatMap((network) => [network, ...(network.children || [])]); return networks; }); </script> <style scoped> .filter-items { max-height: 15rem; overflow-y: auto; } .filter { padding: 0; } .filter-item.channel { .filter-text .icon { width: 2.25rem; height: 1rem; transform: rotate(-135deg); fill: var(--shadow-weak-30); overflow: hidden; /* prevent parent jumping on load */ } } .favicon { width: 1.75rem; height: 1rem; margin-right: .5rem; object-fit: contain; } </style>