// 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 += '
';
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 += '';
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() {};