Improve GraphFX UI editing and image previews
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
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
|
||||
@@ -11,25 +12,94 @@ function typeClass(port: GraphFxPort) {
|
||||
return `io-type-${port.type.toLowerCase()}`
|
||||
}
|
||||
|
||||
function valuePreview(port: GraphFxPort) {
|
||||
if (port.output) return ''
|
||||
if (port.value === null || port.value === undefined) return ''
|
||||
if (typeof port.value === 'string') return port.value.length > 24 ? `${port.value.slice(0, 24)}…` : port.value
|
||||
if (typeof port.value === 'number' || typeof port.value === 'boolean') return String(port.value)
|
||||
if (typeof port.value === 'object') return 'object'
|
||||
return String(port.value)
|
||||
}
|
||||
|
||||
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">
|
||||
<h3>{{ data.name }}</h3>
|
||||
<small>{{ data.id }}</small>
|
||||
<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 }">
|
||||
@@ -48,9 +118,75 @@ function hasCustomLabel(port: GraphFxPort) {
|
||||
:position="Position.Left"
|
||||
class="graphfx-node__handle graphfx-node__handle--input"
|
||||
/>
|
||||
<span class="graphfx-node__port-name">{{ port.name }}</span>
|
||||
<span v-if="hasCustomLabel(port)" class="graphfx-node__port-label">{{ port.label }}</span>
|
||||
<span v-if="valuePreview(port)" class="graphfx-node__port-value">{{ valuePreview(port) }}</span>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -63,8 +199,26 @@ function hasCustomLabel(port: GraphFxPort) {
|
||||
:class="[typeClass(port), { 'graphfx-node__port--labeled': hasCustomLabel(port) }]"
|
||||
:title="`${port.name} · ${port.type}`"
|
||||
>
|
||||
<span class="graphfx-node__port-name">{{ port.name }}</span>
|
||||
<span v-if="hasCustomLabel(port)" class="graphfx-node__port-label">{{ port.label }}</span>
|
||||
<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)"
|
||||
|
||||
Reference in New Issue
Block a user