components/ui/NodeSettingsForm.vue

<template>
  <div class="vue-flow__settings-form" :style="formStyle">
    <SettingsGroup
      v-for="g in groups"
      :key="g.key"
      :title="g.title"
      :group-key="g.key"
      :open="isGroupOpen(g.key)"
      @update:open="setGroupOpen(g.key, $event)"
    >
      <template v-if="g.key === 'basic'">
        <label v-if="!isEx('name')" data-field-key="name">Name
          <SettingsText :value="eff('name')" @input="$emit('update', 'name', $event)" />
        </label>
        <label v-if="!isEx('description')" data-field-key="description">Description
          <SettingsText :value="eff('description')" @input="$emit('update', 'description', $event)" />
        </label>
      </template>
      <template v-else-if="g.key === 'appearance'">
        <label v-if="!isEx('shape')" data-field-key="shape">Shape
          <SettingsSelect :items="shapeItems" :value="eff('shape')" @input="$emit('update', 'shape', $event)" />
        </label>
        <label v-if="!isEx('faceColor')" data-field-key="faceColor">Face Color
          <WColorSelect :value="eff('faceColor')" :size="160" :colorBlockSize="16" :showColorText="false" :btnText="colorConfirmText" @input="$emit('update', 'faceColor', $event)" />
        </label>
        <label v-if="!isEx('edgeColor')" data-field-key="edgeColor">Edge Color
          <WColorSelect :value="eff('edgeColor')" :size="160" :colorBlockSize="16" :showColorText="false" :btnText="colorConfirmText" @input="$emit('update', 'edgeColor', $event)" />
        </label>
        <label v-if="!isEx('edgeWidth')" data-field-key="edgeWidth">Edge Width
          <input type="number" :value="eff('edgeWidth')" min="1" :max="edgeWidthMax" @input="onEdgeWidthInput($event.target.value)">
        </label>
      </template>
      <template v-else-if="g.key === 'text'">
        <label v-if="!isEx('fontSize')" data-field-key="fontSize">Font Size
          <input type="number" :value="eff('fontSize')" :min="defNode.fontSizeMin" :max="defNode.fontSizeMax" @input="onFontSizeInput($event.target.value)">
        </label>
        <label v-if="!isEx('fontColor')" data-field-key="fontColor">Font Color
          <WColorSelect :value="eff('fontColor')" :size="160" :colorBlockSize="16" :showColorText="false" :btnText="colorConfirmText" @input="$emit('update', 'fontColor', $event)" />
        </label>
      </template>
      <template v-else-if="g.key === 'advanced'">
        <label v-if="!isEx('popupDirection')" data-field-key="popupDirection">Popup Direction
          <SettingsSelect :items="popupDirectionItems" :value="eff('popupDirection')" @input="$emit('update', 'popupDirection', $event)" />
        </label>
      </template>
    </SettingsGroup>
    <!-- 刪除不做內建二次確認: 是否需要確認由宿主以 opt.funConfirmDeleting(async)決定, 未提供即直接刪除。
         等待宿主確認期間按鈕 disabled(pending): 慢流程若毫無回饋會被當成沒反應而連點。
         刪除為破壞性操作, 不歸入任何屬性群組, 恆顯示於表單底部 -->
    <div class="vue-flow__delete-area">
      <button class="vue-flow__delete-btn" :disabled="deleteConfirming || node.deletable === false" @click="$emit('delete')">{{ deleteText }}</button>
    </div>
  </div>
</template>

<script>
import WColorSelect from 'w-component-vue/src/components/WColorSelect.vue'
import settingsForm from '../mixins/settingsForm.mjs'
import SettingsGroup from './SettingsGroup.vue'
import SettingsSelect from './SettingsSelect.vue'
import SettingsText from './SettingsText.vue'
import { NODE_SETTING_GROUPS } from '../../js/settingsGroups.mjs'
import { SHAPES } from '../../js/nodeStyle.mjs'
import { SIDES } from '../../js/anchorPolicy.mjs'
import './settingsForm.css'

/** 節點設定表單: 欄位有效值/排除/文字/刪除確認態/數值 clamp/分群展開態 皆由 mixins/settingsForm 提供, 本檔只有節點欄位 */
export default {
    name: 'NodeSettingsForm',
    components: { WColorSelect, SettingsGroup, SettingsSelect, SettingsText },
    mixins: [settingsForm],
    props: {
        node: { type: Object, required: true },
        defNode: { type: Object, required: true },
    },
    computed: {
        item() {
            return this.node
        },
        defaults() {
            return this.defNode
        },
        //分群定義(順序即呈現順序); 群標題與成員之單一來源在 js/settingsGroups.mjs
        groupDefs() {
            return NODE_SETTING_GROUPS
        },
        //下拉選項(值與顯示文字分離): 值域一律由既有單一來源衍生, 不在表單另抄一份。
        //顯示文字由值直接轉寫(kebab → Title Case), 不另備對照表 —— 值本身已足以描述形狀
        //(triangle-up / -right / -down / -left 四向自明), 對照表只會變成第二份需要同步的清單。
        shapeItems() {
            return SHAPES.map(v => ({ value: v, text: titleCase(v) }))
        },
        //popupDirection 之值域與邊之四方位同集(top/right/bottom/left), 沿用 anchorPolicy.SIDES
        popupDirectionItems() {
            return SIDES.map(v => ({ value: v, text: titleCase(v) }))
        },
        deleteTextKey() {
            return 'nodeDelete'
        },
    },
}

/** kebab-case 值 → 顯示文字(triangle-up → Triangle Up) */
function titleCase(v) {
    return String(v).split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')
}
</script>