From ecba7f726f70b37126d852c38c7e3eec7b04d730 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 9 Jul 2026 22:15:54 -0400 Subject: feat: old standalone trigger systems completely unified into trigger->condition->action system --- internal/admin/static/intereditor.js | 1074 +++++++++++++++++++--------------- 1 file changed, 591 insertions(+), 483 deletions(-) (limited to 'internal/admin/static/intereditor.js') diff --git a/internal/admin/static/intereditor.js b/internal/admin/static/intereditor.js index 5cf39a1..653bccf 100644 --- a/internal/admin/static/intereditor.js +++ b/internal/admin/static/intereditor.js @@ -1,38 +1,172 @@ -// intereditor.js — Structured condition & action editors for on_use/on_look/on_kill. -// Replaces free-form JSON textareas with button-driven pickers. -// Shared by objecteditor.js and mobeditor.js. +// intereditor.js — Structured condition & step editors for trigger blocks. +// Drives the unified Trigger model: +// Trigger = { lock, item_id?, condition?, steps[], on_player_flag?, on_global_flag?, value? } +// Step = { condition?, wait, messages[], broadcast, broadcast_global, +// spawn_mob, despawn_mob, set_global_flags, set_player_flags, +// give_item, take_item, teleport, heal, credits, aps_node } +// Shared by objecteditor.js, mobeditor.js, and map.js. // ── Condition leaf types ── var IE_COND_TYPES = [ {type: 'global_flag', label: 'Global Flag', hint: 'global flag name'}, {type: 'player_flag', label: 'Player Flag', hint: 'player flag name'}, + {type: 'room', label: 'In Room', hint: 'room ID'}, {type: 'has_item', label: 'Has Item', hint: 'item ID'}, {type: 'min_credits', label: 'Min Credits', hint: 'amount'}, ]; +// ── Effect field types ── +// +// Every block is sequence-scoped now: broadcast / broadcast_global / +// spawn_mob / despawn_mob are available everywhere. Messages and condition +// are handled separately (plain-string message rows; per-step condition). + var IE_ACT_TYPES = [ - {key: 'set_global_flags', label: 'Set Global Flags', kind: 'kv'}, - {key: 'set_player_flags', label: 'Set Player Flags', kind: 'kv'}, - {key: 'give_item', label: 'Give Item', kind: 'search', entity: 'items'}, - {key: 'take_item', label: 'Take Item', kind: 'search', entity: 'items'}, - {key: 'teleport', label: 'Teleport', kind: 'number', hint: 'room ID'}, - {key: 'heal', label: 'Heal', kind: 'number', hint: 'HP to restore'}, - {key: 'credits', label: 'Credits', kind: 'number', hint: 'amount (negative = cost)'}, - {key: 'aps_node', label: 'APS Node', kind: 'checkbox'}, + {key: 'wait', label: 'Wait', kind: 'number', hint: 'ticks before this step'}, + {key: 'broadcast', label: 'Broadcast', kind: 'text', hint: '%p = player'}, + {key: 'broadcast_global', label: 'Broadcast Global', kind: 'text', hint: '%p = player'}, + {key: 'spawn_mob', label: 'Spawn Mob', kind: 'search', entity: 'mobs'}, + {key: 'despawn_mob', label: 'Despawn Mob', kind: 'search', entity: 'mobs'}, + {key: 'set_global_flags', label: 'Set Global Flags', kind: 'kv'}, + {key: 'set_player_flags', label: 'Set Player Flags', kind: 'kv'}, + {key: 'give_item', label: 'Give Item', kind: 'search', entity: 'items'}, + {key: 'take_item', label: 'Take Item', kind: 'search', entity: 'items'}, + {key: 'teleport', label: 'Teleport', kind: 'number', hint: 'room ID'}, + {key: 'heal', label: 'Heal', kind: 'number', hint: 'HP to restore'}, + {key: 'credits', label: 'Credits', kind: 'number', hint: 'amount (negative = cost)'}, + {key: 'aps_node', label: 'APS Node', kind: 'checkbox'}, ]; -// Sequence-only effects (only shown for scope === 'seq'). -var IE_ACT_TYPES_SEQ = [ - {key: 'broadcast', label: 'Broadcast', kind: 'text', hint: '%p = player'}, - {key: 'broadcast_global', label: 'Broadcast Global', kind: 'text', hint: '%p = player'}, - {key: 'spawn_mob', label: 'Spawn Mob', kind: 'search', entity: 'mobs'}, - {key: 'despawn_mob', label: 'Despawn Mob', kind: 'search', entity: 'mobs'}, +// ── Step-kind buttons for the bottom add-bar (each click creates a new +// step initialised with that single action kind) and the per-step +// "+" toggle bar. messages is handled separately from IE_ACT_TYPES. ── + +var IE_STEP_KINDS = [ + {key: 'messages', label: 'Message', kind: 'messages'}, ]; +IE_ACT_TYPES.forEach(function(at) { + IE_STEP_KINDS.push(at); +}); + +// ── Rendering helpers ── + +// ie_renderStepAddbar: the per-step action add bar revealed by the "+" toggle. +// Only shows kinds not already present on this step. +function ie_renderStepAddbar(step, secId, idx, si, secPath) { + var h = ''; + IE_STEP_KINDS.forEach(function(sk) { + if (sk.key === 'messages') { + if (step && Array.isArray(step.messages)) return; + } else { + if (step && Object.prototype.hasOwnProperty.call(step, sk.key)) { + if (sk.kind === 'checkbox' && step[sk.key] !== true) { /* re-addable */ } + else return; + } + } + if (sk.key === 'messages') { + h += ''; + } else { + h += ''; + } + }); + return h; +} + +// ie_renderBottomAddbar: the always-visible bar at the foot of the step list. +// Each button creates a new step initialised with that single action kind. +function ie_renderBottomAddbar(secId, secPath, idx) { + var h = ''; + IE_STEP_KINDS.forEach(function(sk) { + h += ''; + }); + return h; +} + +// ie_addStepKind creates a new step initialised with a single action kind. +function ie_addStepKind(secId, secPath, idx, kind) { + var ed = window._editorData; + if (!ed) return; + ced_syncDOMToData(ed); + if (window._ieSyncAll) window._ieSyncAll(ed); + if (!ed[secPath]) ed[secPath] = []; + if (!ed[secPath][idx]) ed[secPath][idx] = {}; + if (!ed[secPath][idx].steps || !Array.isArray(ed[secPath][idx].steps)) ed[secPath][idx].steps = []; + var step = {}; + if (kind === 'messages') { + step.messages = ['']; + } else if (kind === 'wait') { + step.wait = 1; + } else { + var at = IE_ACT_TYPES.find(function(a) { return a.key === kind; }); + if (!at) return; + if (at.kind === 'kv') step[kind] = {}; + else if (at.kind === 'checkbox') step[kind] = true; + else if (at.kind === 'number') step[kind] = 0; + else step[kind] = ''; + } + ed[secPath][idx].steps.push(step); + window._ieRenderCurrent(); +} // Sentinel child-index for a root-level single leaf (no parent group). var IE_ROOT_LEAF = -1; +// ── Condition lens helpers (entry vs step) ── +// +// kind is the string-encoded condition anchor: 'entry' for the trigger's own +// condition, or 'step-' for the nth step's condition. Internally we parse +// it to the numeric step index (-1 = entry). + +function _ie_kindStep(kind) { + if (kind === undefined || kind === null || kind === 'entry') return -1; + if (typeof kind === 'string' && kind.indexOf('step-') === 0) { + var n = parseInt(kind.slice(5), 10); + return isNaN(n) ? -1 : n; + } + return -1; +} + +function _ie_getCond(ed, secPath, idx, kind) { + var si = _ie_kindStep(kind); + if (!ed[secPath] || !ed[secPath][idx]) return null; + if (si < 0) return ed[secPath][idx].condition || null; + if (!ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return null; + return ed[secPath][idx].steps[si].condition || null; +} + +function _ie_setCond(ed, secPath, idx, kind, newCond) { + var si = _ie_kindStep(kind); + if (!ed[secPath]) ed[secPath] = []; + if (!ed[secPath][idx]) ed[secPath][idx] = {}; + if (si < 0) { + if (newCond) ed[secPath][idx].condition = newCond; + else delete ed[secPath][idx].condition; + } else { + if (!ed[secPath][idx].steps) ed[secPath][idx].steps = []; + if (!ed[secPath][idx].steps[si]) ed[secPath][idx].steps[si] = {}; + if (newCond) ed[secPath][idx].steps[si].condition = newCond; + else delete ed[secPath][idx].steps[si].condition; + } +} + +function _ie_syncCond(ed, secId, secPath, idx, kind) { + var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); + if (!card) return; + var si = _ie_kindStep(kind); + var container; + if (si < 0) { + container = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"]'); + } else { + container = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); + } + if (!container) return; + var condRoot = container.querySelector('.ie-cond-root'); + if (!condRoot) return; + var cond = ie_collectCond(condRoot); + _ie_setCond(ed, secPath, idx, kind, cond); +} + // ── Navigation in condition tree ── function ie_condAt(cond, path) { @@ -48,7 +182,6 @@ function ie_condAt(cond, path) { return cur; } -// Replaces the condition at path within the overall tree. Returns new root. function ie_condReplace(cond, path, newVal) { if (!path) return newVal; if (!cond) return cond; @@ -72,13 +205,7 @@ function ie_condReplace(cond, path, newVal) { } // ── Clean condition tree for save ── -// -// The live editor tree intentionally holds empty leaves/groups while the user -// is building up a condition (so clicking a second leaf button adds a sibling -// rather than replacing the first). This walker strips those in-progress -// artifacts before save: drops leaves whose value is missing/empty, drops -// 0-child groups, and collapses 1-child non-NOT groups to their child. min_credits -// keeps the value 0 (falsy in JS but a valid threshold). + function ie_cleanCondition(cond) { if (!cond || typeof cond !== 'object') return null; if (cond.all_of || cond.any_of) { @@ -91,7 +218,6 @@ function ie_cleanCondition(cond) { r[mode] = cleaned; return r; } - // Leaf var not = !!cond.not; var leaf = null; if (cond.global_flag !== undefined) { @@ -104,6 +230,8 @@ function ie_cleanCondition(cond) { leaf = { player_flag: cond.player_flag }; if (cond.value !== undefined) leaf.value = cond.value; } + } else if (cond.room !== undefined) { + if (cond.room) leaf = { room: parseInt(cond.room, 10) || 0 }; } else if (cond.has_item !== undefined) { if (cond.has_item) leaf = { has_item: cond.has_item }; } else if (cond.min_credits !== undefined) { @@ -120,12 +248,6 @@ function ie_cleanCondition(cond) { function ie_collectCond(rootEl) { if (!rootEl) return null; - // A .ie-cond-root wraps either a single group (.ie-cond) or a single bare - // leaf (.ie-cond-leaf). Unwrap it so sync is the pure inverse of render: - // cond-root > .ie-cond(group) -> ie_collectCond(.ie-cond) - // cond-root > .ie-cond-leaf -> ie_collectLeaf(leafEl) - // The old version recursed into the .ie-cond child and then wrapped the - // result, producing {all_of:[{all_of:[...]}]} for any group condition. if (rootEl.classList.contains('ie-cond-root')) { var groupEl = rootEl.querySelector(':scope > .ie-cond'); if (groupEl) return ie_collectCond(groupEl); @@ -134,7 +256,6 @@ function ie_collectCond(rootEl) { return null; } - // rootEl is a .ie-cond group box. var mode = 'all_of'; var modeSel = rootEl.querySelector(':scope > .ie-cond-modebar .ie-cond-modesel'); if (modeSel) mode = modeSel.value; @@ -155,13 +276,6 @@ function ie_collectCond(rootEl) { }); } - // Preserve empty groups (no modebar rendered for <2 children, so empty groups - // always serialize as {all_of:[]}). Without this, "+ Group" would vanish on - // the next sync before the user could add their first leaf, and the - // subsequent "+ leaf" would turn it into a bare leaf — silently dropping - // the user's group intent. Empty {all_of:[]} is harmless game-side - // (checkCondition returns true, equivalent to no condition) and - // ced_cleanOutput strips it from saved YAML. var result = {}; if (not) result.not = true; result[mode] = items; @@ -178,11 +292,6 @@ function ie_collectLeaf(el) { var valInput = el.querySelector('.ie-cond-val'); var val = valInput ? valInput.value.trim() : ''; - // Always return the leaf (even when val is empty) so the in-memory - // condition tree keeps "in-progress" leaves the user just added. Without - // this, clicking a second leaf button would sync, see null, and wipe the - // first leaf — breaking multi-add and nested-group building. ie_cleanCondition - // strips empty leaves at save time. switch (leafType) { case 'global_flag': result.global_flag = val; @@ -200,6 +309,9 @@ function ie_collectLeaf(el) { try { result.value = JSON.parse(sv); } catch(e) { result.value = sv; } } break; + case 'room': + result.room = parseInt(val) || 0; + break; case 'has_item': result.has_item = val; break; @@ -212,123 +324,30 @@ function ie_collectLeaf(el) { return result; } -// ── Collect action from DOM ── - -function ie_collectAct(rootEl) { - if (!rootEl) return null; - var r = {}; - - // Messages — collected from the extendable delay+message rows. - var msgsKv = rootEl.querySelector('.ie-act-kv[data-ie-act-key="messages"]'); - if (msgsKv) { - var list = []; - msgsKv.querySelectorAll('.ie-kv-row').forEach(function(row) { - var delayEl = row.querySelector('.ie-act-msg-delay'); - var msgEl = row.querySelector('.ie-act-msg'); - var d = delayEl ? parseInt(delayEl.value) || 0 : 0; - var m = msgEl ? msgEl.value.trim() : ''; - list.push({ message: m, delay: d }); - }); - r.messages = list; - } - - // KV-type effects — include the map even when empty (as long as the - // .ie-act-kv element exists in the DOM). This lets a just-added section - // (e.g. "+ Set Player Flags" → no rows yet) survive the next sync instead - // of being dropped on the next add-effect call. ced_cleanOutput strips - // empty maps at save time. - rootEl.querySelectorAll('.ie-act-kv').forEach(function(kvEl) { - var key = kvEl.getAttribute('data-ie-act-key'); - if (!key) return; - if (key === 'messages') return; - var map = {}; - kvEl.querySelectorAll('.ie-kv-row').forEach(function(row) { - var kEl = row.querySelector('.ie-kv-key'); - var vEl = row.querySelector('.ie-kv-val'); - if (kEl && kEl.value.trim() && vEl) { - var v = vEl.value.trim(); - var num = parseFloat(v); - map[kEl.value.trim()] = v === 'true' ? true : (v === 'false' ? false : (!isNaN(num) ? num : v)); - } - }); - r[key] = map; - }); - - // Scalar effects — preserve empty strings so a present-but-unfilled input - // round-trips on every sync. - rootEl.querySelectorAll('.ie-act-input').forEach(function(el) { - var key = el.getAttribute('data-ie-act-key'); - if (!key) return; - var kind = el.getAttribute('data-ie-act-kind'); - if (kind === 'checkbox') { - r[key] = el.checked; - } else if (kind === 'number') { - var n = parseFloat(el.value); - if (isNaN(n)) return; - r[key] = n; - } else { - r[key] = el.value.trim(); - } - }); - - if (Object.keys(r).length === 0) return null; - return r; -} - -// ── Sync all interaction DOM back to data ── - -function ie_syncAllInteractions(data, secs) { - if (!data || !secs) return; - secs.forEach(function(sec) { - if (!sec.interactionStyle || !sec.isArray) return; - var p = sec.path; - if (!data[p]) return; - var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]'); - if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - rows.forEach(function(row, idx) { - if (!data[p][idx]) data[p][idx] = {}; - var condRoot = row.querySelector('.ie-cond-root'); - if (condRoot) { - var cond = ie_collectCond(condRoot); - if (cond) data[p][idx].condition = cond; - else delete data[p][idx].condition; - } - var actRoot = row.querySelector('.ie-act-root'); - if (actRoot) { - var act = ie_collectAct(actRoot); - if (act) data[p][idx].action = act; - else delete data[p][idx].action; - } - }); - }); -} - // ── Render condition ── -function ie_renderCond(condObj, secId, idx, secPath) { +function ie_renderCond(condObj, secId, idx, secPath, kind) { + kind = kind || 'entry'; var h = ''; var isGroup = condObj && (condObj.all_of || condObj.any_of); if (condObj && typeof condObj === 'object') { h += '
'; - h += ie_renderGroup(condObj, secId, secPath, idx, ''); + h += ie_renderGroup(condObj, secId, secPath, idx, '', kind); h += '
'; } - // For a group root, the group's own addbar (rendered inside _ie_renderGroupNode) - // already covers appending — don't emit a duplicate top-level addbar. var showAddbar = !isGroup; if (showAddbar) { h += '
'; IE_COND_TYPES.forEach(function(ct) { - h += ''; + h += ''; }); - h += ''; + h += ''; h += '
'; } return h; } -function ie_renderGroup(cond, secId, secPath, idx, gpath) { +function ie_renderGroup(cond, secId, secPath, idx, gpath, kind) { var mode = null, children = null; if (cond.all_of) { mode = 'all_of'; children = cond.all_of; } else if (cond.any_of) { mode = 'any_of'; children = cond.any_of; } @@ -336,74 +355,74 @@ function ie_renderGroup(cond, secId, secPath, idx, gpath) { var not = !!cond.not; if (mode && children) { - return _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath); + return _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath, kind); } - // Leaf - if (cond.global_flag !== undefined) return _ie_renderLeaf('global_flag', cond.global_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF); - if (cond.player_flag !== undefined) return _ie_renderLeaf('player_flag', cond.player_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF); - if (cond.has_item !== undefined) return _ie_renderLeaf('has_item', cond.has_item, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF); - if (cond.min_credits !== undefined) return _ie_renderLeaf('min_credits', cond.min_credits, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF); + if (cond.global_flag !== undefined) return _ie_renderLeaf('global_flag', cond.global_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); + if (cond.player_flag !== undefined) return _ie_renderLeaf('player_flag', cond.player_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); + if (cond.room !== undefined) return _ie_renderLeaf('room', cond.room, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); + if (cond.has_item !== undefined) return _ie_renderLeaf('has_item', cond.has_item, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); + if (cond.min_credits !== undefined) return _ie_renderLeaf('min_credits', cond.min_credits, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); return ''; } -function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath) { +function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath, kind) { var h = ''; if (children.length === 0) { - // Empty group: single line — addbar buttons left, X right. h += '
'; h += '
'; IE_COND_TYPES.forEach(function(ct) { - h += ''; + h += ''; }); - h += ''; - h += ''; + h += ''; + h += ''; h += '
'; h += '
'; return h; } - // Non-empty group: modebar (no X) + children + addbar below. h += '
'; h += '
'; if (children.length >= 2) { - h += ''; h += ''; h += ''; h += ''; } if (children.length >= 2 || not) { - h += ''; + h += ''; } h += '
'; h += '
'; children.forEach(function(child, ci) { var cpath = gpath ? gpath + '-' + ci : String(ci); if (child.all_of || child.any_of) { - h += ie_renderGroup(child, secId, secPath, idx, cpath); + h += ie_renderGroup(child, secId, secPath, idx, cpath, kind); } else { var cval = '', csub = '', ctype = '', cnot = !!child.not; if (child.global_flag !== undefined) { ctype = 'global_flag'; cval = child.global_flag; csub = child.value; } else if (child.player_flag !== undefined) { ctype = 'player_flag'; cval = child.player_flag; csub = child.value; } + else if (child.room !== undefined) { ctype = 'room'; cval = child.room; } else if (child.has_item !== undefined) { ctype = 'has_item'; cval = child.has_item; } else if (child.min_credits !== undefined) { ctype = 'min_credits'; cval = child.min_credits; } - if (ctype) h += _ie_renderLeaf(ctype, cval, csub, cnot, secId, secPath, idx, gpath, ci); + if (ctype) h += _ie_renderLeaf(ctype, cval, csub, cnot, secId, secPath, idx, gpath, ci, kind); } }); h += '
'; h += '
'; IE_COND_TYPES.forEach(function(ct) { - h += ''; + h += ''; }); - h += ''; + h += ''; h += '
'; h += '
'; return h; } -function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gpath, ci) { +function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gpath, ci, kind) { + kind = kind || 'entry'; var label = ''; IE_COND_TYPES.forEach(function(ct) { if (ct.type === leafType) label = ct.label; }); var hint = ''; @@ -420,7 +439,7 @@ function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gp h += ''; h += ''; h += ''; - } else if (leafType === 'min_credits') { + } else if (leafType === 'min_credits' || leafType === 'room') { h += ''; } else { h += ''; @@ -429,64 +448,33 @@ function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gp h += '='; h += ''; } - h += ''; - h += ''; + h += ''; + h += ''; h += ''; return h; } -// ── Render action ── +// ── Render step effects (everything except messages and condition). +// The add-bar is rendered separately via ie_renderStepAddbar. ── -function ie_renderAct(actionObj, scope, secId, idx, secPath) { +function ie_renderAct(stepObj, secId, idx, si, secPath) { var hasAny = false; var rows = ''; - // Messages — extendable list of {delay, message} entries. Mirrors the KV-section - // pattern: when the 'messages' key is present we render the section with rows - // and an internal + Add; when it goes away (last row removed) the section - // disappears and the addbar + Add Message button reappears. There is no - // section-level X. - var msgsPresent = actionObj && Object.prototype.hasOwnProperty.call(actionObj, 'messages') && Array.isArray(actionObj.messages); - if (msgsPresent) { - var msgs = actionObj.messages; - hasAny = true; - var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ')'; - rows += '
'; - rows += '
Messages
'; - rows += '
'; - if (msgs.length === 0) { - rows += '
'; - rows += ''; - rows += '
'; - } else { - msgs.forEach(function(m, mi) { - rows += '
'; - rows += ''; - rows += '
'; - }); - } - rows += '
'; - rows += ''; - rows += '
'; - } - - // Standard effects IE_ACT_TYPES.forEach(function(at) { - if (!actionObj || !Object.prototype.hasOwnProperty.call(actionObj, at.key)) return; - var val = actionObj[at.key]; + if (!stepObj || !Object.prototype.hasOwnProperty.call(stepObj, at.key)) return; + var val = stepObj[at.key]; if (at.kind === 'checkbox' && val !== true) return; hasAny = true; if (at.kind === 'kv') { var kvMap = (val && typeof val === 'object') ? val : {}; var kvKeys = Object.keys(kvMap); - var kvRmAttr = 'ie_removeKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')'; + var kvRmAttr = 'ie_removeKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')'; rows += '
'; rows += '
' + esc(at.label) + '
'; rows += '
'; if (kvKeys.length === 0) { - // Start with a blank row so there are input boxes immediately. The - // row's X acts as the section's only remove path (no header X). rows += '
'; rows += ''; rows += '
'; @@ -498,92 +486,239 @@ function ie_renderAct(actionObj, scope, secId, idx, secPath) { }); } rows += '
'; - rows += ''; + rows += ''; rows += '
'; } else if (at.kind === 'checkbox') { rows += '
' + esc(at.label) + ''; rows += ''; - rows += ''; + rows += ''; rows += '
'; } else if (at.kind === 'search') { rows += '
' + esc(at.label) + ''; rows += ''; - rows += ''; + rows += ''; rows += '
'; } else { rows += '
' + esc(at.label) + ''; rows += ''; - rows += ''; + rows += ''; rows += '
'; } }); - // Seq-only effects - if (scope === 'seq') { - IE_ACT_TYPES_SEQ.forEach(function(at) { - if (!actionObj || !Object.prototype.hasOwnProperty.call(actionObj, at.key)) return; - var val = actionObj[at.key]; - if (val === undefined || val === null) return; - hasAny = true; - - if (at.kind === 'search') { - rows += '
' + esc(at.label) + ''; - rows += ''; - rows += ''; - rows += '
'; - } else { - rows += '
' + esc(at.label) + ''; - rows += ''; - rows += ''; - rows += '
'; - } - }); + if (hasAny) { + return '
' + rows + '
'; } + return ''; +} + +// ── Render messages sub-list (plain strings) ── - // Add effect buttons - var addbar = '
'; - if (!msgsPresent) { - addbar += ''; +function ie_renderMessages(step, secId, idx, si, secPath) { + var present = step && Array.isArray(step.messages); + if (!present) { + return ''; } - IE_ACT_TYPES.forEach(function(at) { - if (actionObj && Object.prototype.hasOwnProperty.call(actionObj, at.key)) { - if (at.kind === 'checkbox') { - if (actionObj[at.key] === true) return; - } else { - return; + var msgs = step.messages; + var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')'; + var h = '
'; + h += '
Messages
'; + h += '
'; + msgs.forEach(function(m, mi) { + var upBtn = mi > 0 + ? '' + : ''; + var dnBtn = mi < msgs.length - 1 + ? '' + : ''; + h += '
' + upBtn + dnBtn + '
'; + }); + h += '
'; + h += ''; + h += '
'; + return h; +} + +// ── Render steps list of a trigger entry ── + +function ie_renderStepList(steps, secId, idx, secPath) { + steps = steps || []; + var h = '
'; + steps.forEach(function(step, si) { + var stepCond = step && step.condition && typeof step.condition === 'object'; + h += '
'; + h += '
'; + h += 'Step ' + (si + 1) + ''; + h += ''; + h += ''; + if (si > 0) h += ''; + if (si < steps.length - 1) h += ''; + h += ''; + h += '
'; + + h += '
'; + if (stepCond) { + h += ie_renderCond(step.condition || null, secId, idx, secPath, 'step-' + si); + } else { + h += ''; + } + h += '
'; + + h += '
' + ie_renderAct(step, secId, idx, si, secPath) + '
'; + h += '
' + ie_renderStepAddbar(step, secId, idx, si, secPath) + '
'; + h += '
' + ie_renderMessages(step, secId, idx, si, secPath) + '
'; + + h += '
'; + }); + h += '
' + ie_renderBottomAddbar(secId, secPath, idx) + '
'; + h += '
'; + return h; +} + +// ── Collect effects (everything except messages and condition) ── + +function ie_collectAct(container) { + if (!container) return null; + var r = {}; + + container.querySelectorAll('.ie-act-kv').forEach(function(kvEl) { + var key = kvEl.getAttribute('data-ie-act-key'); + if (!key) return; + if (key === 'messages') return; + var map = {}; + kvEl.querySelectorAll('.ie-kv-row').forEach(function(row) { + var kEl = row.querySelector('.ie-kv-key'); + var vEl = row.querySelector('.ie-kv-val'); + if (kEl && kEl.value.trim() && vEl) { + var v = vEl.value.trim(); + var num = parseFloat(v); + map[kEl.value.trim()] = v === 'true' ? true : (v === 'false' ? false : (!isNaN(num) ? num : v)); } + }); + r[key] = map; + }); + + container.querySelectorAll('.ie-act-input').forEach(function(el) { + var key = el.getAttribute('data-ie-act-key'); + if (!key) return; + var kind = el.getAttribute('data-ie-act-kind'); + if (kind === 'checkbox') { + r[key] = el.checked; + } else if (kind === 'number') { + var n = parseFloat(el.value); + if (isNaN(n)) return; + r[key] = n; + } else { + r[key] = el.value.trim(); } - addbar += ''; }); - if (scope === 'seq') { - IE_ACT_TYPES_SEQ.forEach(function(at) { - if (actionObj && Object.prototype.hasOwnProperty.call(actionObj, at.key)) return; - addbar += ''; + + if (Object.keys(r).length === 0) return null; + return r; +} + +// ── Collect messages (plain strings) ── + +function ie_collectMessages(container) { + if (!container) return null; + var msgsKv = container.querySelector('.ie-act-kv[data-ie-act-key="messages"]'); + if (!msgsKv) return null; + var list = []; + msgsKv.querySelectorAll('.ie-kv-row .ie-act-msg').forEach(function(el) { + list.push(el.value.trim()); + }); + return list; +} + +// ── Sync a single step's DOM back into data ── + +function ie_syncStep(ed, secId, secPath, idx, si) { + var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); + if (!card) return; + var stepRow = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); + if (!stepRow) return; + if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return; + var step = ed[secPath][idx].steps[si]; + + var condRoot = stepRow.querySelector(':scope > .ie-step-cond-panel .ie-cond-root'); + if (condRoot) { + var cond = ie_collectCond(condRoot); + if (cond) step.condition = cond; else delete step.condition; + } + + var effContainer = stepRow.querySelector(':scope > .ie-step-effects'); + if (effContainer) { + var act = ie_collectAct(effContainer); + if (act) { + Object.keys(act).forEach(function(k) { step[k] = act[k]; }); + } + // Drop keys the user X'd out (no longer present in the DOM among the rendered IE_ACT_TYPES set). + IE_ACT_TYPES.forEach(function(at) { + if (at.key === 'messages') return; + if (!Object.prototype.hasOwnProperty.call(act || {}, at.key)) { + if (Object.prototype.hasOwnProperty.call(step, at.key)) delete step[at.key]; + } }); } - addbar += '
'; - // Only wrap the content + addbar in .ie-act-root when there's actual content. - // An empty action just emits the addbar (mirroring ie_renderCond, which - // emits an unwrapped addbar for an empty condition). This keeps the empty - // condition and empty action panels visually symmetric — one box, not two. - if (hasAny) { - return '
' + rows + addbar + '
'; + var msgsContainer = stepRow.querySelector(':scope > .ie-step-messages'); + if (msgsContainer) { + var list = ie_collectMessages(msgsContainer); + if (list !== null) step.messages = list; + else delete step.messages; + if (Array.isArray(step.messages) && step.messages.length === 0) { + // keep empty array — survives next render, ced_cleanOutput will strip + // it on save. + } } - return addbar; +} + +// ── Sync all interaction DOM back to data ── + +function ie_syncAllInteractions(data, secs) { + if (!data || !secs) return; + secs.forEach(function(sec) { + if (!sec.interactionStyle || !sec.isArray) return; + var p = sec.path; + if (!data[p]) return; + var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]'); + if (!card) return; + var rows = card.querySelectorAll('.obj-sub-row.ie-inter-row'); + rows.forEach(function(row, idx) { + if (!data[p][idx]) data[p][idx] = {}; + var lockCb = row.querySelector(':scope > .ie-inter-header .ie-lock-cb'); + if (lockCb) { + if (lockCb.checked) data[p][idx].lock = true; + else delete data[p][idx].lock; + } + var entryCondRoot = row.querySelector(':scope > .ie-inter-panel .ie-cond-root'); + if (entryCondRoot) { + var cond = ie_collectCond(entryCondRoot); + if (cond) data[p][idx].condition = cond; + else delete data[p][idx].condition; + } + if (!data[p][idx].steps) data[p][idx].steps = []; + var stepRows = row.querySelectorAll(':scope > .ie-inter-steps .ie-step-row'); + stepRows.forEach(function(stepRow) { + var si = parseInt(stepRow.getAttribute('data-step'), 10); + if (isNaN(si)) return; + ie_syncStep(data, sec.id, p, idx, si); + }); + }); + }); } // ── Add/Remove/Toggle: Condition ── -function ie_addLeaf(secId, secPath, idx, groupPath, leafType) { +function ie_addLeaf(secId, secPath, idx, kind, groupPath, leafType) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - var cond = (ed[secPath] && ed[secPath][idx]) ? ed[secPath][idx].condition : null; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); var group = ie_condAt(cond, groupPath); var leaf = {}; - leaf[leafType] = leafType === 'min_credits' ? 0 : ''; + leaf[leafType] = (leafType === 'min_credits' || leafType === 'room') ? 0 : ''; var newGroup; if (!group) { @@ -599,28 +734,21 @@ function ie_addLeaf(secId, secPath, idx, groupPath, leafType) { } var newCond = ie_condReplace(cond, groupPath, newGroup); - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (newCond) ed[secPath][idx].condition = newCond; - else delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, newCond); ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_removeCond(secId, secPath, idx, groupPath, childIdx) { +function ie_removeCond(secId, secPath, idx, kind, groupPath, childIdx) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - var cond = (ed[secPath] && ed[secPath][idx]) ? ed[secPath][idx].condition : null; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); - // Removing the single root leaf. Note: children of a *root-level group* - // are also rendered with groupPath='' (and a real child index), so the - // test must be childIdx, not groupPath, otherwise removing any leaf from - // a root group wipes the entire condition. if (childIdx === IE_ROOT_LEAF) { - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, null); ced_syncDOMToData(ed); - window._ieRenderOnly(); + window._ieRenderOnly(); return; } @@ -635,8 +763,6 @@ function ie_removeCond(secId, secPath, idx, groupPath, childIdx) { if (children.length === 0) { newGroup = null; } else if (children.length === 1 && !group.not) { - // Collapse a single-child non-NOT group to the child itself so removal - // doesn't leave a one-child all_of/any_of wrapper in the live tree. newGroup = children[0]; } else { newGroup = {}; @@ -644,36 +770,28 @@ function ie_removeCond(secId, secPath, idx, groupPath, childIdx) { newGroup[mode] = children; } var newCond = ie_condReplace(cond, groupPath, newGroup); - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (newCond) ed[secPath][idx].condition = newCond; - else delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, newCond); } else { var nc = ie_condReplace(cond, groupPath, null); - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (nc) ed[secPath][idx].condition = nc; - else delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, nc); } ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_removeGroup(secId, secPath, idx, groupPath) { +function ie_removeGroup(secId, secPath, idx, kind, groupPath) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - if (!ed[secPath] || !ed[secPath][idx]) return; - var cond = ed[secPath][idx].condition; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); - // Root group: removing the entire condition. if (!groupPath) { - delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, null); ced_syncDOMToData(ed); - window._ieRenderOnly(); + window._ieRenderOnly(); return; } - // Non-root group: gpath is the group's own path. Parent path = gpath minus - // last segment; childIdx = last segment. var parts = groupPath.split('-').map(Number); var childIdx = parts[parts.length - 1]; var parentPath = parts.slice(0, -1).join('-'); @@ -696,41 +814,41 @@ function ie_removeGroup(secId, secPath, idx, groupPath) { } var newCond = ie_condReplace(cond, parentPath, newParent); - if (newCond) ed[secPath][idx].condition = newCond; - else delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, newCond); ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_addRootConditionGroup(secPath, idx) { +function ie_addRootConditionGroup(secPath, idx, kind) { var ed = window._editorData; if (!ed) return; if (!ed[secPath]) ed[secPath] = []; if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (!ed[secPath][idx].condition || typeof ed[secPath][idx].condition !== 'object') { - ed[secPath][idx].condition = {all_of: []}; + var si = _ie_kindStep(kind); + if (si < 0) { + if (!ed[secPath][idx].condition || typeof ed[secPath][idx].condition !== 'object') { + ed[secPath][idx].condition = {all_of: []}; + } + } else { + if (!ed[secPath][idx].steps) ed[secPath][idx].steps = []; + if (!ed[secPath][idx].steps[si]) ed[secPath][idx].steps[si] = {}; + if (!ed[secPath][idx].steps[si].condition || typeof ed[secPath][idx].steps[si].condition !== 'object') { + ed[secPath][idx].steps[si].condition = {all_of: []}; + } } ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_addEffectToInteraction(secPath, idx) { - var ed = window._editorData; - if (!ed) return; - if (!ed[secPath]) ed[secPath] = []; - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (!ed[secPath][idx].action || typeof ed[secPath][idx].action !== 'object') { - ed[secPath][idx].action = {}; - } - ced_syncDOMToData(ed); - window._ieRenderOnly(); +function ie_addStepCond(secId, secPath, idx, si) { + ie_addRootConditionGroup(secPath, idx, 'step-' + si); } -function ie_addGroup(secId, secPath, idx, groupPath) { +function ie_addGroup(secId, secPath, idx, kind, groupPath) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - var cond = (ed[secPath] && ed[secPath][idx]) ? ed[secPath][idx].condition : null; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); var group = ie_condAt(cond, groupPath); var newSub = {all_of: []}; @@ -749,17 +867,16 @@ function ie_addGroup(secId, secPath, idx, groupPath) { } var newCond = ie_condReplace(cond, groupPath, newGroup); - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - ed[secPath][idx].condition = newCond; + _ie_setCond(ed, secPath, idx, kind, newCond); ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_toggleCondMode(secId, secPath, idx, groupPath) { +function ie_toggleCondMode(secId, secPath, idx, kind, groupPath) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - var cond = (ed[secPath] && ed[secPath][idx]) ? ed[secPath][idx].condition : null; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); var group = ie_condAt(cond, groupPath); if (!group || !group.all_of && !group.any_of) return; @@ -770,291 +887,257 @@ function ie_toggleCondMode(secId, secPath, idx, groupPath) { newGroup[oldMode === 'all_of' ? 'any_of' : 'all_of'] = children; var newCond = ie_condReplace(cond, groupPath, newGroup); - ed[secPath][idx].condition = newCond; + _ie_setCond(ed, secPath, idx, kind, newCond); ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_toggleCondNot(secId, secPath, idx, groupPath) { +function ie_toggleCondNot(secId, secPath, idx, kind, groupPath) { + ced_syncDOMToData(window._editorData); + window._ieRenderOnly(); +} + +function ie_toggleNotLeaf(secId, secPath, idx, kind, groupPath, ci) { + ced_syncDOMToData(window._editorData); + window._ieRenderOnly(); +} + +// ── Add/Remove: Steps ── + +function ie_removeStep(secId, secPath, idx, si) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); ced_syncDOMToData(ed); - window._ieRenderOnly(); + if (window._ieSyncAll) window._ieSyncAll(ed); + if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps) return; + ed[secPath][idx].steps.splice(si, 1); + window._ieRenderCurrent(); } -function ie_toggleNotLeaf(secId, secPath, idx, groupPath, ci) { +function ie_moveStep(secId, secPath, idx, fromSi, toSi) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); ced_syncDOMToData(ed); + if (window._ieSyncAll) window._ieSyncAll(ed); + var steps = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps; + if (!steps || fromSi < 0 || fromSi >= steps.length || toSi < 0 || toSi >= steps.length) return; + var m = steps.splice(fromSi, 1)[0]; + steps.splice(toSi, 0, m); + window._ieRenderCurrent(); +} + +function ie_moveRow(secPath, fromIdx, toIdx) { + var ed = window._editorData; + if (!ed) return; + ced_syncDOMToData(ed); + if (window._ieSyncAll) window._ieSyncAll(ed); + var arr = ed[secPath]; + if (!arr || fromIdx < 0 || fromIdx >= arr.length || toIdx < 0 || toIdx >= arr.length) return; + var m = arr.splice(fromIdx, 1)[0]; + arr.splice(toIdx, 0, m); + window._ieRenderCurrent(); +} + +function ie_moveMessage(secId, secPath, idx, si, fromMi, toMi) { + var ed = window._editorData; + if (!ed) return; + ced_syncDOMToData(ed); + ie_syncStep(ed, secId, secPath, idx, si); + var step = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps && ed[secPath][idx].steps[si]; + if (!step || !Array.isArray(step.messages)) return; + if (fromMi < 0 || fromMi >= step.messages.length || toMi < 0 || toMi >= step.messages.length) return; + var m = step.messages.splice(fromMi, 1)[0]; + step.messages.splice(toMi, 0, m); window._ieRenderOnly(); } -// ── Add/Remove: Action ── +// ── Add/Remove: Action effects ── -function ie_addActEffect(secId, secPath, idx, effectKey, scope) { +function ie_addActEffect(secId, secPath, idx, si, effectKey) { var ed = window._editorData; if (!ed) return; - ie_syncActEntry(ed, secId, secPath, idx); + ie_syncStep(ed, secId, secPath, idx, si); if (!ed[secPath]) ed[secPath] = []; if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (!ed[secPath][idx].action) ed[secPath][idx].action = {}; - - var act = ed[secPath][idx].action; - if (effectKey === 'messages') { - act.messages = []; + if (!ed[secPath][idx].steps) ed[secPath][idx].steps = []; + if (!ed[secPath][idx].steps[si]) ed[secPath][idx].steps[si] = {}; + var step = ed[secPath][idx].steps[si]; + + var at = IE_ACT_TYPES.find(function(a) { return a.key === effectKey; }); + if (!at) return; + if (at.kind === 'kv') { + step[effectKey] = {}; + } else if (at.kind === 'checkbox') { + step[effectKey] = true; + } else if (at.kind === 'number') { + step[effectKey] = 0; } else { - var at = IE_ACT_TYPES.find(function(a) { return a.key === effectKey; }); - var at2 = IE_ACT_TYPES_SEQ.find(function(a) { return a.key === effectKey; }); - var info = at || at2; - if (!info) return; - if (info.kind === 'kv') { - act[effectKey] = {}; - } else if (info.kind === 'checkbox') { - act[effectKey] = true; - } else if (info.kind === 'number') { - act[effectKey] = 0; - } else { - act[effectKey] = ''; - } + step[effectKey] = ''; } ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_removeActEffect(secId, secPath, idx, effectKey) { +function ie_removeActEffect(secId, secPath, idx, si, effectKey) { var ed = window._editorData; if (!ed) return; - ie_syncActEntry(ed, secId, secPath, idx); - if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].action) return; - delete ed[secPath][idx].action[effectKey]; - if (Object.keys(ed[secPath][idx].action).length === 0) { - delete ed[secPath][idx].action; - } + ie_syncStep(ed, secId, secPath, idx, si); + var step = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps && ed[secPath][idx].steps[si]; + if (!step) return; + delete step[effectKey]; ced_syncDOMToData(ed); window._ieRenderOnly(); } -// ── Add/Remove: KV action row ── -// -// KV rows are added/removed through these handlers rather than inline DOM -// manipulation so removal can drop the whole effect when the last row goes. -// "+ Add" appends a blank row to the DOM (no data change yet — it joins the -// map on the next sync, the same way typing into an existing row does). -// Removal deletes the row's key from the data map; if the map empties out the -// whole action key is dropped so the section "disappears when all flags are -// removed" (per the spec) and re-renders with a single starter row only when -// the user clicks the addbar "+ Set ... Flags" again. -function ie_addKVRow(secId, secPath, idx, effectKey) { +// ── Add/Remove: KV row ── + +function ie_addKVRow(secId, secPath, idx, si, effectKey) { var ed = window._editorData; if (!ed) return; - // Sync first so any in-progress typing in the existing rows is captured. - ie_syncActEntry(ed, secId, secPath, idx); - // Append a blank row directly to the DOM (no data change yet). + ie_syncStep(ed, secId, secPath, idx, si); var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - var row = rows[idx]; - if (!row) return; - var list = row.querySelector('.ie-act-kv[data-ie-act-key="' + effectKey + '"] .ie-kv-list'); + var stepRow = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); + if (!stepRow) return; + var list = stepRow.querySelector('.ie-act-kv[data-ie-act-key="' + effectKey + '"] .ie-kv-list'); if (!list) return; var rowEl = document.createElement('div'); rowEl.className = 'ie-kv-row'; - rowEl.innerHTML = ''; + rowEl.innerHTML = ''; list.appendChild(rowEl); rowEl.querySelector('.ie-kv-key').focus(); } -function ie_removeKVRow(rowEl, secId, secPath, idx, effectKey) { +function ie_removeKVRow(rowEl, secId, secPath, idx, si, effectKey) { var ed = window._editorData; if (!ed) return; - // rowEl may be the X button itself (callers use `this` in inline onclick) — - // walk up to the actual .ie-kv-row so the key input below is found. if (rowEl && rowEl.closest) { var kvRow = rowEl.closest('.ie-kv-row'); if (kvRow) rowEl = kvRow; } - ie_syncActEntry(ed, secId, secPath, idx); - if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].action) return; - var act = ed[secPath][idx].action; - if (!act.hasOwnProperty(effectKey)) return; + ie_syncStep(ed, secId, secPath, idx, si); + var step = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps && ed[secPath][idx].steps[si]; + if (!step || !step.hasOwnProperty(effectKey)) return; var keyEl = rowEl.querySelector('.ie-kv-key'); var k = keyEl ? keyEl.value.trim() : ''; - if (k && act[effectKey].hasOwnProperty(k)) delete act[effectKey][k]; - // Empty-key rows are not in the data map at all; nothing to delete there. - if (Object.keys(act[effectKey]).length === 0) { - delete act[effectKey]; - if (Object.keys(act).length === 0) delete ed[secPath][idx].action; + if (k && step[effectKey].hasOwnProperty(k)) delete step[effectKey][k]; + if (Object.keys(step[effectKey]).length === 0) { + delete step[effectKey]; } ced_syncDOMToData(ed); window._ieRenderOnly(); } -// ── Add/Remove: Messages list row ── +// ── Add/Remove: Messages ── + +function ie_addMessages(secId, secPath, idx, si) { + var ed = window._editorData; + if (!ed) return; + ie_syncStep(ed, secId, secPath, idx, si); + if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return; + ed[secPath][idx].steps[si].messages = ['']; + ced_syncDOMToData(ed); + window._ieRenderOnly(); +} -// ie_addMessage appends a blank delay+message row to the DOM (no data change -// yet — it joins the list on the next sync, the same way typing into an -// existing row does). The Messages section must already be present (created -// via + Add Message in the addbar). Focuses the new row's message input. -function ie_addMessage(secId, secPath, idx) { +function ie_addMessage(secId, secPath, idx, si) { var ed = window._editorData; if (!ed) return; - ie_syncActEntry(ed, secId, secPath, idx); + ie_syncStep(ed, secId, secPath, idx, si); var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - var row = rows[idx]; - if (!row) return; - var list = row.querySelector('.ie-act-kv[data-ie-act-key="messages"] .ie-kv-list'); + var stepRow = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); + if (!stepRow) return; + var list = stepRow.querySelector('.ie-act-kv[data-ie-act-key="messages"] .ie-kv-list'); if (!list) return; var rowEl = document.createElement('div'); - rowEl.className = 'ie-kv-row'; - var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ')'; - rowEl.innerHTML = ''; + rowEl.className = 'ie-kv-row ie-msg-row'; + var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')'; + rowEl.innerHTML = ''; list.appendChild(rowEl); rowEl.querySelector('.ie-act-msg').focus(); } -// ie_removeMessage removes a message entry from the action's messages list. If -// the list becomes empty the entire messages key is dropped (and the section -// disappears on re-render). rowEl may be the X button itself. -function ie_removeMessage(rowEl, secId, secPath, idx) { +function ie_removeMessage(rowEl, secId, secPath, idx, si) { var ed = window._editorData; if (!ed) return; if (rowEl && rowEl.closest) { var kvRow = rowEl.closest('.ie-kv-row'); if (kvRow) rowEl = kvRow; } - ie_syncActEntry(ed, secId, secPath, idx); - if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].action) return; - var act = ed[secPath][idx].action; - if (!act.hasOwnProperty('messages') || !Array.isArray(act.messages)) return; + ie_syncStep(ed, secId, secPath, idx, si); + var step = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps && ed[secPath][idx].steps[si]; + if (!step || !Array.isArray(step.messages)) return; - // Find which row index to remove by locating our row in the list. var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); - var subRows = card ? card.querySelectorAll('.obj-sub-row') : []; - var row = subRows[idx]; - if (row) { - var kvRows = row.querySelectorAll('.ie-act-kv[data-ie-act-key="messages"] .ie-kv-row'); + var stepRow = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); + if (stepRow) { + var kvRows = stepRow.querySelectorAll('.ie-act-kv[data-ie-act-key="messages"] .ie-kv-row'); for (var ri = 0; ri < kvRows.length; ri++) { if (kvRows[ri] === rowEl || kvRows[ri].contains(rowEl)) { - act.messages.splice(ri, 1); + step.messages.splice(ri, 1); break; } } } - if (act.messages.length === 0) { - delete act.messages; - if (Object.keys(act).length === 0) delete ed[secPath][idx].action; + if (step.messages.length === 0) { + delete step.messages; } ced_syncDOMToData(ed); window._ieRenderOnly(); } -// ── Pre-render sync helpers ── +// ── Shared trigger-style array-section renderer ── -function ie_syncCondEntry(ed, secId, secPath, idx) { - var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); - if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - var row = rows[idx]; - if (!row) return; - var condRoot = row.querySelector('.ie-cond-root'); - if (!condRoot) return; - var cond = ie_collectCond(condRoot); - if (!ed[secPath]) ed[secPath] = []; - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (cond) ed[secPath][idx].condition = cond; - else delete ed[secPath][idx].condition; -} - -function ie_syncActEntry(ed, secId, secPath, idx) { - var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); - if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - var row = rows[idx]; - if (!row) return; - var actRoot = row.querySelector('.ie-act-root'); - if (!actRoot) return; - var act = ie_collectAct(actRoot); - if (!ed[secPath]) ed[secPath] = []; - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (act) { - // Preserve any action keys that aren't rendered in the DOM (e.g. legacy - // seq-only effects on an inline section, or unknown future keys). Without - // this, the first sync after any edit would silently drop them. - var existing = (ed[secPath][idx].action && typeof ed[secPath][idx].action === 'object') ? ed[secPath][idx].action : null; - if (existing) { - Object.keys(existing).forEach(function(k) { - if (!Object.prototype.hasOwnProperty.call(act, k)) act[k] = existing[k]; - }); - } - ed[secPath][idx].action = act; - } else { - delete ed[secPath][idx].action; - } -} - -// ── Shared interaction-style array-section renderer ── -// -// Replaces the duplicated `renderArraySection` (objecteditor.js) and -// `renderMobArraySection` (mobeditor.js). -// -// ctx: { -// sec: section def (.id, .path, .isArray, .interactionStyle, .interScope, .fields, .label), -// data: editor's data root, -// visMap: per-editor vis object (e.g. window._interVis[sec.id]), -// showRemove(idx) => onclick string for "Remove Interaction", -// showField(secId, idx, key) => onclick string for "+ Add Required Item/...", -// hideField(secId, idx, key) => onclick string for the panel "X", -// renderField(sec, f, data, idx, optStyle) => HTML for the item_id field, -// onAdd() => onclick string for the bottom "+ Add ...", -// } function ie_renderArraySection(ctx) { var sec = ctx.sec; var data = ctx.data; var visMap = ctx.visMap; var arr = data[sec.path] || []; var h = ''; + var allowItem = sec.itemIDAllowed !== false; arr.forEach(function(item, idx) { visMap[idx] = visMap[idx] || {}; var vis = visMap[idx]; - var showItem = vis.item_id || (item.item_id && item.item_id !== ''); + var showItem = allowItem && (vis.item_id || (item.item_id && item.item_id !== '')); var condExists = !!(item.condition && typeof item.condition === 'object'); - var actExists = !!(item.action && typeof item.action === 'object'); + var lockVal = !!item.lock; + var steps = item.steps || []; h += '
'; h += '
'; - if (!showItem) { - h += ''; - } else { - h += ctx.renderField(sec, sec.fields[0], data, idx, 'flex:1;min-width:0'); - h += ''; + h += ''; + if (idx > 0) h += ''; + if (idx < arr.length - 1) h += ''; + + if (allowItem) { + if (!showItem) { + h += ''; + } else { + h += ctx.renderField(sec, sec.fields[0], data, idx, 'flex:1;min-width:0'); + h += ''; + } } if (!condExists) { - h += ''; - } - if (!actExists) { - h += ''; + h += ''; } h += ''; - h += ''; + h += ''; h += '
'; h += '
'; - h += '
' + ie_renderCond(item.condition || null, sec.id, idx, sec.path) + '
'; + h += '
' + ie_renderCond(item.condition || null, sec.id, idx, sec.path, 'entry') + '
'; h += '
'; - h += '
'; - h += '
' + ie_renderAct(item.action || null, sec.interScope || 'inline', sec.id, idx, sec.path) + '
'; + h += '
'; + h += ie_renderStepList(steps, sec.id, idx, sec.path); h += '
'; h += '
'; }); - h += ''; + h += ''; return h; } @@ -1072,12 +1155,37 @@ function ie_hideField(visMap, secId, idx, key) { visMap[secId][idx][key] = false; } +// ── Step-cleaning helper used by save paths ── + +function ie_cleanStep(step) { + var s = {}; + if (step && step.condition) { + var c = ie_cleanCondition(step.condition); + if (c) s.condition = c; + } + if (step && step.wait !== undefined && step.wait !== null && step.wait !== 0) s.wait = step.wait; + if (step && Array.isArray(step.messages) && step.messages.filter(function(m){return m;}).length > 0) { + s.messages = step.messages.filter(function(m){return m;}); + } + IE_ACT_TYPES.forEach(function(at) { + if (at.key === 'wait' || at.key === 'messages') return; + if (!step || !Object.prototype.hasOwnProperty.call(step, at.key)) return; + var v = step[at.key]; + if (v === undefined || v === null || v === '') return; + if (typeof v === 'object') { + if (Object.keys(v).length === 0) return; + s[at.key] = v; + } else if (typeof v === 'boolean') { + if (!v) return; + s[at.key] = v; + } else { + s[at.key] = v; + } + }); + return s; +} + // ── Bindings (set by each editor) ── -// _ieRenderCurrent: full sync + render. Used by chrome toggles -// (show/hide panels, add row, remove row, add section, etc.). -// _ieRenderOnly: render only, no sync. Used by ie_* mutation handlers -// that have already synced + mutated the data and must not re-read the -// stale DOM before the next render. window._ieRenderCurrent = function() {}; -window._ieRenderOnly = function() {}; +window._ieRenderOnly = function() {}; \ No newline at end of file -- cgit v1.2.3