// talktree.js — Conversation tree editor with unified condition & action // editors powered by intereditor.js. Tree navigation, messages, and options // (text / goto) stay here; condition and action editing delegates to // intereditor via talk-scoped contexts that target talk.nodes[key]... paths. (function() { 'use strict'; // ---- Path helpers ---- function talkRoot() { return window._talkData && window._talkData.talk; } function talkNodes() { var r = talkRoot(); return r && r.nodes; } function talkSync() { if (typeof window.onTalkTreeChange === 'function') window.onTalkTreeChange(window._talkData); } function talkGet(path) { var parts = path.split('.'), obj = talkRoot(); for (var i = 0; i < parts.length; i++) { if (obj == null) return undefined; var k = parts[i]; obj = /^\d+$/.test(k) ? obj[parseInt(k, 10)] : obj[k]; } return obj; } function talkSet(path, value) { var parts = path.split('.'), root = talkRoot(); for (var i = 0; i < parts.length - 1; i++) { var k = parts[i]; if (/^\d+$/.test(k)) { var idx = parseInt(k, 10); if (root[idx] == null) root[idx] = /^\d+$/.test(parts[i + 1]) ? [] : {}; root = root[idx]; } else { if (root[k] == null) root[k] = /^\d+$/.test(parts[i + 1]) ? [] : {}; root = root[k]; } } var lk = parts[parts.length - 1]; if (/^\d+$/.test(lk)) { root[parseInt(lk, 10)] = value; } else { root[lk] = value; } } function talkDel(path) { var parts = path.split('.'), root = talkRoot(); for (var i = 0; i < parts.length - 1; i++) { if (root == null) return; var k = parts[i]; root = /^\d+$/.test(k) ? root[parseInt(k, 10)] : root[k]; } if (!root) return; var lk = parts[parts.length - 1]; if (/^\d+$/.test(lk)) { root.splice(parseInt(lk, 10), 1); } else { delete root[lk]; } } // ---- Talk scopes for intereditor.js ---- // Each scope targets a specific condition or action object inside the talk // tree and handles re-render + talkSync on every mutation. // ---- Talk scopes for intereditor.js ---- // Each scope targets a specific condition or action object inside the talk // tree and handles re-render + talkSync on every mutation. var _talkScopeSeq = 0; function talkCondScope(nodeKey, optIdx) { var token = 'tk' + (++_talkScopeSeq); return ie_registerScope({ getCond: function() { var t = talkTarget(nodeKey, optIdx); return t && t.condition || null; }, setCond: function(c) { var t = talkTarget(nodeKey, optIdx); if (!t) return; if (c) t.condition = c; else delete t.condition; }, onChange: function() { talkSync(); renderTalkTree(window._talkData); } }, token); } function talkActionScope(nodeKey, optIdx) { var token = 'tk' + (++_talkScopeSeq); return ie_registerScope({ getStep: function() { var t = talkTarget(nodeKey, optIdx); return t && t.action || {}; }, setStep: function(s) { var t = talkTarget(nodeKey, optIdx); if (!t) return; if (s && Object.keys(s).length > 0) t.action = s; else delete t.action; }, onChange: function() { talkSync(); renderTalkTree(window._talkData); } }, token); } function talkTarget(nodeKey, optIdx) { var node = talkNodes() && talkNodes()[nodeKey]; if (!node) return null; if (optIdx !== null) { if (!node.options || !node.options[optIdx]) return null; return node.options[optIdx]; } return node; } // ---- Render a single action step for a talk node or option ---- function talkRenderActionStep(nodeKey, optIdx) { var t = talkTarget(nodeKey, optIdx); var step = (t && t.action) || {}; var scopeToken = talkActionScope(nodeKey, optIdx); var h = '
'; h += ie_renderMessages(step, '', 0, 0, ''); h += ie_renderAct(step, '', 0, 0, ''); h += '
' + ie_renderStepAddbar(step, '', 0, 0, '') + '
'; h += '
'; return h; } // ---- talk cond/act add/remove helpers (called from onclick data-action) ---- function talkAddNodeCond(nodeKey) { var node = talkNodes() && talkNodes()[nodeKey]; if (!node || (node.condition && typeof node.condition === 'object')) return; node.condition = {all_of: []}; renderTalkTree(window._talkData); talkSync(); } function talkRemoveNodeCond(nodeKey) { var node = talkNodes() && talkNodes()[nodeKey]; if (!node) return; delete node.condition; renderTalkTree(window._talkData); talkSync(); } function talkAddNodeAct(nodeKey) { var node = talkNodes() && talkNodes()[nodeKey]; if (!node || (node.action && typeof node.action === 'object')) return; node.action = {}; renderTalkTree(window._talkData); talkSync(); } function talkRemoveNodeAct(nodeKey) { var node = talkNodes() && talkNodes()[nodeKey]; if (!node) return; delete node.action; renderTalkTree(window._talkData); talkSync(); } function talkAddOptionCond(nodeKey, optIdx) { var t = talkTarget(nodeKey, optIdx); if (!t || (t.condition && typeof t.condition === 'object')) return; t.condition = {all_of: []}; renderTalkTree(window._talkData); talkSync(); } function talkRemoveOptionCond(nodeKey, optIdx) { var t = talkTarget(nodeKey, optIdx); if (!t) return; delete t.condition; renderTalkTree(window._talkData); talkSync(); } function talkAddOptionAct(nodeKey, optIdx) { var t = talkTarget(nodeKey, optIdx); if (!t || (t.action && typeof t.action === 'object')) return; t.action = {}; renderTalkTree(window._talkData); talkSync(); } function talkRemoveOptionAct(nodeKey, optIdx) { var t = talkTarget(nodeKey, optIdx); if (!t) return; delete t.action; renderTalkTree(window._talkData); talkSync(); } // ---- Event delegation ---- function talkEvent(e) { var el = e.target; var path = el.getAttribute('data-tp'); if (!path) return; var tag = el.tagName; var type = el.type; if (tag === 'INPUT' || tag === 'TEXTAREA') { var val; if (type === 'checkbox') { val = el.checked; } else if (type === 'number') { var n = parseFloat(el.value); val = isNaN(n) ? '' : n; } else { val = el.value; } talkSyncPath(path, val); if (path.endsWith('.goto') && e.type === 'change') renderTalkTree(window._talkData); } else if (tag === 'SELECT') { talkSyncPath(path, el.value); } } function talkSyncPath(path, val) { talkSet(path, val); talkSync(); } function talkHandleClick(e) { var el = e.target; var action = el.getAttribute('data-action'); if (!action) return; var nodeKey = el.getAttribute('data-nk'); var optIdx = el.getAttribute('data-oi'); switch (action) { case 'delete-node': deleteNode(nodeKey); break; case 'add-option': addOption(nodeKey); break; case 'delete-option': deleteOption(nodeKey, parseInt(optIdx, 10)); break; case 'add-node': addNode(); break; case 'add-message': addMessageLine(nodeKey); break; case 'delete-message': deleteMessageLine(nodeKey, parseInt(optIdx, 10)); break; case 'add-node-cond': talkAddNodeCond(nodeKey); break; case 'remove-node-cond': talkRemoveNodeCond(nodeKey); break; case 'add-node-act': talkAddNodeAct(nodeKey); break; case 'remove-node-act': talkRemoveNodeAct(nodeKey); break; case 'add-opt-cond': talkAddOptionCond(nodeKey, parseInt(optIdx, 10)); break; case 'remove-opt-cond': talkRemoveOptionCond(nodeKey, parseInt(optIdx, 10)); break; case 'add-opt-act': talkAddOptionAct(nodeKey, parseInt(optIdx, 10)); break; case 'remove-opt-act': talkRemoveOptionAct(nodeKey, parseInt(optIdx, 10)); break; } } // ---- Render entry point ---- window.renderTalkTree = function(data) { var container = document.getElementById('talktree'); if (!container) return; // Purge old talk scopes before re-rendering (new ones will be registered). if (window._ieScopes) { Object.keys(window._ieScopes).forEach(function(k) { if (k.indexOf('tk') === 0) delete window._ieScopes[k]; }); } _talkScopeSeq = 0; window._talkData = data; window._talkOpenNodes = window._talkOpenNodes || new Set(); window._talkOpenNodes.add('start'); if (!data || !data.talk || !data.talk.nodes) { container.innerHTML = '

No conversation tree defined.

'; return; } var nodes = data.talk.nodes; var visited = {}; var html = '
'; html += ''; Object.keys(nodes).forEach(function(k) { html += ''; html += renderTreeNodes(nodes, 'start', visited); var allKeys = Object.keys(nodes); var orphanKeys = []; for (var i = 0; i < allKeys.length; i++) { if (!visited[allKeys[i]]) orphanKeys.push(allKeys[i]); } if (orphanKeys.length > 0) { html += '
'; html += '
Unreferenced Nodes
'; html += '
These nodes are not reachable from start. Add a goto to them from another node\'s option.
'; for (var j = 0; j < orphanKeys.length; j++) { html += renderOrphanNode(orphanKeys[j], nodes[orphanKeys[j]], nodes); } html += '
'; } html += renderAddNodeRow(); html += '
'; container.innerHTML = html; if (typeof ced_setupSearchFields === 'function') { setTimeout(function() { ced_setupSearchFields(); }, 10); } }; // ---- Add/Delete Node ---- function renderAddNodeRow() { return '
' + '' + '' + '
'; } function addNode() { var input = document.getElementById('talkNewNodeKey'); if (!input) return; var key = input.value.trim(); if (!key) return; if (talkNodes()[key]) { notify('Node "' + key + '" already exists.', 'error'); return; } talkNodes()[key] = { messages: [], options: [] }; input.value = ''; renderTalkTree(window._talkData); talkSync(); } function deleteNode(nodeKey) { if (nodeKey === 'start') { notify('Cannot delete the start node.', 'error'); return; } var nodes = talkNodes(); if (!nodes || !nodes[nodeKey]) return; delete nodes[nodeKey]; Object.keys(nodes).forEach(function(k) { var n = nodes[k]; if (n.goto === nodeKey) delete n.goto; if (n.options) { n.options.forEach(function(opt) { if (opt.goto === nodeKey) opt.goto = ''; }); } }); renderTalkTree(window._talkData); talkSync(); } // ---- Orphan node ---- function renderOrphanNode(key, node, nodes) { var h = '
'; h += '
'; h += ''; h += '' + esc(key) + ''; h += '' + esc((getNodePreview(node) || '').substring(0, 60)) + ''; h += ''; h += ''; h += '
'; h += '
'; h += renderNodeBody(key, node, nodes); h += '
'; h += '
'; return h; } // ---- Recursive tree render ---- function renderTreeNodes(nodes, rootKey, seen, indent) { indent = indent || 0; var node = nodes[rootKey]; if (!node) return ''; if (seen[rootKey]) { return '
' + esc(rootKey) + ' (see above)
'; } seen[rootKey] = true; var previewText = getNodePreview(node); var isOpen = window._talkOpenNodes.has(rootKey); var html = '
'; html += '
'; html += ''; html += '' + esc(rootKey) + ''; html += '' + esc((previewText || '').substring(0, 60)) + ''; html += ''; html += ''; html += '
'; html += '
'; html += renderNodeBody(rootKey, node, nodes); html += '
'; var childKeys = []; if (node.goto && nodes[node.goto]) childKeys.push(node.goto); if (node.options) { node.options.forEach(function(opt) { if (opt.goto && nodes[opt.goto] && childKeys.indexOf(opt.goto) < 0) childKeys.push(opt.goto); }); } if (childKeys.length > 0) { html += '
'; childKeys.forEach(function(k) { html += renderTreeNodes(nodes, k, seen, indent + 1); }); html += '
'; } html += '
'; return html; } function getNodePreview(node) { if (node.messages && node.messages.length) return node.messages[0]; return ''; } // ---- Node body: condition, messages, options, action via intereditor ---- window._talkOpenNodes = window._talkOpenNodes || new Set(); function renderNodeBody(nodeKey, node, nodes) { var hasCond = !!(node.condition && typeof node.condition === 'object'); var hasAct = !!(node.action && typeof node.action === 'object' && !Array.isArray(node.action)); var hasOpts = node.options && node.options.length > 0; var html = ''; // Node condition (via intereditor) if (hasCond) { html += '
'; html += '
' + 'Condition (for this node)' + '' + '
'; html += '
'; html += ie_renderCond(node.condition, '', 0, '', '', {scopeToken: talkCondScope(nodeKey, null), alwaysOuterAddbar: true}); html += '
'; html += '
'; } else { html += ''; } // Node messages (unchanged) html += renderMessagesSection(nodeKey, node); // Node options html += renderOptionsSection(nodeKey, node); html += '
'; // Node action (via intereditor) if (hasAct) { html += '
'; html += '
' + 'Action (at the start of this node)' + '
'; html += '
'; html += talkRenderActionStep(nodeKey, null); html += '
'; html += '
'; } else { html += ''; } // Goto fallback if (hasCond) { html += '
'; html += renderGotoFallback(nodeKey, node, 'If condition fails, go to'); } else if (!hasOpts) { html += renderGotoFallback(nodeKey, node, 'Auto-advance to'); } return html; } // ---- Goto fallback ---- function renderGotoFallback(nodeKey, node, label) { label = label || 'Auto-advance to'; return '
' + '' + esc(label) + '' + '' + '
'; } // ---- Messages section ---- function renderMessagesSection(nodeKey, node) { var msgs = node.messages || []; var html = '
'; html += '
Messages
'; msgs.forEach(function(line, i) { html += '
' + '' + '' + '' + '
'; }); html += '
' + '' + '' + '' + '
'; html += '
'; return html; } // ---- Options section ---- function renderOptionsSection(nodeKey, node) { var options = node.options || []; var html = '
'; html += 'Options'; if (options.length > 0) { options.forEach(function(opt, idx) { html += '
'; html += '' + (idx + 1) + ''; html += ''; html += ''; html += '
'; if (opt.condition && typeof opt.condition === 'object' && !Array.isArray(opt.condition)) { html += 'cond'; } else { html += ''; } if (opt.action) { html += 'act'; } else { html += ''; } html += '
'; html += ''; html += '
'; // Option condition detail if (opt.condition && typeof opt.condition === 'object' && !Array.isArray(opt.condition)) { html += '
'; html += '
'; html += '
Condition (for this option to show up)' + '
'; html += '
'; html += ie_renderCond(opt.condition, '', 0, '', '', {scopeToken: talkCondScope(nodeKey, idx), alwaysOuterAddbar: true}); html += '
'; html += '
'; html += '
'; } // Option action detail if (opt.action) { html += '
'; html += '
'; html += '
' + 'Action (when this option is chosen)' + '
'; html += '
'; html += talkRenderActionStep(nodeKey, idx); html += '
'; html += '
'; html += '
'; } }); } else { html += '
none
'; } html += '
' + '' + (options.length + 1) + '' + '' + '' + '' + '' + '
'; html += '
'; return html; } window.talkToggleNode = function(nodeKey, nodeEl) { var open = nodeEl.classList.toggle('open'); if (open) { window._talkOpenNodes.add(nodeKey); } else { window._talkOpenNodes.delete(nodeKey); } }; // ---- Add/Delete Option ---- function addOption(nodeKey) { var textInput = document.getElementById('talkOptText_' + nodeKey); var gotoInput = document.getElementById('talkOptGoto_' + nodeKey); var text = textInput ? textInput.value.trim() : ''; var dest = gotoInput ? gotoInput.value.trim() : ''; var node = talkNodes()[nodeKey]; if (!node) return; if (!node.options) node.options = []; node.options.push({ text: text, goto: dest }); if (textInput) textInput.value = ''; if (gotoInput) gotoInput.value = ''; renderTalkTree(window._talkData); talkSync(); } function deleteOption(nodeKey, idx) { var node = talkNodes()[nodeKey]; if (!node || !node.options || idx < 0 || idx >= node.options.length) return; node.options.splice(idx, 1); renderTalkTree(window._talkData); talkSync(); } // ---- Add/Delete Sequence Line ---- function addMessageLine(nodeKey) { var input = document.getElementById('talkMsgAdd_' + nodeKey); var text = input ? input.value.trim() : ''; var node = talkNodes()[nodeKey]; if (!node) return; if (!node.messages) node.messages = []; node.messages.push(text); renderTalkTree(window._talkData); talkSync(); } function deleteMessageLine(nodeKey, idx) { var node = talkNodes()[nodeKey]; if (!node || !node.messages) return; node.messages.splice(idx, 1); if (node.messages.length === 0) delete node.messages; renderTalkTree(window._talkData); talkSync(); } // ---- Global event listeners ---- document.addEventListener('input', function(e) { var el = e.target; if (!el.closest || !el.closest('#talktree')) return; if (!el.getAttribute('data-tp')) return; talkEvent(e); }); document.addEventListener('change', function(e) { var el = e.target; if (!el.closest || !el.closest('#talktree')) return; talkEvent(e); }); document.addEventListener('click', function(e) { var el = e.target; if (!el.closest || !el.closest('#talktree')) return; talkHandleClick(e); }); document.addEventListener('keydown', function(e) { if (e.key !== 'Enter') return; var el = e.target; if (!el.closest || !el.closest('#talktree')) return; if (el.id === 'talkNewNodeKey') { e.preventDefault(); addNode(); return; } if (el.id && (el.id.startsWith('talkOptText_') || el.id.startsWith('talkOptGoto_'))) { e.preventDefault(); var nodeKey = el.id.replace('talkOptText_', '').replace('talkOptGoto_', ''); addOption(nodeKey); return; } if (el.id && el.id.startsWith('talkMsgAdd_')) { e.preventDefault(); var nk = el.id.replace('talkMsgAdd_', ''); addMessageLine(nk); return; } }); })();