Files
graphfx/graphfx-ui/src/components/GraphFxNode.vue

260 lines
9.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
import type { GraphFxNodeData, GraphFxPort } from '../lib/graphfxToVueFlow'
import { getInputHandleId, getOutputHandleId } from '../lib/graphfxToVueFlow'
import { displayIoValue } from '../lib/editorHelpers'
const props = defineProps<{
data: GraphFxNodeData
}>()
const emit = defineEmits<{
'inspect-image': [inspection: {
nodeId: string
nodeName: string
direction: 'in' | 'out'
ioName: string
label: string
url: string
}]
}>()
function typeClass(port: GraphFxPort) {
return `io-type-${port.type.toLowerCase()}`
}
function hasCustomLabel(port: GraphFxPort) {
return Boolean(port.label && port.label !== port.name)
}
function labelValue(port: GraphFxPort) {
return hasCustomLabel(port) ? port.label : ''
}
function previewUrl(direction: 'in' | 'out', port: GraphFxPort) {
return props.data.imagePreviews?.[`${direction}:${port.name}`] || port.previewUrl || ''
}
function hasSelectOptions(port: GraphFxPort) {
return Boolean(port.options?.length)
}
function updateNodeId(event: Event) {
const target = event.target as HTMLInputElement
props.data.actions?.renameNodeId(props.data.id, target.value)
}
function updateLabel(direction: 'in' | 'out', port: GraphFxPort, event: Event) {
const target = event.target as HTMLInputElement
props.data.actions?.updateIoLabel(props.data.id, direction, port.name, target.value)
}
function updateInput(port: GraphFxPort, event: Event) {
const target = event.target as HTMLInputElement | HTMLSelectElement
const value = port.type === 'Boolean' ? (target as HTMLInputElement).checked : target.value
props.data.actions?.updateInputValue(props.data.id, port.name, value)
}
function selectImage(port: GraphFxPort, event: Event) {
const target = event.target as HTMLSelectElement
if (!target.value) return
props.data.actions?.selectInputImage(props.data.id, port.name, target.value)
}
function selectedImage(port: GraphFxPort) {
return props.data.selectedImages?.[port.name] || ''
}
function deleteNode() {
props.data.actions?.deleteNode(props.data.id)
}
function duplicateNode() {
props.data.actions?.duplicateNode(props.data.id)
}
function disconnectInput(port: GraphFxPort) {
props.data.actions?.disconnectInput(props.data.id, port.name)
}
function inputType(port: GraphFxPort) {
if (port.type === 'Number') return 'number'
if (port.type === 'Color') return 'color'
return 'text'
}
function connectedText(port: GraphFxPort) {
return port.connectedDisplay || port.output || 'connected'
}
function inspectImage(direction: 'in' | 'out', port: GraphFxPort) {
const url = previewUrl(direction, port)
if (!url) return
emit('inspect-image', {
nodeId: props.data.id,
nodeName: props.data.name,
direction,
ioName: port.name,
label: port.label || port.name,
url,
})
}
</script>
<template>
<article class="graphfx-node">
<header class="graphfx-node__header">
<div>
<h3>{{ data.name }}</h3>
<input
class="graphfx-node__id nodrag"
:value="data.id"
aria-label="Node id"
title="Node id"
@change="updateNodeId"
@pointerdown.stop
@keydown.stop
/>
</div>
<details class="graphfx-node__menu nodrag" @pointerdown.stop @click.stop>
<summary title="Node actions"></summary>
<div class="graphfx-node__menu-popover">
<button type="button" class="secondary" @click="duplicateNode">Duplicate</button>
<button type="button" class="danger" @click="deleteNode">Delete</button>
</div>
</details>
</header>
<section class="graphfx-node__ports" :class="{ 'graphfx-node__ports--empty': !data.inputs.length && !data.outputs.length }">
<div class="graphfx-node__column graphfx-node__column--inputs">
<div v-if="!data.inputs.length" class="graphfx-node__empty">no inputs</div>
<div
v-for="port in data.inputs"
:key="port.name"
class="graphfx-node__port graphfx-node__port--input"
:class="[typeClass(port), { 'graphfx-node__port--connected': port.output, 'graphfx-node__port--labeled': hasCustomLabel(port) }]"
:title="`${port.name} · ${port.type}${port.output ? ` · ${port.output}` : ''}`"
>
<Handle
type="target"
:id="getInputHandleId(data.id, port.name)"
:position="Position.Left"
class="graphfx-node__handle graphfx-node__handle--input"
/>
<div class="graphfx-node__port-head">
<span class="graphfx-node__port-name">{{ port.name }}</span>
<span class="graphfx-node__label-slot nodrag" @pointerdown.stop>
<span v-if="hasCustomLabel(port)" class="graphfx-node__label-chip">{{ port.label }}</span>
<input
class="graphfx-node__label-inline"
:value="labelValue(port)"
placeholder="label"
title="Input label"
@change="updateLabel('in', port, $event)"
@keydown.stop
/>
</span>
<span class="graphfx-node__type-dot" :title="port.type" aria-hidden="true"></span>
</div>
<button v-if="previewUrl('in', port)" type="button" class="graphfx-node__image-preview nodrag" :title="`Inspect ${port.name}`" @click.stop="inspectImage('in', port)" @pointerdown.stop>
<img :src="previewUrl('in', port)" :alt="`${port.name} preview`" />
<span>inspect</span>
</button>
<div v-if="port.output" class="graphfx-node__connected-value nodrag" @pointerdown.stop>
<span :title="port.output"> {{ connectedText(port) }}</span>
<button type="button" class="ghost danger" title="Disconnect input" @click="disconnectInput(port)">×</button>
</div>
<template v-else>
<select
v-if="port.type === 'Image'"
class="graphfx-node__value-input nodrag"
:value="selectedImage(port)"
:disabled="!data.imageOptions?.length"
title="Use uploaded image as this input"
@change="selectImage(port, $event)"
@pointerdown.stop
@keydown.stop
>
<option value="">{{ data.imageOptions?.length ? 'choose uploaded image' : 'upload images first' }}</option>
<option v-for="image in data.imageOptions" :key="image.id" :value="image.id">{{ image.name }}</option>
</select>
<select
v-else-if="hasSelectOptions(port)"
class="graphfx-node__value-input nodrag"
:value="displayIoValue(port.value)"
title="Input value"
@change="updateInput(port, $event)"
@pointerdown.stop
@keydown.stop
>
<option v-for="option in port.options" :key="option" :value="option">{{ option }}</option>
</select>
<label v-else-if="port.type === 'Boolean'" class="graphfx-node__checkbox nodrag" @pointerdown.stop>
<input :checked="Boolean(port.value)" type="checkbox" @change="updateInput(port, $event)" />
{{ Boolean(port.value) ? 'true' : 'false' }}
</label>
<input
v-else
class="graphfx-node__value-input nodrag"
:type="inputType(port)"
:value="displayIoValue(port.value)"
title="Input value"
@change="updateInput(port, $event)"
@pointerdown.stop
@keydown.stop
/>
</template>
</div>
</div>
<div class="graphfx-node__column graphfx-node__column--outputs">
<div v-if="!data.outputs.length" class="graphfx-node__empty">no outputs</div>
<div
v-for="port in data.outputs"
:key="port.name"
class="graphfx-node__port graphfx-node__port--output"
:class="[typeClass(port), { 'graphfx-node__port--labeled': hasCustomLabel(port) }]"
:title="`${port.name} · ${port.type}`"
>
<div class="graphfx-node__port-head graphfx-node__port-head--right">
<span class="graphfx-node__type-dot" :title="port.type" aria-hidden="true"></span>
<span class="graphfx-node__label-slot nodrag" @pointerdown.stop>
<span v-if="hasCustomLabel(port)" class="graphfx-node__label-chip">{{ port.label }}</span>
<input
class="graphfx-node__label-inline"
:value="labelValue(port)"
placeholder="label"
title="Output label"
@change="updateLabel('out', port, $event)"
@keydown.stop
/>
</span>
<span class="graphfx-node__port-name">{{ port.name }}</span>
</div>
<button v-if="previewUrl('out', port)" type="button" class="graphfx-node__image-preview nodrag" :title="`Inspect ${port.name}`" @click.stop="inspectImage('out', port)" @pointerdown.stop>
<img :src="previewUrl('out', port)" :alt="`${port.name} preview`" />
<span>inspect</span>
</button>
<div v-else-if="port.type === 'Image'" class="graphfx-node__image-empty">no image yet</div>
<Handle
type="source"
:id="getOutputHandleId(data.id, port.name)"
:position="Position.Right"
class="graphfx-node__handle graphfx-node__handle--output"
/>
</div>
</div>
</section>
</article>
</template>