/**
* opt 解析 —— 單一來源。WFlowVue 之每個 opt 鍵對應一個 computed(名稱即鍵名, 由 OPT_SPEC 生成),
* 不再逐鍵手寫 fallback 樣板; 群組型(defNode / defConn / menu)以具名解析函式提供。
*
* kind(回退規則, 依 JSDoc 契約逐鍵指定):
* defined : opt 值 !== undefined 即採用(布林/數值 0 為合法值)
* truthy : opt 值為 truthy 即採用(空字串/0/null 回退)
* enum : 值須在 values 內, 否則回退
* nonneg : 有限且 >= 0 之數值, 否則回退
* padding : 非負數值或 { top,right,bottom,left } 物件, 逐邊正規化(非法之邊回退 def)
* notFalse : 只有明確 false 才關閉(預設開)
* nullable : truthy 即採用, 否則 null
* fn : 須為函式, 否則 null
* text : 非空字串即採用, 否則(非字串、空字串)回退 def; 純空白照收(顯示文字用)
*/
import { NODE_DEFAULTS, CONN_DEFAULTS, SETTINGS_TRIGGERS } from './defaults.mjs'
import { resolvePadding } from './viewport.mjs'
import { settingsTextOptSpec } from './settingsTexts.mjs'
export const OPT_SPEC = {
//尺寸
widthInp: { key: 'width', kind: 'truthy', def: 800 },
heightInp: { key: 'height', kind: 'truthy', def: 600 },
//互動能力
nodesDraggable: { kind: 'defined', def: true },
nodesConnectable: { kind: 'defined', def: true },
nodesResizable: { kind: 'defined', def: true },
elementsSelectable: { kind: 'defined', def: true },
selectNodesOnDrag: { kind: 'defined', def: true },
multiSelectEnabled: { kind: 'defined', def: true },
boxSelectionKeyCode: { kind: 'truthy', def: 'Shift' },
multiSelectionKeyCode: { kind: 'truthy', def: 'Shift' },
deleteKeyEnabled: { kind: 'defined', def: false },
deleteKeyCode: { kind: 'truthy', def: 'Backspace' },
snapToGrid: { kind: 'defined', def: false },
snapGridSize: { kind: 'truthy', def: 20 },
//設定入口: 'hover' | 'click' | 'dblclick'(非法值回退 dblclick)
nodesSettingsEnabled: { kind: 'defined', def: true },
connsSettingsEnabled: { kind: 'defined', def: true },
nodesSettingsTrigger: { kind: 'enum', values: SETTINGS_TRIGGERS, def: 'dblclick' },
connsSettingsTrigger: { kind: 'enum', values: SETTINGS_TRIGGERS, def: 'dblclick' },
nodesSettingsExcludes: { kind: 'truthy', def: [] },
connsSettingsExcludes: { kind: 'truthy', def: [] },
//設定彈窗之顯示文字: 一句一鍵(節點 26 / 連線 48, 如 nodesSettingsTextLabelName), 鍵名與英文預設由 settingsTexts 產生
...settingsTextOptSpec(),
//視口
zoomOnScroll: { kind: 'defined', def: true },
//捏合縮放(觸控雙指): 與 zoomOnScroll 各自獨立, 不想要觸控縮放者設 false
zoomOnPinch: { kind: 'defined', def: true },
panOnDrag: { kind: 'defined', def: true },
zoomMin: { kind: 'defined', def: 0.5 },
zoomMax: { kind: 'defined', def: 2 },
zoom: { kind: 'defined', def: 1 },
center: { kind: 'truthy', def: [0, 0] },
panLimits: { kind: 'nullable' },
fitViewOnInit: { kind: 'notFalse' },
fitViewPadding: { kind: 'padding', def: 50 },
//建線預覽線
defConnCreatingType: { kind: 'truthy', def: 'bezier' },
defConnCreatingEdgeColor: { kind: 'truthy', def: CONN_DEFAULTS.edgeColor },
defConnCreatingEdgeWidth: { kind: 'defined', def: 1 },
defConnCreatingEdgeDasharray: { kind: 'truthy', def: '5 5' },
//畫布背景
platformBackgroundPatternType: { kind: 'truthy', def: 'dots' },
platformBackgroundPatternGap: { kind: 'defined', def: 20 },
platformBackgroundPatternSize: { kind: 'defined', def: 1 },
platformBackgroundPatternColor: { kind: 'truthy', def: '#81818a' },
platformBackgroundColor: { kind: 'truthy', def: '#fff' },
//popup 樣式
settingsPopupBackgroundColor: { kind: 'truthy', def: '#fff' },
settingsPopupTextColor: { kind: 'truthy', def: '#333' },
settingsPopupTextFontSize: { kind: 'truthy', def: '12px' },
//設定表單之最大高度(CSS 長度字串); 超出即表單內捲動, 使屬性多時 popup 不會長過視窗。
//kind 為 defined 而非 truthy: 空字串是「明確不設限」之合法值, truthy 會把它回退成預設值而使該契約失效
settingsPopupMaxHeight: { kind: 'defined', def: '400px' },
inforPopupBackgroundColor: { kind: 'truthy', def: '#fff' },
inforPopupTitleTextColor: { kind: 'truthy', def: '#333' },
inforPopupTitleTextFontSize: { kind: 'truthy', def: '12px' },
inforPopupDescriptionTextColor: { kind: 'truthy', def: '#888' },
inforPopupDescriptionTextFontSize: { kind: 'truthy', def: '10px' },
//callback
funValidConnCreating: { kind: 'nullable' },
//刪除確認(async): 未提供即直接刪除, 提供則須回傳 true 才真的刪除
funConfirmDeleting: { kind: 'fn' },
}
/** 依 OPT_SPEC 解析單一鍵 */
export function resolveOptValue(opt, name) {
const spec = OPT_SPEC[name]
if (!spec) throw new Error(`resolveOpt: unknown opt name '${name}'`)
const v = (opt || {})[spec.key || name]
switch (spec.kind) {
case 'defined': return v !== undefined ? v : spec.def
case 'truthy': return v || spec.def
case 'enum': return spec.values.indexOf(v) >= 0 ? v : spec.def
case 'nonneg': return (typeof v === 'number' && isFinite(v) && v >= 0) ? v : spec.def
case 'padding': return resolvePadding(v, spec.def)
case 'notFalse': return v !== false
case 'nullable': return v || null
case 'fn': return typeof v === 'function' ? v : null
case 'text': return (typeof v === 'string' && v !== '') ? v : spec.def
default: throw new Error(`resolveOpt: unknown kind '${spec.kind}'`)
}
}
/** 已移除之 opt 鍵 → 取代之鍵(墓碑: 只用於點名新鍵, 不讀其值、不生效, 故非相容層) */
export const REMOVED_OPT_KEYS = Object.freeze({
//色票確認鈕文字由節點、連線共用一個鍵改為各一個(節點與連線分開命名)
settingsColorConfirmText: 'opt.nodesSettingsColorConfirmText and opt.connsSettingsColorConfirmText',
})
//拼錯即無聲失效之鍵族: 以此前綴開頭而不在 OPT_SPEC 者視為未知鍵(其他 opt 鍵另有具名解析, 不在此檢查範圍)
const CHECKED_PREFIXES = ['nodesSettings', 'connsSettings', 'settings']
/**
* opt 之誤用(供 WFlowVue 逐則警告一次): 已移除之鍵、上列鍵族中之未知鍵、顯示文字鍵之值非字串。
* 只讀取需檢查之鍵之值(不碰 nodes / conns 等資料鍵), 避免警告之 computed 依賴到圖資料。
*/
export function collectOptIssues(opt) {
const o = Object.prototype.toString.call(opt) === '[object Object]' ? opt : {}
const specByKey = {}
for (const name of Object.keys(OPT_SPEC)) specByKey[OPT_SPEC[name].key || name] = OPT_SPEC[name]
const msgs = []
const unknown = []
const notText = []
for (const k of Object.keys(o)) {
if (Object.prototype.hasOwnProperty.call(REMOVED_OPT_KEYS, k)) {
if (o[k] !== undefined) msgs.push(`opt.${k} has been removed and has no effect; use ${REMOVED_OPT_KEYS[k]} instead`)
continue
}
const spec = Object.prototype.hasOwnProperty.call(specByKey, k) ? specByKey[k] : null
if (!spec) {
if (CHECKED_PREFIXES.some(p => k.indexOf(p) === 0)) unknown.push(k)
continue
}
if (spec.kind === 'text') {
const v = o[k]
if (v !== undefined && v !== null && typeof v !== 'string') notText.push(k)
}
}
if (unknown.length) msgs.push(`unknown opt keys ignored: ${unknown.join(', ')}`)
if (notText.length) msgs.push(`opt texts must be strings, defaults used for: ${notText.join(', ')}`)
return msgs
}
/** 全部純量鍵一次解析(測試/除錯用; 元件內以 optComputeds 逐鍵 computed, 避免整包失效) */
export function resolveOpt(opt) {
const r = {}
for (const name of Object.keys(OPT_SPEC)) r[name] = resolveOptValue(opt, name)
return r
}
/** 供 Vue computed 展開: 每個 OPT_SPEC 鍵一個 computed(讀 this.opt) */
export function optComputeds() {
const c = {}
for (const name of Object.keys(OPT_SPEC)) {
c[name] = function() {
return resolveOptValue(this.opt, name)
}
}
return c
}
/** 節點預設(opt.defNode* / opt.defHandle* → NODE_DEFAULTS); 數值型以 !== undefined 判斷(0 為合法之框線寬) */
export function resolveDefNode(opt) {
const o = opt || {}
const d = NODE_DEFAULTS
return {
shape: o.defNodeShape || d.shape,
width: o.defNodeWidth || d.width,
height: o.defNodeHeight || d.height,
fontSize: o.defNodeFontSize || d.fontSize,
fontSizeMin: o.defNodeFontSizeMin || d.fontSizeMin,
fontSizeMax: o.defNodeFontSizeMax || d.fontSizeMax,
fontColor: o.defNodeFontColor || d.fontColor,
faceColor: o.defNodeFaceColor || d.faceColor,
edgeColor: o.defNodeEdgeColor || d.edgeColor,
edgeWidth: o.defNodeEdgeWidth !== undefined ? o.defNodeEdgeWidth : d.edgeWidth,
popupDirection: o.defNodePopupDirection || d.popupDirection,
//連接點(把手)樣式(四把手同一組)
handleFaceColor: o.defHandleFaceColor || d.handleFaceColor,
handleEdgeColor: o.defHandleEdgeColor || d.handleEdgeColor,
handleEdgeWidth: o.defHandleEdgeWidth !== undefined ? o.defHandleEdgeWidth : d.handleEdgeWidth,
handleSize: o.defHandleSize || d.handleSize,
}
}
/** 連線預設(opt.defConn* → CONN_DEFAULTS): 兩端方位/箭頭/animated/step offset 皆在此 */
export function resolveDefConn(opt) {
const o = opt || {}
const d = CONN_DEFAULTS
return {
type: o.defConnType || d.type,
fontSize: o.defConnFontSize || d.fontSize,
fontSizeMin: o.defConnFontSizeMin || d.fontSizeMin,
fontSizeMax: o.defConnFontSizeMax || d.fontSizeMax,
fontColor: o.defConnFontColor || d.fontColor,
edgeColor: o.defConnEdgeColor || d.edgeColor,
edgeWidth: o.defConnEdgeWidth !== undefined ? o.defConnEdgeWidth : d.edgeWidth,
edgeDasharray: o.defConnEdgeDasharray || '',
fromPosition: o.defConnFromPosition || d.fromPosition,
toPosition: o.defConnToPosition || d.toPosition,
markerFrom: o.defConnMarkerFrom || d.markerFrom,
markerFromSize: o.defConnMarkerFromSize || d.markerFromSize,
markerFromFaceColor: o.defConnMarkerFromFaceColor || d.markerFromFaceColor,
markerFromEdgeColor: o.defConnMarkerFromEdgeColor || d.markerFromEdgeColor,
markerTo: o.defConnMarkerTo || d.markerTo,
markerToSize: o.defConnMarkerToSize || d.markerToSize,
markerToFaceColor: o.defConnMarkerToFaceColor || d.markerToFaceColor,
markerToEdgeColor: o.defConnMarkerToEdgeColor || d.markerToEdgeColor,
animated: o.defConnAnimated !== undefined ? o.defConnAnimated : d.animated,
defOffset: o.defOffset != null ? o.defOffset : d.defOffset,
popupDirection: o.defConnPopupDirection || d.popupDirection,
}
}
/**
* 垂直選單設定: 自 opt 原樣透傳(不套預設)——各項未給或型別不符由 Controls.vue 之 menuDef 回退,
* 預設值單一來源在 Controls(icon 預設需 @mdi/js 常數), 本模組刻意不複製一份。
*/
export const MENU_OPT_KEYS = [
'useMenu', 'menuPosition', 'menuYShift', 'useSetting',
'menuSettingIcon', 'menuSettingTooltip',
'useMenuItemZoomIn', 'menuZoomInIcon', 'menuZoomInTooltip',
'useMenuItemZoomOut', 'menuZoomOutIcon', 'menuZoomOutTooltip',
'useMenuItemFitView', 'menuFitViewIcon', 'menuFitViewTooltip',
'useMenuItemLock', 'menuLockIcon', 'menuLockTooltip', 'menuLockIconLocked', 'menuLockTooltipLocked',
'menuIconColor', 'menuIconColorHover', 'menuIconColorFocus', 'menuIconSize',
'menuBackgroundColor', 'menuBackgroundColorHover', 'menuBackgroundColorFocus',
'menuSeparatorColor', 'menuShadow',
'menuTooltipTextColor', 'menuTooltipTextFontSize', 'menuTooltipBackgroundColor',
]
export function pickMenuOpt(opt) {
const o = opt || {}
const r = {}
for (const k of MENU_OPT_KEYS) r[k] = o[k]
return r
}