Organize searchbox files (#315)

This commit is contained in:
Chenlei Hu
2024-08-05 18:29:06 -04:00
committed by GitHub
parent 1eb45ddc55
commit b90b1194d6
7 changed files with 6 additions and 6 deletions

View File

@@ -0,0 +1,178 @@
<template>
<div class="comfy-vue-node-search-container">
<div class="comfy-vue-node-preview-container">
<NodePreview
:nodeDef="hoveredSuggestion"
:key="hoveredSuggestion?.name || ''"
v-if="hoveredSuggestion"
/>
</div>
<NodeSearchFilter @addFilter="onAddFilter" />
<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"
@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">
{{ option.display_name }}
<NodeSourceChip
v-if="option.python_module !== undefined"
:python_module="option.python_module"
/>
</div>
<div v-if="option.description" class="option-description">
{{ option.description }}
</div>
</div>
</template>
<!-- FilterAndValue -->
<template v-slot:chip="{ value }">
<Chip removable @remove="onRemoveFilter($event, value)">
<Badge size="small" :class="value[0].invokeSequence + '-badge'">
{{ value[0].invokeSequence.toUpperCase() }}
</Badge>
{{ value[1] }}
</Chip>
</template>
</AutoCompletePlus>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
import Chip from 'primevue/chip'
import Badge from 'primevue/badge'
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'
const props = defineProps({
filters: {
type: Array<FilterAndValue>
},
searchLimit: {
type: Number,
default: 64
}
})
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`
const suggestions = ref<ComfyNodeDefImpl[]>([])
const hoveredSuggestion = ref<ComfyNodeDefImpl | null>(null)
const placeholder = computed(() => {
return props.filters.length === 0 ? 'Search for nodes' : ''
})
const search = (query: string) => {
suggestions.value = useNodeDefStore().nodeSearchService.searchNode(
query,
props.filters,
{
limit: props.searchLimit
}
)
}
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) => {
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 flex-col px-4 py-2 cursor-pointer overflow-hidden w-full;
}
.option-container:hover .option-description {
@apply overflow-visible;
/* Allows text to wrap */
white-space: normal;
}
.option-display-name {
@apply font-semibold;
}
.option-description {
@apply text-sm text-gray-400 overflow-hidden text-ellipsis;
/* Keeps the text on a single line by default */
white-space: nowrap;
}
.i-badge {
@apply bg-green-500 text-white;
}
.o-badge {
@apply bg-red-500 text-white;
}
.c-badge {
@apply bg-blue-500 text-white;
}
.s-badge {
@apply bg-yellow-500;
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div>
<Dialog
v-model:visible="visible"
pt:root="invisible-dialog-root"
pt:mask="node-search-box-dialog-mask"
modal
:dismissable-mask="dismissable"
@hide="clearFilters"
>
<template #container>
<NodeSearchBox
:filters="nodeFilters"
@add-filter="addFilter"
@remove-filter="removeFilter"
@add-node="addNode"
/>
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import { app } from '@/scripts/app'
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import NodeSearchBox from './NodeSearchBox.vue'
import Dialog from 'primevue/dialog'
import { LiteGraphCanvasEvent, ConnectingLink } from '@comfyorg/litegraph'
import { FilterAndValue } from '@/services/nodeSearchService'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { ConnectingLinkImpl } from '@/types/litegraphTypes'
interface LiteGraphPointerEvent extends Event {
canvasX: number
canvasY: number
}
const visible = ref(false)
const dismissable = ref(true)
const triggerEvent = ref<LiteGraphCanvasEvent | null>(null)
const getNewNodeLocation = (): [number, number] => {
if (triggerEvent.value === null) {
return [100, 100]
}
const originalEvent = triggerEvent.value.detail
.originalEvent as LiteGraphPointerEvent
return [originalEvent.canvasX, originalEvent.canvasY]
}
const nodeFilters = reactive([])
const addFilter = (filter: FilterAndValue) => {
nodeFilters.push(filter)
}
const removeFilter = (filter: FilterAndValue) => {
const index = nodeFilters.findIndex((f) => f === filter)
if (index !== -1) {
nodeFilters.splice(index, 1)
}
}
const clearFilters = () => {
nodeFilters.splice(0, nodeFilters.length)
}
const closeDialog = () => {
visible.value = false
}
const addNode = (nodeDef: ComfyNodeDefImpl) => {
const node = app.addNodeOnGraph(nodeDef, { pos: getNewNodeLocation() })
const eventDetail = triggerEvent.value.detail
if (eventDetail.subType === 'empty-release') {
eventDetail.linkReleaseContext.links.forEach((link: ConnectingLink) => {
ConnectingLinkImpl.createFromPlainObject(link).connectTo(node)
})
}
// TODO: This is not robust timing-wise.
// PrimeVue complains about the dialog being closed before the event selecting
// item is fully processed.
window.setTimeout(() => {
closeDialog()
}, 100)
}
const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
const shiftPressed = (e.detail.originalEvent as KeyboardEvent).shiftKey
// Ignore empty releases unless shift is pressed
// Empty release without shift is trigger right click menu
if (e.detail.subType === 'empty-release' && !shiftPressed) {
return
}
if (e.detail.subType === 'empty-release') {
const context = e.detail.linkReleaseContext
if (context.links.length === 0) {
console.warn('Empty release with no links! This should never happen')
return
}
const firstLink = ConnectingLinkImpl.createFromPlainObject(context.links[0])
const filter = useNodeDefStore().nodeSearchService.getFilterById(
firstLink.releaseSlotType
)
const dataType = firstLink.type
addFilter([filter, dataType])
}
triggerEvent.value = e
visible.value = true
// Prevent the dialog from being dismissed immediately
dismissable.value = false
setTimeout(() => {
dismissable.value = true
}, 300)
}
const handleEscapeKeyPress = (event) => {
if (event.key === 'Escape') {
closeDialog()
}
}
onMounted(() => {
document.addEventListener('litegraph:canvas', canvasEventHandler)
document.addEventListener('keydown', handleEscapeKeyPress)
})
onUnmounted(() => {
document.removeEventListener('litegraph:canvas', canvasEventHandler)
document.removeEventListener('keydown', handleEscapeKeyPress)
})
</script>
<style>
.invisible-dialog-root {
width: 30%;
min-width: 24rem;
max-width: 48rem;
border: 0 !important;
background-color: transparent !important;
margin-top: 25vh;
}
.node-search-box-dialog-mask {
align-items: flex-start !important;
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<Button
icon="pi pi-filter"
severity="secondary"
class="_filter-button"
@click="showModal"
/>
<Dialog v-model:visible="visible" class="_dialog">
<template #header>
<h3>Add node filter condition</h3>
</template>
<div class="_dialog-body">
<SelectButton
v-model="selectedFilter"
:options="filters"
:allowEmpty="false"
optionLabel="name"
@change="updateSelectedFilterValue"
/>
<AutoComplete
v-model="selectedFilterValue"
:suggestions="filterValues"
:min-length="0"
@complete="(event) => updateFilterValues(event.query)"
completeOnFocus
forceSelection
dropdown
></AutoComplete>
</div>
<template #footer>
<Button type="button" label="Add" @click="submit"></Button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { NodeFilter, type FilterAndValue } from '@/services/nodeSearchService'
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import SelectButton from 'primevue/selectbutton'
import AutoComplete from 'primevue/autocomplete'
import { ref, onMounted } from 'vue'
import { useNodeDefStore } from '@/stores/nodeDefStore'
const visible = ref<boolean>(false)
const filters = ref<NodeFilter[]>([])
const selectedFilter = ref<NodeFilter>()
const filterValues = ref<string[]>([])
const selectedFilterValue = ref<string>('')
onMounted(() => {
const nodeSearchService = useNodeDefStore().nodeSearchService
filters.value = nodeSearchService.nodeFilters
selectedFilter.value = nodeSearchService.nodeFilters[0]
})
const emit = defineEmits(['addFilter'])
const updateSelectedFilterValue = () => {
updateFilterValues('')
if (filterValues.value.includes(selectedFilterValue.value)) {
return
}
selectedFilterValue.value = filterValues.value[0]
}
const updateFilterValues = (query: string) => {
filterValues.value = selectedFilter.value.fuseSearch.search(query)
}
const submit = () => {
visible.value = false
emit('addFilter', [
selectedFilter.value,
selectedFilterValue.value
] as FilterAndValue)
}
const showModal = () => {
updateSelectedFilterValue()
visible.value = true
}
</script>
<style scoped>
._filter-button {
z-index: 10;
}
._dialog {
@apply min-w-96;
}
._dialog-body {
@apply flex flex-col space-y-2;
}
</style>