diff options
| author | historia <[not public]> | 2026-07-09 04:03:00 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-09 04:03:00 -0400 |
| commit | 74153c7814fc4cef988066ef04e38731a943bc12 (patch) | |
| tree | debd8229a1fd02daeb3059e9396008bb525ba4b9 /internal/admin/static/intereditor.js | |
| parent | 09d325dcf5e779eab5550d3fd3377bde50101428 (diff) | |
| download | thehouseoficarus-74153c7814fc4cef988066ef04e38731a943bc12.tar.gz | |
feat(admin): nested condition/action buttons to manage complex YAML
Diffstat (limited to 'internal/admin/static/intereditor.js')
| -rw-r--r-- | internal/admin/static/intereditor.js | 1050 |
1 files changed, 1050 insertions, 0 deletions
diff --git a/internal/admin/static/intereditor.js b/internal/admin/static/intereditor.js new file mode 100644 index 0000000..4e48df1 --- /dev/null +++ b/internal/admin/static/intereditor.js @@ -0,0 +1,1050 @@ +// 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 += '<div class="ie-cond-root">'; + h += ie_renderGroup(condObj, secId, secPath, idx, ''); + h += '</div>'; + } + // 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 += '<div class="ie-cond-addbar">'; + IE_COND_TYPES.forEach(function(ct) { + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; + }); + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'\')">+ Group</button>'; + h += '</div>'; + } + 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 += '<div class="ie-cond ie-cond-empty" data-ie-gpath="' + escAttr(gpath) + '">'; + h += '<div class="ie-cond-modebar ie-cond-empty-bar">'; + IE_COND_TYPES.forEach(function(ct) { + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; + }); + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')">+ Group</button>'; + h += '<button class="btn btn-sm btn-danger ie-cond-grp-rm" onclick="ie_removeGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')" title="Remove this group">X</button>'; + h += '</div>'; + h += '</div>'; + return h; + } + + // Non-empty group: modebar (no X) + children + addbar below. + h += '<div class="ie-cond" data-ie-gpath="' + escAttr(gpath) + '">'; + h += '<div class="ie-cond-modebar">'; + if (children.length >= 2) { + h += '<select class="ie-cond-modesel" onchange="ie_toggleCondMode(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')">'; + h += '<option value="all_of"' + (mode === 'all_of' ? ' selected' : '') + '>All of</option>'; + h += '<option value="any_of"' + (mode === 'any_of' ? ' selected' : '') + '>Any of</option>'; + h += '</select>'; + } + if (children.length >= 2 || not) { + h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleCondNot(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')"' + (not ? ' checked' : '') + '> NOT</label>'; + } + h += '</div>'; + h += '<div class="ie-cond-children">'; + 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 += '</div>'; + h += '<div class="ie-cond-addbar" data-ie-gpath="' + escAttr(gpath) + '">'; + IE_COND_TYPES.forEach(function(ct) { + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; + }); + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')">+ Group</button>'; + h += '</div>'; + h += '</div>'; + 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 = '<div class="ie-cond-leaf" data-ie-leaftype="' + leafType + '">'; + h += '<span class="ie-cond-leaf-label">' + esc(label) + '</span>'; + if (isSearch) { + h += '<div style="position:relative;min-width:0;flex:1">'; + h += '<input class="ie-cond-val search-input" data-search-type="items" placeholder="' + escAttr(hint) + '" value="' + escAttr(sv) + '">'; + h += '<div class="search-results" style="display:none"></div>'; + h += '</div>'; + } else if (leafType === 'min_credits') { + h += '<input class="ie-cond-val" placeholder="' + escAttr(hint) + '" value="' + escAttr(sv) + '" type="number">'; + } else { + h += '<input class="ie-cond-val" placeholder="' + escAttr(hint) + '" value="' + escAttr(sv) + '">'; + } + if (leafType === 'player_flag' || leafType === 'global_flag') { + h += '<input class="ie-cond-subval" placeholder="Value (optional)" value="' + escAttr(ssv) + '">'; + } + h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleNotLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\',' + ciToken + ')"' + (not ? ' checked' : '') + '> NOT</label>'; + h += '<button class="btn btn-sm btn-danger ie-cond-rm" onclick="ie_removeCond(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\',' + ciToken + ')" title="Remove">X</button>'; + h += '</div>'; + 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 += '<div class="ie-act-row"><span class="ie-act-label">Message</span>'; + rows += '<input class="ie-act-input ie-act-msg" data-ie-act-key="message" placeholder="Shown to player" value="' + escAttr(msg) + '" style="flex:1">'; + rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'message\')">X</button>'; + rows += '</div>'; + } + + // 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 += '<div class="ie-act-kv" data-ie-act-key="' + escAttr(at.key) + '">'; + rows += '<div class="ie-act-kv-header"><span>' + esc(at.label) + '</span></div>'; + rows += '<div class="ie-kv-list">'; + 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 += '<div class="ie-kv-row"><input value="" placeholder="key" class="ie-kv-key">'; + rows += '<input value="" placeholder="val" class="ie-kv-val">'; + rows += '<button class="btn btn-sm btn-danger" onclick="' + kvRmAttr + '">X</button></div>'; + } else { + kvKeys.forEach(function(k) { + rows += '<div class="ie-kv-row"><input value="' + escAttr(k) + '" placeholder="key" class="ie-kv-key">'; + rows += '<input value="' + escAttr(String(kvMap[k])) + '" placeholder="val" class="ie-kv-val">'; + rows += '<button class="btn btn-sm btn-danger" onclick="' + kvRmAttr + '">X</button></div>'; + }); + } + rows += '</div>'; + rows += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addKVRow(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">+ Add</button>'; + rows += '</div>'; + } else if (at.kind === 'checkbox') { + rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; + rows += '<label><input type="checkbox" class="ie-act-input" data-ie-act-key="' + escAttr(at.key) + '" data-ie-act-kind="checkbox" checked> Enabled</label>'; + rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>'; + rows += '</div>'; + } else if (at.kind === 'search') { + rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; + rows += '<div style="position:relative;min-width:0;flex:1"><input class="ie-act-input search-input" data-ie-act-key="' + escAttr(at.key) + '" data-search-type="' + escAttr(at.entity) + '" placeholder="' + escAttr(at.hint || 'Search ' + at.entity + '...') + '" value="' + escAttr(String(val)) + '"><div class="search-results" style="display:none"></div></div>'; + rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>'; + rows += '</div>'; + } else { + rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; + rows += '<input class="ie-act-input" data-ie-act-key="' + escAttr(at.key) + '"' + (at.kind === 'number' ? ' type="number"' : '') + ' data-ie-act-kind="' + escAttr(at.kind) + '" value="' + escAttr(String(val)) + '" placeholder="' + escAttr(at.hint || '') + '" style="flex:1">'; + rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>'; + rows += '</div>'; + } + }); + + // 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 += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; + rows += '<div style="position:relative;min-width:0;flex:1"><input class="ie-act-input search-input" data-ie-act-key="' + escAttr(at.key) + '" data-search-type="' + escAttr(at.entity) + '" placeholder="' + escAttr(at.hint || 'Search ' + at.entity + '...') + '" value="' + escAttr(String(val)) + '"><div class="search-results" style="display:none"></div></div>'; + rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>'; + rows += '</div>'; + } else { + rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; + rows += '<input class="ie-act-input" data-ie-act-key="' + escAttr(at.key) + '" value="' + escAttr(String(val)) + '" placeholder="' + escAttr(at.hint || '') + '" style="flex:1">'; + rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>'; + rows += '</div>'; + } + }); + } + + // Add effect buttons + var addbar = '<div class="ie-act-addbar">'; + if (!msgPresent) { + addbar += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'message\',\'' + escAttr(scope) + '\')">+ Add Message</button>'; + } + 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 += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\',\'' + escAttr(scope) + '\')">+ ' + esc(at.label) + '</button>'; + }); + if (scope === 'seq') { + IE_ACT_TYPES_SEQ.forEach(function(at) { + if (actionObj && Object.prototype.hasOwnProperty.call(actionObj, at.key)) return; + addbar += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\',\'' + escAttr(scope) + '\')">+ ' + esc(at.label) + '</button>'; + }); + } + addbar += '</div>'; + + // 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 '<div class="ie-act-root">' + rows + addbar + '</div>'; + } + 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 = '<input value="" placeholder="key" class="ie-kv-key"><input value="" placeholder="val" class="ie-kv-val"><button class="btn btn-sm btn-danger" onclick="ie_removeKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(effectKey) + '\')">X</button>'; + 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; +} + +// ── Upgrade: migrate legacy message/condition/action on load ── + +function ie_upgradeItem(item) { + if (!item) return; + if (item.message && typeof item.message === 'string' && item.message.trim()) { + if (!item.action || typeof item.action !== 'object') item.action = {}; + if (!item.action.message) item.action.message = item.message; + delete item.message; + } + if (item.condition && typeof item.condition === 'string') { + try { item.condition = JSON.parse(item.condition); } catch(e) { delete item.condition; } + } + if (item.action && typeof item.action === 'string') { + try { item.action = JSON.parse(item.action); } catch(e) { delete item.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 = ''; + + arr.forEach(function(item, idx) { + ie_upgradeItem(item); + + 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 += '<div class="obj-sub-row ie-inter-row" data-idx="' + idx + '">'; + h += '<div class="ie-inter-header">'; + if (!showItem) { + h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.showField(sec.id, idx, 'item_id') + '">+ Add Required Item</button>'; + } else { + h += ctx.renderField(sec, sec.fields[0], data, idx, 'flex:1;min-width:0'); + h += '<button class="btn btn-sm btn-danger ie-inter-field-x" onclick="' + ctx.hideField(sec.id, idx, 'item_id') + '" title="Remove required item">X</button>'; + } + if (!condExists) { + h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.addCondition(sec.id, idx) + '">+ Add Condition</button>'; + } + if (!actExists) { + h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.addEffect(sec.id, idx) + '">+ Add Action</button>'; + } + h += '<span class="ie-inter-header-spacer"></span>'; + h += '<button class="btn btn-sm btn-danger ie-inter-remove" onclick="' + ctx.showRemove(idx) + '">Remove Interaction</button>'; + h += '</div>'; + + h += '<div class="ie-inter-panel" style="display:' + (condExists ? 'block' : 'none') + '">'; + h += '<div class="ie-inter-panel-body">' + ie_renderCond(item.condition || null, sec.id, idx, sec.path) + '</div>'; + h += '</div>'; + + h += '<div class="ie-inter-panel" style="display:' + (actExists ? 'block' : 'none') + '">'; + h += '<div class="ie-inter-panel-body">' + ie_renderAct(item.action || null, sec.interScope || 'inline', sec.id, idx, sec.path) + '</div>'; + h += '</div>'; + + h += '</div>'; + }); + + h += '<button class="btn btn-sm obj-sub-add" onclick="' + ctx.onAdd() + '">+ Add ' + esc(sec.label) + '</button>'; + 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() {}; |
