// 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 += '';
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 += '