/**
* 設定表單共用(NodeSettingsForm / ConnSettingsForm): 注入文字/刪除確認態、排除欄位、有效值解析、數值 clamp、
* 屬性分群之展開態。
*
* 使用元件須提供:
* computed item(): 被編輯之節點/連線; defaults(): defNode / defConn
* computed groupDefs(): 分群定義(js/settingsGroups.mjs 之 NODE_SETTING_GROUPS / CONN_SETTING_GROUPS)
* deleteTextKey: 'nodeDelete' | 'connDelete'
*/
import { EDGE_WIDTH_MAX } from '../../js/defaults.mjs'
import { DEFAULT_OPEN_GROUPS, visibleGroups } from '../../js/settingsGroups.mjs'
/**
* 每欄位之有效值(item → defaults)回退政策(不同欄位之「空值」語義不同, 不用通用規則抹平):
* raw : 只取 item 自身(name/description 之空字串為有效值, 不繼承)
* defined : item 非 undefined/null 即採用(edgeWidth 0、animated false 皆為明確值)
* explicit-empty : item 非 undefined/null 即採用, 含 ''(markerFrom/End 之 '' 為明確「無」, 不落回 defaults——與 edgeMarker 同一規則)
* truthy(預設) : item 為 truthy 才採用, '' 視為未給 → defaults
*/
export const FIELD_POLICY = {
name: 'raw',
description: 'raw',
edgeWidth: 'defined',
animated: 'defined',
markerFrom: 'explicit-empty',
markerTo: 'explicit-empty',
}
/**
* 判斷一個 CSS 色字串是否為深色(相對亮度 < 0.5)。支援 #rgb / #rrggbb / rgb(a)();
* 認不得的字串(具名色、hsl、漸層…)一律回 false —— 寧可維持淺色預設, 不亂猜。
*
* 用途: 群標題之底色/三角形色須隨 popup 底色走。淺色底以「疊黑」加深、深色底須改為「疊白」加亮,
* 否則深色 popup 上的標題帶會比背景更暗、三角形直接看不見。
*/
export function isDarkColor(css) {
if (typeof css !== 'string') return false
let r, g, b
const hex = css.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)
if (hex) {
const h = hex[1]
const f = h.length === 3 ? h.split('').map(c => c + c) : [h.slice(0, 2), h.slice(2, 4), h.slice(4, 6)]
;[r, g, b] = f.map(v => parseInt(v, 16))
}
else {
const m = css.trim().match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i)
if (!m) return false
;[r, g, b] = [m[1], m[2], m[3]].map(Number)
}
if (![r, g, b].every(v => isFinite(v))) return false
const lin = (c) => {
c /= 255
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
}
return (0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)) < 0.5
}
export function effectiveField(key, item, defaults) {
const v = (item || {})[key]
const d = (defaults || {})[key]
const policy = FIELD_POLICY[key] || 'truthy'
const has = v !== undefined && v !== null
switch (policy) {
case 'raw': return has ? v : ''
case 'defined':
case 'explicit-empty': return has ? v : (d !== undefined && d !== null ? d : '')
default: return (has && v !== '') ? v : (d !== undefined && d !== null ? d : '')
}
}
export default {
inject: {
//刪除確認進行中(getter注入, 預設值使本元件可獨立掛載): 等待宿主回覆期間刪除鈕 disabled
getDeleteConfirming: { default: () => () => false },
//設定表單文字(刪除鈕/色票確認鈕; 由 WFlowVue 依 opt 注入, 預設英文)
getSettingsText: { default: () => () => ({}) },
},
props: {
textFontSize: { type: String, default: '' },
//表單最大高度(CSS 長度字串, 如 '400px' / '50vh'); 超出即於表單內捲動。空字串為不設限
maxHeight: { type: String, default: '' },
//popup 底色: 群標題之分隔線取此色(相鄰收合群之間的 1px 區隔), 宿主換深色主題時分隔線才跟著走
backgroundColor: { type: String, default: '' },
excludes: { type: Array, default: () => [] },
//預設展開之群鍵(其餘收合); 各群展開態彼此獨立, 開一群不會關掉別群。
//語義為「初值」而非受控 prop: 只在建立時取用一次, 掛載後再改本 prop 不生效, 故命名為 default*。
//表單亦不送出展開態變更事件 —— popup 每次開啟都會重建表單(NodeWrapper/EdgeWrapper 之 v-if),
//展開態刻意不跨開關記憶; 宿主若要記憶, 以 SettingsGroup 自行組裝表單。
defaultOpenGroups: { type: Array, default: () => DEFAULT_OPEN_GROUPS.slice() },
},
data() {
return {
//展開態為表單本地狀態(popup 關閉重建即回到 defaultOpenGroups)
openGroupsLocal: this.defaultOpenGroups.slice(),
}
},
//預設展開鍵可能因 excludes 而全數不可見(如 basic 整群被排除), 那會讓 popup 一開全是收合的標題列;
//此時退而展開第一個可見群, 使 popup 開啟即有內容可改
created() {
const gs = this.groups
if (gs.length && !gs.some(g => this.openGroupsLocal.indexOf(g.key) >= 0)) {
this.openGroupsLocal = [gs[0].key]
}
},
computed: {
//實際渲染之群(整群欄位都被 excludes 排除者不出現, 不留空群)
groups() {
return visibleGroups(this.groupDefs, this.excludes)
},
deleteConfirming() {
return this.getDeleteConfirming()
},
deleteText() {
return this.getSettingsText()[this.deleteTextKey] || 'Delete'
},
colorConfirmText() {
return this.getSettingsText().colorConfirm || 'Confirm'
},
formStyle() {
const s = {}
if (this.textFontSize) s.fontSize = this.textFontSize
//群標題各層顏色隨 popup 底色走(CSS 變數; 宿主仍可自行覆寫這些變數再細調):
//分隔線取 popup 底色本身; 底色為深色時, 標題帶改「疊白」加亮、三角形改淺色, 否則深色主題下
//標題帶會比背景更暗、三角形直接看不見。
if (this.backgroundColor) {
s['--vf-settings-bg'] = this.backgroundColor
if (isDarkColor(this.backgroundColor)) {
s['--vf-settings-surface'] = 'rgba(255, 255, 255, 0.10)'
s['--vf-settings-surface-hover'] = 'rgba(255, 255, 255, 0.20)'
s['--vf-settings-caret'] = 'rgba(255, 255, 255, 0.72)'
s['--vf-settings-caret-hover'] = 'rgba(255, 255, 255, 0.95)'
s['--vf-settings-rule'] = 'rgba(255, 255, 255, 0.16)'
}
}
//超出上限即表單內捲動(非讓 popup 無限長高)。
//刻意不用 scrollbar-gutter:stable —— 它在不需捲動時也永遠預留捲動條空間, 於 240px 寬的 popup
//內是一條明顯的無用留白; 改以 thin 捲動條, 只在真的超出時出現。
if (this.maxHeight) {
s.maxHeight = this.maxHeight
s.overflowY = 'auto'
s.scrollbarWidth = 'thin'
}
return s
},
edgeWidthMax() {
return EDGE_WIDTH_MAX
},
},
methods: {
isEx(key) {
return this.excludes.indexOf(key) >= 0
},
isGroupOpen(key) {
return this.openGroupsLocal.indexOf(key) >= 0
},
setGroupOpen(key, b) {
const i = this.openGroupsLocal.indexOf(key)
if (b && i < 0) this.openGroupsLocal.push(key)
else if (!b && i >= 0) this.openGroupsLocal.splice(i, 1)
},
//有效值(item → defaults; 逐欄位政策 FIELD_POLICY)
eff(key) {
return effectiveField(key, this.item, this.defaults)
},
onFontSizeInput(val) {
let n = Number(val)
const d = this.defaults
if (!val || isNaN(n) || n < d.fontSizeMin) return
if (n > d.fontSizeMax) n = d.fontSizeMax
this.$emit('update', 'fontSize', n)
},
onEdgeWidthInput(val) {
let n = Number(val)
if (!val || isNaN(n) || n < 1) return
if (n > EDGE_WIDTH_MAX) n = EDGE_WIDTH_MAX
this.$emit('update', 'edgeWidth', n)
},
},
}