// 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. // ── Condition leaf types ── var IE_COND_TYPES = [ {type: 'global_flag', label: 'Global Flag', hint: 'world flag name'}, {type: 'player_flag', label: 'Player Flag', hint: 'player flag name'}, {type: 'has_item', label: 'Has Item', hint: 'item ID'}, {type: 'min_credits', label: 'Min Credits', hint: 'amount'}, ]; 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'}, ]; // 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'}, ]; // Sentinel child-index for a root-level single leaf (no parent group). var IE_ROOT_LEAF = -1; // ── Navigation in condition tree ── function ie_condAt(cond, path) { if (!path) return cond; var parts = path.split('-').map(Number); var cur = cond; for (var i = 0; i < parts.length; i++) { if (!cur || (!cur.all_of && !cur.any_of)) return null; var children = cur.all_of || cur.any_of || []; if (parts[i] >= children.length) return null; cur = children[parts[i]]; } 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; var parts = path.split('-').map(Number); function replace(cur, depth) { if (depth === parts.length) return newVal; if (!cur || (!cur.all_of && !cur.any_of)) return cur; var mode = cur.all_of ? 'all_of' : 'any_of'; var children = (cur.all_of || cur.any_of).slice(); children[parts[depth]] = replace(children[parts[depth]], depth + 1); children = children.filter(function(c) { return c !== null; }); if (children.length === 0) return null; var r = {}; if (cur.not) r.not = true; r[mode] = children; return r; } return replace(cond, 0); } // ── 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) { var mode = cond.all_of ? 'all_of' : 'any_of'; var cleaned = cond[mode].map(ie_cleanCondition).filter(function(c) { return c !== null; }); if (cleaned.length === 0) return null; if (cleaned.length === 1 && !cond.not) return cleaned[0]; var r = {}; if (cond.not) r.not = true; r[mode] = cleaned; return r; } // Leaf var not = !!cond.not; var leaf = null; if (cond.global_flag !== undefined) { if (cond.global_flag) { leaf = { global_flag: cond.global_flag }; if (cond.value !== undefined) leaf.value = cond.value; } } else if (cond.player_flag !== undefined) { if (cond.player_flag) { leaf = { player_flag: cond.player_flag }; if (cond.value !== undefined) leaf.value = cond.value; } } else if (cond.has_item !== undefined) { if (cond.has_item) leaf = { has_item: cond.has_item }; } else if (cond.min_credits !== undefined) { var mc = parseInt(cond.min_credits, 10); if (!isNaN(mc)) leaf = { min_credits: mc }; } if (!leaf) return null; if (not) leaf.not = true; return leaf; } // ── Collect condition from DOM ── 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); var leafEl = rootEl.querySelector(':scope > .ie-cond-leaf'); if (leafEl) return ie_collectLeaf(leafEl); 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; var notCb = rootEl.querySelector(':scope > .ie-cond-modebar .ie-cond-not'); var not = notCb ? notCb.checked : false; var container = rootEl.querySelector(':scope > .ie-cond-children'); var items = []; if (container) { Array.prototype.forEach.call(container.children, function(child) { if (child.classList.contains('ie-cond-leaf')) { var leaf = ie_collectLeaf(child); if (leaf) items.push(leaf); } else if (child.classList.contains('ie-cond')) { var sub = ie_collectCond(child); if (sub) items.push(sub); } }); } // 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; return result; } function ie_collectLeaf(el) { var leafType = el.getAttribute('data-ie-leaftype'); if (!leafType) return null; var result = {}; var notCb = el.querySelector('.ie-cond-not'); if (notCb && notCb.checked) result.not = true; 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; var gfSubInput = el.querySelector('.ie-cond-subval'); if (gfSubInput && gfSubInput.value.trim()) { var gfsv = gfSubInput.value.trim(); try { result.value = JSON.parse(gfsv); } catch(e) { result.value = gfsv; } } break; case 'player_flag': result.player_flag = val; var subInput = el.querySelector('.ie-cond-subval'); if (subInput && subInput.value.trim()) { var sv = subInput.value.trim(); try { result.value = JSON.parse(sv); } catch(e) { result.value = sv; } } break; case 'has_item': result.has_item = val; break; case 'min_credits': result.min_credits = parseInt(val) || 0; break; default: return null; } return result; } // ── Collect action from DOM ── function ie_collectAct(rootEl) { if (!rootEl) return null; var r = {}; // Message (rolled into action) var msgEl = rootEl.querySelector('.ie-act-msg'); if (msgEl) { var msg = msgEl.value.trim(); r.message = msg; } // 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; 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; // Clear any legacy top-level message on sync. delete data[p][idx].message; } }); }); } // ── Render condition ── function ie_renderCond(condObj, secId, idx, secPath) { 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 += '
'; } // 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 += '
'; } return h; } function ie_renderGroup(cond, secId, secPath, idx, gpath) { 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; } var not = !!cond.not; if (mode && children) { return _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath); } // 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); return ''; } function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath) { 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 += '
'; return h; } // Non-empty group: modebar (no X) + children + addbar below. h += '
'; h += '
'; if (children.length >= 2) { h += ''; } if (children.length >= 2 || not) { 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); } 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.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); } }); h += '
'; h += '
'; IE_COND_TYPES.forEach(function(ct) { h += ''; }); h += ''; h += '
'; h += '
'; return h; } function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gpath, ci) { var label = ''; IE_COND_TYPES.forEach(function(ct) { if (ct.type === leafType) label = ct.label; }); var hint = ''; IE_COND_TYPES.forEach(function(ct) { if (ct.type === leafType) hint = ct.hint; }); var isSearch = leafType === 'has_item'; var sv = leafVal === undefined || leafVal === null ? '' : String(leafVal); var ssv = leafSub === undefined || leafSub === null ? '' : String(leafSub); var ciToken = (ci === undefined || ci === null) ? IE_ROOT_LEAF : ci; var h = '
'; h += '' + esc(label) + ''; if (isSearch) { h += '
'; h += ''; h += ''; h += '
'; } else if (leafType === 'min_credits') { h += ''; } else { h += ''; } if (leafType === 'player_flag' || leafType === 'global_flag') { h += ''; } h += ''; h += ''; h += '
'; return h; } // ── Render action ── function ie_renderAct(actionObj, scope, secId, idx, secPath) { var hasAny = false; var rows = ''; // Message var msg = (actionObj && actionObj.message !== undefined && actionObj.message !== null) ? String(actionObj.message) : ''; var msgPresent = actionObj && Object.prototype.hasOwnProperty.call(actionObj, 'message'); if (msgPresent) { hasAny = true; rows += '
Message'; 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 (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) + '\')'; 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 += '
'; } else { kvKeys.forEach(function(k) { rows += '
'; rows += ''; rows += '
'; }); } rows += '
'; rows += ''; rows += '
'; } else if (at.kind === 'checkbox') { rows += '
' + esc(at.label) + ''; rows += ''; rows += ''; rows += '
'; } else if (at.kind === 'search') { rows += '
' + esc(at.label) + ''; rows += '
'; rows += ''; rows += '
'; } else { rows += '
' + esc(at.label) + ''; 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 += '
'; } }); } // Add effect buttons var addbar = '
'; if (!msgPresent) { addbar += ''; } 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; } } addbar += ''; }); if (scope === 'seq') { IE_ACT_TYPES_SEQ.forEach(function(at) { if (actionObj && Object.prototype.hasOwnProperty.call(actionObj, at.key)) return; addbar += ''; }); } 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 + '
'; } return addbar; } // ── Add/Remove/Toggle: Condition ── function ie_addLeaf(secId, secPath, idx, 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; var group = ie_condAt(cond, groupPath); var leaf = {}; leaf[leafType] = leafType === 'min_credits' ? 0 : ''; var newGroup; if (!group) { newGroup = leaf; } else if (group.all_of || group.any_of) { var mode = group.all_of ? 'all_of' : 'any_of'; var children = (group.all_of || group.any_of).concat([leaf]); newGroup = {}; if (group.not) newGroup.not = true; newGroup[mode] = children; } else { newGroup = {all_of: [group, leaf]}; } 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; ced_syncDOMToData(ed); window._ieRenderOnly(); } function ie_removeCond(secId, secPath, idx, 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; // 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; ced_syncDOMToData(ed); window._ieRenderOnly(); return; } var group = ie_condAt(cond, groupPath); if (!group) return; if (group.all_of || group.any_of) { var mode = group.all_of ? 'all_of' : 'any_of'; var children = (group.all_of || group.any_of).slice(); children.splice(childIdx, 1); var newGroup; 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 = {}; if (group.not) newGroup.not = true; 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; } 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; } ced_syncDOMToData(ed); window._ieRenderOnly(); } function ie_removeGroup(secId, secPath, idx, 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; // Root group: removing the entire condition. if (!groupPath) { delete ed[secPath][idx].condition; ced_syncDOMToData(ed); 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('-'); var parent = ie_condAt(cond, parentPath); if (!parent || !(parent.all_of || parent.any_of)) return; var mode = parent.all_of ? 'all_of' : 'any_of'; var children = parent[mode].slice(); children.splice(childIdx, 1); var newParent; if (children.length === 0) { newParent = null; } else if (children.length === 1 && !parent.not) { newParent = children[0]; } else { newParent = {}; if (parent.not) newParent.not = true; newParent[mode] = children; } var newCond = ie_condReplace(cond, parentPath, newParent); if (newCond) ed[secPath][idx].condition = newCond; else delete ed[secPath][idx].condition; ced_syncDOMToData(ed); window._ieRenderOnly(); } function ie_addRootConditionGroup(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].condition || typeof ed[secPath][idx].condition !== 'object') { ed[secPath][idx].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_addGroup(secId, secPath, idx, 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; var group = ie_condAt(cond, groupPath); var newSub = {all_of: []}; var newGroup; if (!group) { newGroup = newSub; } else if (group.all_of || group.any_of) { var mode = group.all_of ? 'all_of' : 'any_of'; var children = (group.all_of || group.any_of).concat([newSub]); newGroup = {}; if (group.not) newGroup.not = true; newGroup[mode] = children; } else { newGroup = {all_of: [group, newSub]}; } var newCond = ie_condReplace(cond, groupPath, newGroup); if (!ed[secPath][idx]) ed[secPath][idx] = {}; ed[secPath][idx].condition = newCond; ced_syncDOMToData(ed); window._ieRenderOnly(); } function ie_toggleCondMode(secId, secPath, idx, 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; var group = ie_condAt(cond, groupPath); if (!group || !group.all_of && !group.any_of) return; var oldMode = group.all_of ? 'all_of' : 'any_of'; var children = group.all_of || group.any_of; var newGroup = {}; if (group.not) newGroup.not = true; newGroup[oldMode === 'all_of' ? 'any_of' : 'all_of'] = children; var newCond = ie_condReplace(cond, groupPath, newGroup); ed[secPath][idx].condition = newCond; ced_syncDOMToData(ed); window._ieRenderOnly(); } function ie_toggleCondNot(secId, secPath, idx, 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; var group = ie_condAt(cond, groupPath); if (!group) return; if (group.all_of || group.any_of) { group.not = !group.not; if (!group.not) delete group.not; var nc = ie_condReplace(cond, groupPath, group); if (!ed[secPath][idx]) ed[secPath][idx] = {}; if (nc) ed[secPath][idx].condition = nc; else delete ed[secPath][idx].condition; } ced_syncDOMToData(ed); window._ieRenderOnly(); } function ie_toggleNotLeaf(secId, secPath, idx, groupPath, ci) { 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; var parent = ie_condAt(cond, groupPath); if (!parent) return; var leaf; if (ci === IE_ROOT_LEAF) { leaf = parent; } else { if (!parent.all_of && !parent.any_of) { leaf = parent; } else { var children = parent.all_of || parent.any_of; if (typeof ci !== 'number' || ci < 0 || ci >= children.length) return; leaf = children[ci]; } } leaf.not = !leaf.not; if (!leaf.not) delete leaf.not; ed[secPath][idx].condition = cond; ced_syncDOMToData(ed); window._ieRenderOnly(); } // ── Add/Remove: Action ── function ie_addActEffect(secId, secPath, idx, effectKey, scope) { var ed = window._editorData; if (!ed) return; ie_syncActEntry(ed, secId, secPath, idx); 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 === 'message') { act.message = ''; } 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] = ''; } } ced_syncDOMToData(ed); window._ieRenderOnly(); } function ie_removeActEffect(secId, secPath, idx, 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; } 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) { 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). 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'); if (!list) return; var rowEl = document.createElement('div'); rowEl.className = 'ie-kv-row'; rowEl.innerHTML = ''; list.appendChild(rowEl); rowEl.querySelector('.ie-kv-key').focus(); } function ie_removeKVRow(rowEl, secId, secPath, idx, 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; 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; } ced_syncDOMToData(ed); window._ieRenderOnly(); } // ── Pre-render sync helpers ── 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; } // Clear any legacy top-level message on sync. delete ed[secPath][idx].message; } // ── 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 = ''; 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 condExists = !!(item.condition && typeof item.condition === 'object'); var actExists = !!(item.action && typeof item.action === 'object'); h += '
'; h += '
'; 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 += '
' + ie_renderCond(item.condition || null, sec.id, idx, sec.path) + '
'; h += '
'; h += '
'; h += '
' + ie_renderAct(item.action || null, sec.interScope || 'inline', sec.id, idx, sec.path) + '
'; h += '
'; h += '
'; }); h += ''; return h; } // ── Show/Hide field on a row ── function ie_showField(visMap, secId, idx, key) { if (!visMap[secId]) visMap[secId] = {}; if (!visMap[secId][idx]) visMap[secId][idx] = {}; visMap[secId][idx][key] = true; } function ie_hideField(visMap, secId, idx, key) { if (!visMap[secId]) visMap[secId] = {}; if (!visMap[secId][idx]) visMap[secId][idx] = {}; visMap[secId][idx][key] = false; } // ── 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() {};