mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-31 13:29:55 +00:00
Rewrite searchbox without primevue
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
<!-- Auto complete with extra event "focused-option-changed" -->
|
||||
<script>
|
||||
import AutoComplete from 'primevue/autocomplete'
|
||||
|
||||
export default {
|
||||
name: 'AutoCompletePlus',
|
||||
extends: AutoComplete,
|
||||
emits: ['focused-option-changed'],
|
||||
data() {
|
||||
return {
|
||||
// Flag to determine if IME is active
|
||||
isComposing: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (typeof AutoComplete.mounted === 'function') {
|
||||
AutoComplete.mounted.call(this)
|
||||
}
|
||||
|
||||
// Retrieve the actual <input> element and attach IME events
|
||||
const inputEl = this.$el.querySelector('input')
|
||||
if (inputEl) {
|
||||
inputEl.addEventListener('compositionstart', () => {
|
||||
this.isComposing = true
|
||||
})
|
||||
inputEl.addEventListener('compositionend', () => {
|
||||
this.isComposing = false
|
||||
})
|
||||
}
|
||||
// Add a watcher on the focusedOptionIndex property
|
||||
this.$watch(
|
||||
() => this.focusedOptionIndex,
|
||||
(newVal, oldVal) => {
|
||||
// Emit a custom event when focusedOptionIndex changes
|
||||
this.$emit('focused-option-changed', newVal)
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
// Override onKeyDown to block Enter when IME is active
|
||||
onKeyDown(event) {
|
||||
if (event.key === 'Enter' && this.isComposing) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
AutoComplete.methods.onKeyDown.call(this, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -13,20 +13,12 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
:aria-label="$t('g.addNodeFilterCondition')"
|
||||
class="filter-button z-10"
|
||||
@click="nodeSearchFilterVisible = true"
|
||||
>
|
||||
<i class="pi pi-filter" />
|
||||
</Button>
|
||||
<Dialog
|
||||
v-model:visible="nodeSearchFilterVisible"
|
||||
class="min-w-96"
|
||||
dismissable-mask
|
||||
modal
|
||||
@hide="reFocusInput"
|
||||
@hide="nextTick(() => inputRef?.focus())"
|
||||
>
|
||||
<template #header>
|
||||
<h3>{{ $t('g.addNodeFilterCondition') }}</h3>
|
||||
@@ -36,57 +28,67 @@
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<AutoCompletePlus
|
||||
ref="autoCompletePlus"
|
||||
:model-value="filters"
|
||||
class="comfy-vue-node-search-box z-10 grow"
|
||||
scroll-height="40vh"
|
||||
:placeholder="placeholder"
|
||||
:input-id="inputId"
|
||||
append-to="self"
|
||||
:suggestions="suggestions"
|
||||
:delay="100"
|
||||
:loading="!nodeFrequencyStore.isLoaded"
|
||||
complete-on-focus
|
||||
auto-option-focus
|
||||
force-selection
|
||||
multiple
|
||||
option-label="display_name"
|
||||
@complete="search($event.query)"
|
||||
@option-select="onAddNode($event.value)"
|
||||
@focused-option-changed="setHoverSuggestion($event)"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<NodeSearchItem :node-def="option" :current-query="currentQuery" />
|
||||
</template>
|
||||
<!-- FilterAndValue -->
|
||||
<template #chip="{ value }">
|
||||
<SearchFilterChip
|
||||
v-if="value.filterDef && value.value"
|
||||
<div class="comfy-vue-node-search-box z-10 grow">
|
||||
<div
|
||||
class="flex w-full items-center bg-base-background rounded-lg py-1 px-4 border-primary-background border"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
:aria-label="$t('g.addNodeFilterCondition')"
|
||||
class="filter-button z-10 absolute -left-10"
|
||||
@click="nodeSearchFilterVisible = true"
|
||||
>
|
||||
<i class="pi pi-filter" />
|
||||
</Button>
|
||||
<template
|
||||
v-for="value in filters"
|
||||
:key="`${value.filterDef.id}-${value.value}`"
|
||||
:text="value.value"
|
||||
:badge="value.filterDef.invokeSequence.toUpperCase()"
|
||||
:badge-class="value.filterDef.invokeSequence + '-badge'"
|
||||
@remove="
|
||||
onRemoveFilter(
|
||||
$event,
|
||||
value as FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
)
|
||||
"
|
||||
>
|
||||
<SearchFilterChip
|
||||
v-if="value.filterDef && value.value"
|
||||
:text="value.value"
|
||||
:badge="value.filterDef.invokeSequence.toUpperCase()"
|
||||
:badge-class="value.filterDef.invokeSequence + '-badge'"
|
||||
@remove="
|
||||
onRemoveFilter(
|
||||
$event,
|
||||
value as FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="currentQuery"
|
||||
class="text-base h-5 bg-transparent border-0 focus:outline-0 flex-1"
|
||||
autofocus
|
||||
:placeholder="t('g.searchNodes') + '...'"
|
||||
@keydown.enter.prevent="onAddNode(hoveredSuggestion)"
|
||||
/>
|
||||
</template>
|
||||
</AutoCompletePlus>
|
||||
</div>
|
||||
<div class="bg-comfy-menu-bg p-1 rounded-lg border-border-subtle border">
|
||||
<NodeSearchItem
|
||||
v-for="(option, index) in suggestions.slice(0, 10)"
|
||||
:key="option.name"
|
||||
class="hover:bg-secondary-background-hover p-1 rounded-sm"
|
||||
:node-def="option"
|
||||
:current-query="debouncedQuery"
|
||||
@click="onAddNode(option)"
|
||||
@pointerover="setHoverSuggestion(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -111,15 +113,11 @@ const { filters, searchLimit = 64 } = defineProps<{
|
||||
searchLimit?: number
|
||||
}>()
|
||||
|
||||
const autoCompletePlus = ref()
|
||||
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 hoveredSuggestion = ref<ComfyNodeDefImpl>()
|
||||
const currentQuery = ref('')
|
||||
const placeholder = computed(() => {
|
||||
return filters.length === 0 ? t('g.searchNodes') + '...' : ''
|
||||
})
|
||||
const debouncedQuery = refDebounced(currentQuery, 100, { maxWait: 400 })
|
||||
const inputRef = useTemplateRef('inputRef')
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeFrequencyStore = useNodeFrequencyStore()
|
||||
@@ -131,48 +129,34 @@ const debouncedTrackSearch = debounce((query: string) => {
|
||||
}
|
||||
}, 500)
|
||||
|
||||
const search = (query: string) => {
|
||||
const suggestions = computed(() => {
|
||||
const query = debouncedQuery.value
|
||||
const queryIsEmpty = query === '' && filters.length === 0
|
||||
currentQuery.value = query
|
||||
suggestions.value = queryIsEmpty
|
||||
|
||||
// Track search queries with debounce
|
||||
debouncedTrackSearch(query)
|
||||
|
||||
return queryIsEmpty
|
||||
? nodeFrequencyStore.topNodeDefs
|
||||
: [
|
||||
...nodeDefStore.nodeSearchService.searchNode(query, filters, {
|
||||
limit: searchLimit
|
||||
})
|
||||
]
|
||||
|
||||
// Track search queries with debounce
|
||||
debouncedTrackSearch(query)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
|
||||
|
||||
// Track node selection and emit addNode event
|
||||
const onAddNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
const onAddNode = (nodeDef?: ComfyNodeDefImpl) => {
|
||||
if (!nodeDef) return
|
||||
telemetry?.trackNodeSearchResultSelected({
|
||||
node_type: nodeDef.name,
|
||||
last_query: currentQuery.value
|
||||
last_query: debouncedQuery.value
|
||||
})
|
||||
emit('addNode', nodeDef)
|
||||
}
|
||||
|
||||
let inputElement: HTMLInputElement | null = null
|
||||
const reFocusInput = async () => {
|
||||
inputElement ??= document.getElementById(inputId) as HTMLInputElement
|
||||
if (inputElement) {
|
||||
inputElement.blur()
|
||||
await nextTick(() => inputElement?.focus())
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
inputElement ??= document.getElementById(inputId) as HTMLInputElement
|
||||
if (inputElement) inputElement.focus()
|
||||
autoCompletePlus.value.hide = () => search('')
|
||||
search('')
|
||||
autoCompletePlus.value.show()
|
||||
})
|
||||
const onAddFilter = (
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
) => {
|
||||
@@ -186,11 +170,11 @@ const onRemoveFilter = async (
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
emit('removeFilter', filterAndValue)
|
||||
await reFocusInput()
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
const setHoverSuggestion = (index: number) => {
|
||||
if (index === -1) {
|
||||
hoveredSuggestion.value = null
|
||||
hoveredSuggestion.value = undefined
|
||||
return
|
||||
}
|
||||
const value = suggestions.value[index]
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
},
|
||||
mask: { class: 'node-search-box-dialog-mask' },
|
||||
transition: {
|
||||
enterFromClass: 'opacity-0 scale-75',
|
||||
enterFromClass: 'opacity-0',
|
||||
// 100ms is the duration of the transition in the dialog component
|
||||
enterActiveClass: 'transition-all duration-100 ease-out',
|
||||
leaveActiveClass: 'transition-all duration-100 ease-in',
|
||||
enterActiveClass: 'transition-all duration-20 ease-out',
|
||||
leaveActiveClass: 'transition-all duration-20 ease-in',
|
||||
leaveToClass: 'opacity-0 scale-75'
|
||||
}
|
||||
}"
|
||||
|
||||
Reference in New Issue
Block a user