Files
ComfyUI_frontend/src/components/searchbox/NodeSearchBox.vue
pythongosssss 968f417061 Node library search filters (#636)
* Add search filters to node library

* Fix

* Dont close on add

* Fix wildcard

---------

Co-authored-by: Chenlei Hu <chenlei.hu@mail.utoronto.ca>
2024-08-27 21:17:34 -04:00

226 lines
6.1 KiB
Vue

<template>
<div class="comfy-vue-node-search-container">
<div class="comfy-vue-node-preview-container" v-if="enableNodePreview">
<NodePreview
:nodeDef="hoveredSuggestion"
:key="hoveredSuggestion?.name || ''"
v-if="hoveredSuggestion"
/>
</div>
<Button
icon="pi pi-filter"
severity="secondary"
class="_filter-button"
@click="nodeSearchFilterVisible = true"
/>
<Dialog v-model:visible="nodeSearchFilterVisible" class="_dialog">
<template #header>
<h3>Add node filter condition</h3>
</template>
<div class="_dialog-body">
<NodeSearchFilter @addFilter="onAddFilter"></NodeSearchFilter>
</div>
</Dialog>
<AutoCompletePlus
:model-value="props.filters"
class="comfy-vue-node-search-box"
scrollHeight="40vh"
:placeholder="placeholder"
:input-id="inputId"
append-to="self"
:suggestions="suggestions"
:min-length="0"
:delay="100"
@complete="search($event.query)"
@option-select="emit('addNode', $event.value)"
@focused-option-changed="setHoverSuggestion($event)"
complete-on-focus
auto-option-focus
force-selection
multiple
>
<template v-slot:option="{ option }">
<div class="option-container">
<div class="option-display-name">
<span
v-html="highlightQuery(option.display_name, currentQuery)"
></span>
<div v-if="showCategory" class="option-category">
{{ option.category.replaceAll('/', ' > ') }}
</div>
</div>
<div class="option-badges">
<Tag
v-if="option.experimental"
:value="$t('experimental')"
severity="primary"
/>
<Tag
v-if="option.deprecated"
:value="$t('deprecated')"
severity="danger"
/>
<NodeSourceChip
v-if="option.python_module !== undefined"
:python_module="option.python_module"
/>
</div>
</div>
</template>
<!-- FilterAndValue -->
<template v-slot:chip="{ value }">
<SearchFilterChip
@remove="onRemoveFilter($event, value)"
:text="value[1]"
:badge="value[0].invokeSequence.toUpperCase()"
:badge-class="value[0].invokeSequence + '-badge'"
/>
</template>
</AutoCompletePlus>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
import Tag from 'primevue/tag'
import Dialog from 'primevue/dialog'
import Button from 'primevue/button'
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
import NodeSourceChip from '@/components/node/NodeSourceChip.vue'
import { type FilterAndValue } from '@/services/nodeSearchService'
import NodePreview from '@/components/node/NodePreview.vue'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { useSettingStore } from '@/stores/settingStore'
import { useI18n } from 'vue-i18n'
import SearchFilterChip from '../common/SearchFilterChip.vue'
const settingStore = useSettingStore()
const { t } = useI18n()
const enableNodePreview = computed(() =>
settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview')
)
const showCategory = computed(() =>
settingStore.get('Comfy.NodeSearchBoxImpl.ShowCategory')
)
const props = defineProps({
filters: {
type: Array<FilterAndValue>
},
searchLimit: {
type: Number,
default: 64
}
})
const nodeSearchFilterVisible = ref(false)
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`
const suggestions = ref<ComfyNodeDefImpl[]>([])
const hoveredSuggestion = ref<ComfyNodeDefImpl | null>(null)
const currentQuery = ref('')
const placeholder = computed(() => {
return props.filters.length === 0 ? t('searchNodes') + '...' : ''
})
const search = (query: string) => {
currentQuery.value = query
suggestions.value = [
...useNodeDefStore().nodeSearchService.searchNode(query, props.filters, {
limit: props.searchLimit
})
]
}
const highlightQuery = (text: string, query: string) => {
if (!query) return text
const regex = new RegExp(`(${query})`, 'gi')
return text.replace(regex, '<span class="highlight">$1</span>')
}
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
const reFocusInput = () => {
const inputElement = document.getElementById(inputId) as HTMLInputElement
if (inputElement) {
inputElement.blur()
inputElement.focus()
}
}
onMounted(reFocusInput)
const onAddFilter = (filterAndValue: FilterAndValue) => {
nodeSearchFilterVisible.value = false
emit('addFilter', filterAndValue)
reFocusInput()
}
const onRemoveFilter = (event: Event, filterAndValue: FilterAndValue) => {
event.stopPropagation()
event.preventDefault()
emit('removeFilter', filterAndValue)
reFocusInput()
}
const setHoverSuggestion = (index: number) => {
if (index === -1) {
hoveredSuggestion.value = null
return
}
const value = suggestions.value[index]
hoveredSuggestion.value = value
}
</script>
<style scoped>
.comfy-vue-node-search-container {
@apply flex justify-center items-center w-full min-w-96;
}
.comfy-vue-node-search-container * {
pointer-events: auto;
}
.comfy-vue-node-preview-container {
position: absolute;
left: -350px;
top: 50px;
}
.comfy-vue-node-search-box {
@apply z-10 flex-grow;
}
.option-container {
@apply flex justify-between items-center px-2 py-0 cursor-pointer overflow-hidden w-full;
}
.option-display-name {
@apply font-semibold flex flex-col;
}
.option-category {
@apply font-light text-sm text-gray-400 overflow-hidden text-ellipsis;
/* Keeps the text on a single line by default */
white-space: nowrap;
}
:deep(.highlight) {
background-color: var(--p-primary-color);
color: var(--p-primary-contrast-color);
font-weight: bold;
border-radius: 0.25rem;
padding: 0rem 0.125rem;
margin: -0.125rem 0.125rem;
}
._filter-button {
z-index: 10;
}
._dialog {
@apply min-w-96;
}
</style>