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

233 lines
8.7 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
}>()
function typeClass(port: GraphFxPort) {
return `io-type-${port.type.toLowerCase()}`
}
function hasCustomLabel(port: GraphFxPort) {
return 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'
}
</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 v-if="hasCustomLabel(port)" class="graphfx-node__label-chip">{{ port.label }}</span>
<span class="graphfx-node__port-type">{{ port.type }}</span>
</div>
<img v-if="port.type === 'Image' && previewUrl('in', port)" class="graphfx-node__image-preview" :src="previewUrl('in', port)" :alt="`${port.name} preview`" />
<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>
<details class="graphfx-node__label-editor nodrag" @pointerdown.stop @click.stop>
<summary title="Edit input label">label</summary>
<input
class="graphfx-node__label-input"
:value="labelValue(port)"
placeholder="optional label"
title="Input label"
@change="updateLabel('in', port, $event)"
@keydown.stop
/>
</details>
</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__port-type">{{ port.type }}</span>
<span v-if="hasCustomLabel(port)" class="graphfx-node__label-chip">{{ port.label }}</span>
<span class="graphfx-node__port-name">{{ port.name }}</span>
</div>
<img v-if="port.type === 'Image' && previewUrl('out', port)" class="graphfx-node__image-preview" :src="previewUrl('out', port)" :alt="`${port.name} preview`" />
<details class="graphfx-node__label-editor nodrag" @pointerdown.stop @click.stop>
<summary title="Edit output label">label</summary>
<input
class="graphfx-node__label-input"
:value="labelValue(port)"
placeholder="optional label"
title="Output label"
@change="updateLabel('out', port, $event)"
@keydown.stop
/>
</details>
<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>