467 lines
26 KiB
Vue
467 lines
26 KiB
Vue
<template>
|
|
<aside class="h-full overflow-y-auto p-[18px] flex flex-col gap-4 border-l border-border max-[1100px]:border-l-0 max-[1100px]:border-t">
|
|
<section class="bg-gradient-to-b from-white to-[#f6f9fc] border border-border rounded-[14px] p-3.5 shadow-[0_10px_24px_rgba(15,23,42,0.12)]" id="properties">
|
|
<div v-if="!w" class="text-muted text-center py-5 text-[13px]">
|
|
Kein Widget ausgewaehlt.<br><br>
|
|
Waehle ein Widget im Canvas oder im Baum.
|
|
</div>
|
|
<div v-else>
|
|
<!-- Layout -->
|
|
<h4 :class="headingClass">Layout</h4>
|
|
<div :class="rowClass"><label :class="labelClass">X</label><input :class="inputClass" type="number" v-model.number="w.x"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Y</label><input :class="inputClass" type="number" v-model.number="w.y"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" v-model.number="w.w"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Hoehe</label><input :class="inputClass" type="number" v-model.number="w.h"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Sichtbar</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.visible"></div>
|
|
|
|
<!-- Content -->
|
|
<template v-if="key === 'label'">
|
|
<h4 :class="headingClass">Inhalt</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
|
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
|
|
<option v-for="opt in sourceOptions.label" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
|
</select>
|
|
</div>
|
|
<div v-if="w.textSrc === 0" :class="rowClass">
|
|
<label :class="labelClass">Text</label><input :class="inputClass" type="text" v-model="w.text">
|
|
</div>
|
|
<template v-else>
|
|
<div :class="rowClass"><label :class="labelClass">Format</label><input :class="inputClass" type="text" v-model="w.text"></div>
|
|
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
|
<select :class="inputClass" v-model.number="w.knxAddr">
|
|
<option :value="0">-- Waehlen --</option>
|
|
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
|
GO{{ addr.index }} ({{ addr.addrStr }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<template v-if="key === 'button'">
|
|
<h4 :class="headingClass">Button</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Hinweis</label><span :class="noteClass">Text als Label-Child anlegen.</span></div>
|
|
</template>
|
|
|
|
<template v-if="key === 'led'">
|
|
<h4 :class="headingClass">LED</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
|
<select :class="inputClass" v-model.number="w.textSrc">
|
|
<option v-for="opt in sourceOptions.led" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
|
</select>
|
|
</div>
|
|
<div v-if="w.textSrc === 2" :class="rowClass">
|
|
<label :class="labelClass">KNX Objekt</label>
|
|
<select :class="inputClass" v-model.number="w.knxAddr">
|
|
<option :value="0">-- Waehlen --</option>
|
|
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
|
GO{{ addr.index }} ({{ addr.addrStr }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Icon Widget -->
|
|
<template v-if="key === 'icon'">
|
|
<h4 :class="headingClass">Icon</h4>
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Icon</label>
|
|
<button :class="iconSelectClass" @click="showIconPicker = true">
|
|
<span v-if="w.iconCodepoint" class="material-symbols-outlined text-[20px]">{{ String.fromCodePoint(w.iconCodepoint) }}</span>
|
|
<span v-else>Auswaehlen</span>
|
|
</button>
|
|
</div>
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Groesse</label>
|
|
<select :class="inputClass" v-model.number="w.iconSize">
|
|
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
|
|
</select>
|
|
</div>
|
|
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
|
<select :class="inputClass" v-model.number="w.textSrc">
|
|
<option v-for="opt in sourceOptions.icon" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
|
</select>
|
|
</div>
|
|
<div v-if="w.textSrc === 2" :class="rowClass">
|
|
<label :class="labelClass">KNX Objekt</label>
|
|
<select :class="inputClass" v-model.number="w.knxAddr">
|
|
<option :value="0">-- Waehlen --</option>
|
|
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
|
GO{{ addr.index }} ({{ addr.addrStr }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-if="key === 'tabview'">
|
|
<h4 :class="headingClass">Tabs</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Position</label>
|
|
<select :class="inputClass" v-model.number="w.iconPosition">
|
|
<option :value="0">Oben</option>
|
|
<option :value="1">Unten</option>
|
|
<option :value="2">Links</option>
|
|
<option :value="3">Rechts</option>
|
|
</select>
|
|
</div>
|
|
<div :class="rowClass"><label :class="labelClass">Tab Hoehe</label>
|
|
<input :class="inputClass" type="number" v-model.number="w.iconSize" min="1" max="20">
|
|
<span class="text-[10px] text-muted ml-1">x10px</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-if="key === 'tabpage'">
|
|
<h4 :class="headingClass">Tab Seite</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Titel</label><input :class="inputClass" type="text" v-model="w.text"></div>
|
|
</template>
|
|
|
|
<template v-if="key === 'powerflow'">
|
|
<h4 :class="headingClass">Power Flow</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Titel</label><input :class="inputClass" type="text" v-model="w.text"></div>
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Knoten</label>
|
|
<span class="text-[12px] text-muted">{{ powerNodeCount }}</span>
|
|
<button class="ml-auto border border-border bg-panel-2 px-2.5 py-1.5 rounded-lg text-[11px] font-semibold hover:bg-[#e4ebf2]" @click="addPowerNode">+ Node</button>
|
|
</div>
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Verbindungen</label>
|
|
<button class="border border-border bg-panel-2 px-2.5 py-1.5 rounded-lg text-[11px] font-semibold hover:bg-[#e4ebf2]" @click="togglePowerLinkMode">
|
|
{{ isLinkModeActive ? 'Modus: aktiv' : 'Modus: aus' }}
|
|
</button>
|
|
<button v-if="isLinkModeActive && linkSourceLabel" class="ml-auto border border-border bg-panel-2 px-2.5 py-1.5 rounded-lg text-[11px] hover:bg-[#e4ebf2]" @click="clearPowerLinkSource">Quelle loeschen</button>
|
|
</div>
|
|
<div class="text-[11px] text-muted mb-2">{{ linkModeHint }}</div>
|
|
<div v-if="powerFlowLinkItems.length" class="mt-2">
|
|
<div v-for="link in powerFlowLinkItems" :key="link.id" class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
|
<span class="px-1.5 py-0.5 rounded-md bg-panel-2 border border-border text-text max-w-[90px] truncate">{{ link.fromLabel }}</span>
|
|
<span>-></span>
|
|
<span class="px-1.5 py-0.5 rounded-md bg-panel-2 border border-border text-text max-w-[90px] truncate">{{ link.toLabel }}</span>
|
|
<input class="h-[22px] w-[32px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="link.widget.bgColor">
|
|
<button class="ml-auto w-6 h-6 rounded-md border border-red-200 bg-[#f7dede] text-[#b3261e] grid place-items-center text-[11px] cursor-pointer hover:bg-[#f2cfcf]" @click="removePowerLink(link.id)">x</button>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-[11px] text-muted">Keine Verbindungen.</div>
|
|
</template>
|
|
|
|
<template v-if="key === 'powernode'">
|
|
<h4 :class="headingClass">Power Node</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Label</label><input :class="inputClass" type="text" v-model="powerNodeLabel"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
|
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
|
|
<option v-for="opt in sourceOptions.powernode" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
|
</select>
|
|
</div>
|
|
<div v-if="w.textSrc === 0" :class="rowClass">
|
|
<label :class="labelClass">Wert</label><input :class="inputClass" type="text" v-model="powerNodeValue">
|
|
</div>
|
|
<template v-else>
|
|
<div :class="rowClass"><label :class="labelClass">Format</label><input :class="inputClass" type="text" v-model="powerNodeValue"></div>
|
|
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
|
<select :class="inputClass" v-model.number="w.knxAddr">
|
|
<option :value="0">-- Waehlen --</option>
|
|
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
|
GO{{ addr.index }} ({{ addr.addrStr }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<!-- Typography -->
|
|
<template v-if="key === 'label'">
|
|
<h4 :class="headingClass">Typo</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Schriftgr.</label>
|
|
<select :class="inputClass" v-model.number="w.fontSize">
|
|
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-if="key === 'powernode'">
|
|
<h4 :class="headingClass">Typo</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Wert Schriftgr.</label>
|
|
<select :class="inputClass" v-model.number="w.fontSize">
|
|
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Icon for Label/Button -->
|
|
<template v-if="key === 'label'">
|
|
<h4 :class="headingClass">Icon</h4>
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Icon</label>
|
|
<button :class="iconSelectClass" @click="showIconPicker = true">
|
|
<span v-if="w.iconCodepoint" class="material-symbols-outlined text-[20px]">{{ String.fromCodePoint(w.iconCodepoint) }}</span>
|
|
<span v-else>Kein Icon</span>
|
|
</button>
|
|
<button v-if="w.iconCodepoint" :class="iconRemoveClass" @click="w.iconCodepoint = 0">x</button>
|
|
</div>
|
|
<template v-if="w.iconCodepoint">
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Position</label>
|
|
<select :class="inputClass" v-model.number="w.iconPosition">
|
|
<option :value="0">Links</option>
|
|
<option :value="1">Rechts</option>
|
|
<option :value="2">Oben</option>
|
|
<option :value="3">Unten</option>
|
|
</select>
|
|
</div>
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Icon-Gr.</label>
|
|
<select :class="inputClass" v-model.number="w.iconSize">
|
|
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
|
|
</select>
|
|
</div>
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Abstand</label>
|
|
<input :class="inputClass" type="number" v-model.number="w.iconGap" min="0" max="50">
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<template v-if="key === 'powernode'">
|
|
<h4 :class="headingClass">Icon</h4>
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Icon</label>
|
|
<button :class="iconSelectClass" @click="showIconPicker = true">
|
|
<span v-if="w.iconCodepoint" class="material-symbols-outlined text-[20px]">{{ String.fromCodePoint(w.iconCodepoint) }}</span>
|
|
<span v-else>Kein Icon</span>
|
|
</button>
|
|
<button v-if="w.iconCodepoint" :class="iconRemoveClass" @click="w.iconCodepoint = 0">x</button>
|
|
</div>
|
|
<template v-if="w.iconCodepoint">
|
|
<div :class="rowClass">
|
|
<label :class="labelClass">Icon-Gr.</label>
|
|
<select :class="inputClass" v-model.number="w.iconSize">
|
|
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<!-- Style -->
|
|
<template v-if="key === 'led'">
|
|
<h4 :class="headingClass">Stil</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Farbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Helligkeit</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
|
|
</template>
|
|
<template v-else-if="key === 'icon'">
|
|
<h4 :class="headingClass">Stil</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Farbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
|
|
</template>
|
|
<template v-else-if="key === 'button'">
|
|
<h4 :class="headingClass">Stil</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
|
|
</template>
|
|
<template v-else-if="key === 'powerflow'">
|
|
<h4 :class="headingClass">Stil</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
|
|
</template>
|
|
<template v-else-if="key === 'powernode'">
|
|
<h4 :class="headingClass">Stil</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Ringfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Ring Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
|
|
</template>
|
|
<template v-else>
|
|
<h4 :class="headingClass">Stil</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
|
|
</template>
|
|
|
|
<!-- Shadow/Glow -->
|
|
<template v-if="key !== 'icon'">
|
|
<h4 :class="headingClass">{{ key === 'led' || key === 'powernode' ? 'Glow' : 'Schatten' }}</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Aktiv</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.shadow.enabled"></div>
|
|
<div :class="rowClass" v-if="key !== 'led'"><label :class="labelClass">X</label><input :class="inputClass" type="number" v-model.number="w.shadow.x"></div>
|
|
<div :class="rowClass" v-if="key !== 'led'"><label :class="labelClass">Y</label><input :class="inputClass" type="number" v-model.number="w.shadow.y"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Blur</label><input :class="inputClass" type="number" v-model.number="w.shadow.blur"></div>
|
|
<div :class="rowClass"><label :class="labelClass">Farbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.shadow.color"></div>
|
|
</template>
|
|
|
|
<!-- Button Actions -->
|
|
<template v-if="key === 'button'">
|
|
<h4 :class="headingClass">Aktion</h4>
|
|
<div :class="rowClass"><label :class="labelClass">Typ</label>
|
|
<select :class="inputClass" v-model.number="w.action">
|
|
<option :value="BUTTON_ACTIONS.KNX">KNX</option>
|
|
<option :value="BUTTON_ACTIONS.JUMP">Sprung</option>
|
|
<option :value="BUTTON_ACTIONS.BACK">Zurueck</option>
|
|
</select>
|
|
</div>
|
|
<div v-if="w.action === BUTTON_ACTIONS.JUMP" :class="rowClass">
|
|
<label :class="labelClass">Ziel</label>
|
|
<select :class="inputClass" v-model.number="w.targetScreen">
|
|
<option v-for="s in store.config.screens" :key="s.id" :value="s.id">{{ s.name }}</option>
|
|
</select>
|
|
</div>
|
|
<template v-if="w.action === BUTTON_ACTIONS.KNX">
|
|
<div :class="rowClass"><label :class="labelClass">Toggle</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.isToggle"></div>
|
|
<div :class="rowClass"><label :class="labelClass">KNX Schreib</label>
|
|
<select :class="inputClass" v-model.number="w.knxAddrWrite">
|
|
<option :value="0">-- Keine --</option>
|
|
<option v-for="addr in writeableAddresses" :key="addr.index" :value="addr.index">
|
|
GO{{ addr.index }} ({{ addr.addrStr }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<div class="mt-4">
|
|
<button class="border border-red-200 bg-[#f7dede] text-[#b3261e] px-3.5 py-2 rounded-[10px] text-[13px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#f2cfcf] active:translate-y-0.5" @click="store.deleteWidget">Widget loeschen</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Icon Picker Modal -->
|
|
<IconPicker
|
|
v-if="showIconPicker"
|
|
v-model="w.iconCodepoint"
|
|
@close="showIconPicker = false"
|
|
/>
|
|
</aside>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue';
|
|
import { useEditorStore } from '../stores/editor';
|
|
import { typeKeyFor } from '../utils';
|
|
import { sourceOptions, textSources, fontSizes, BUTTON_ACTIONS, defaultFormats, WIDGET_TYPES } from '../constants';
|
|
import IconPicker from './IconPicker.vue';
|
|
|
|
const store = useEditorStore();
|
|
const w = computed(() => store.selectedWidget);
|
|
const key = computed(() => w.value ? typeKeyFor(w.value.type) : 'label');
|
|
const showIconPicker = ref(false);
|
|
const powerNodeCount = computed(() => {
|
|
if (!w.value || w.value.type !== WIDGET_TYPES.POWERFLOW || !store.activeScreen) return 0;
|
|
return store.activeScreen.widgets.filter((child) => child.parentId === w.value.id && child.type === WIDGET_TYPES.POWERNODE).length;
|
|
});
|
|
const powerFlowLinkItems = computed(() => {
|
|
if (!w.value || w.value.type !== WIDGET_TYPES.POWERFLOW || !store.activeScreen) return [];
|
|
const nodes = store.activeScreen.widgets.filter((child) => child.parentId === w.value.id && child.type === WIDGET_TYPES.POWERNODE);
|
|
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
|
|
|
|
return store.activeScreen.widgets
|
|
.filter((child) => child.parentId === w.value.id && child.type === WIDGET_TYPES.POWERLINK)
|
|
.map((link) => {
|
|
const fromNode = nodeMap.get(link.x);
|
|
const toNode = nodeMap.get(link.y);
|
|
return {
|
|
id: link.id,
|
|
widget: link,
|
|
fromLabel: getPowerNodeLabel(fromNode, link.x),
|
|
toLabel: getPowerNodeLabel(toNode, link.y)
|
|
};
|
|
});
|
|
});
|
|
|
|
const isLinkModeActive = computed(() => {
|
|
return store.powerLinkMode.active && store.powerLinkMode.powerflowId === w.value?.id;
|
|
});
|
|
|
|
const linkSourceLabel = computed(() => {
|
|
if (!isLinkModeActive.value || !store.activeScreen || !store.powerLinkMode.fromNodeId) return '';
|
|
const node = store.activeScreen.widgets.find((child) => child.id === store.powerLinkMode.fromNodeId);
|
|
return getPowerNodeLabel(node, store.powerLinkMode.fromNodeId);
|
|
});
|
|
|
|
const linkModeHint = computed(() => {
|
|
if (!isLinkModeActive.value) return 'Aktiviere den Modus und klicke zwei Knoten, um eine Verbindung zu erstellen.';
|
|
if (!linkSourceLabel.value) return 'Klicke den Startknoten.';
|
|
return `Quelle: ${linkSourceLabel.value} - jetzt Zielknoten waehlen.`;
|
|
});
|
|
|
|
const powerNodeLabel = computed({
|
|
get() {
|
|
return splitPowerNodeText(w.value?.text).label;
|
|
},
|
|
set(value) {
|
|
const parts = splitPowerNodeText(w.value?.text);
|
|
setPowerNodeText(value, parts.value);
|
|
}
|
|
});
|
|
|
|
const powerNodeValue = computed({
|
|
get() {
|
|
return splitPowerNodeText(w.value?.text).value;
|
|
},
|
|
set(value) {
|
|
const parts = splitPowerNodeText(w.value?.text);
|
|
setPowerNodeText(parts.label, value);
|
|
}
|
|
});
|
|
|
|
const writeableAddresses = computed(() => store.knxAddresses.filter(a => a.write));
|
|
const rowClass = 'flex items-center gap-2.5 mb-2';
|
|
const labelClass = 'w-[90px] text-[12px] text-muted';
|
|
const inputClass = 'flex-1 bg-panel-2 border border-border rounded-lg px-2 py-1.5 text-text text-[12px] focus:outline-none focus:border-accent';
|
|
const headingClass = 'mt-4 mb-2.5 text-[12px] uppercase tracking-[0.08em] text-[#3a5f88]';
|
|
const noteClass = 'text-[11px] text-muted leading-tight';
|
|
const iconSelectClass = 'flex-1 bg-panel-2 border border-border rounded-lg px-2.5 py-1.5 text-text text-[12px] flex items-center justify-center gap-1.5 cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]';
|
|
const iconRemoveClass = 'w-7 h-7 rounded-md border border-red-200 bg-[#f7dede] text-[#b3261e] grid place-items-center text-[12px] cursor-pointer hover:bg-[#f2cfcf]';
|
|
|
|
function addPowerNode() {
|
|
store.addWidget('powernode');
|
|
}
|
|
|
|
function togglePowerLinkMode() {
|
|
if (!w.value || w.value.type !== WIDGET_TYPES.POWERFLOW) return;
|
|
const nextState = !(store.powerLinkMode.active && store.powerLinkMode.powerflowId === w.value.id);
|
|
store.setPowerLinkMode(nextState, w.value.id);
|
|
}
|
|
|
|
function clearPowerLinkSource() {
|
|
if (!w.value || w.value.type !== WIDGET_TYPES.POWERFLOW) return;
|
|
store.powerLinkMode.fromNodeId = null;
|
|
}
|
|
|
|
function removePowerLink(linkId) {
|
|
store.removePowerLink(linkId);
|
|
}
|
|
|
|
function handleTextSrcChange() {
|
|
if (!w.value) return;
|
|
const newSrc = w.value.textSrc;
|
|
if (w.value.type === WIDGET_TYPES.LABEL && newSrc > 0 && defaultFormats[newSrc]) {
|
|
w.value.text = defaultFormats[newSrc];
|
|
}
|
|
if (w.value.type === WIDGET_TYPES.POWERNODE && newSrc > 0 && defaultFormats[newSrc]) {
|
|
const parts = splitPowerNodeText(w.value.text);
|
|
setPowerNodeText(parts.label, defaultFormats[newSrc]);
|
|
}
|
|
}
|
|
|
|
function splitPowerNodeText(text) {
|
|
if (typeof text !== 'string') return { label: '', value: '' };
|
|
const parts = text.split('\n');
|
|
const label = parts[0] ?? '';
|
|
const value = parts.slice(1).join('\n');
|
|
return { label, value };
|
|
}
|
|
|
|
function setPowerNodeText(label, value) {
|
|
if (!w.value) return;
|
|
const labelLine = label ?? '';
|
|
const valueLine = value ?? '';
|
|
w.value.text = valueLine !== '' || labelLine !== '' ? `${labelLine}${valueLine !== '' ? `\n${valueLine}` : ''}` : '';
|
|
}
|
|
|
|
function getPowerNodeLabel(node, fallbackId) {
|
|
if (!node) return `Node ${fallbackId ?? ''}`.trim();
|
|
const parts = splitPowerNodeText(node.text);
|
|
return parts.label || `Node ${node.id}`;
|
|
}
|
|
</script>
|